[cuebot] Licensing limits - #2498
Conversation
This work is manually cherry-picked from AcademySoftwareFoundation#2495 as an attempt to isolate this logic from the overall scheduler redesign. **Description by original owner** Layers declare licenses in their environment (CUE_LICENSES=hengine,katana); the planner books them only while the license server reports free seats. Seats are per frame (floating) or per machine (host_based, shared by all frames on the machine, packed onto as few machines as possible). LicenseSource polls an http endpoint or a script wrapping the vendor CLI: scheduler.license.provider=http://lic-reporter:9101/licenses scheduler.license.provider=script:/site/bin/cue_licenses.sh Either returns the same JSON (hosts is optional; available is server truth, net of every consumer): {"queried_at": <epoch s>, "licenses": [{"name": "hengine", "total": 800, "available": 794, "host_based": false, "hosts": [{"host": "wolf1018", "count": 1}]}]} Tuning, all under scheduler.license.*: poll_seconds, timeout_seconds, stale_seconds, inflight_pad_seconds, headroom.<name> (seats withheld per license, e.g. headroom.hengine=5), env_key (default CUE_LICENSES), and denied_exit_statuses (vendor exit codes meaning "no license").
Apply the LicenseSource budgets (introduced in the previous commit) to Cuebot's own booking logic. A layer that declares application licenses in its environment (CUE_LICENSES=hengine,katana) is now only dispatched while the license server reports a free seat in every pool it lists; everything else books exactly as before. The sensitive DispatchQuery SQL is untouched: license state lives in-process, so the gate runs in Java on the candidate frames the existing queries return. LicenseBookingGate - Resolves each layer's declaration from layer_env through a 10-minute cache, so unlicensed farms pay only an indexed single-row lookup per layer per cache period. - A per-booking-pass Session snapshots budgets lazily (at most one in-flight read per pass) and does pass-local accounting: floating pools lose a seat per booked frame; host-based pools are free on already-seated hosts and only open new seats under the seat cap. Stale sample, unknown license, missing provider, or a failed lookup all fail closed. - Gated paths: CoreUnitDispatcher.dispatchHost/dispatchProcToJob and all four LocalDispatcher paths -- artists' local bookings draw from the same pools as render nodes. A held layer is skipped, never breaking the loop, so other layers of the same job still book. License packing (host-based licenses) - When a host report shows running frames holding host-based licenses, jobs whose waiting layers need those licenses get the first shot at that host's idle resources: an extra frame on a seated machine shares its one checkout, while a fresh machine burns a seat. - The report thread only runs a zero-DB trigger (pack_jobs_max > 0, then cached layer lookups); the budget snapshot, the pack-job query (hard LIMIT, priority-ordered, facility/OS-filtered) and the dispatching run in the new DispatchBookHostLicensePack command on a booking thread. The command ends with the exact booking the host would have received without packing, preferred show included. License-denied requeue bound - scheduler.license.denied_requeue_limit (default 10) caps how many times one frame may take the free SKIP_RETRY requeue for a license-denied exit. Genuine denials are rare races (the gate holds layers whose pool is full); a frame denied repeatedly is misconfigured and now falls back to ordinary retry accounting instead of bouncing forever. The OOM pre-mark wins over a denied exit code, and both decision sites read the same status against the same count. Schema: V46 adds i_layer_env_str_key ON layer_env (str_key, pk_layer), which all the licensing queries drive from; layer_env previously had no key index. Deploy note: plain CREATE INDEX briefly write-locks layer_env; on large installs pre-create it with CONCURRENTLY before upgrading, or migrate in a quiet window. Wiring: licenseSource/licenseBookingGate beans move to a shared LicenseConfig imported by both AppConfig and TestAppConfig; the XML dispatcher, localDispatcher and hostReportHandler beans reference the gate. Tests: 38 licensing unit tests across five classes, none needing Spring or a database -- gate session semantics (floating consumption, multi-pool all-or-nothing, seat/cap logic, fail-closed paths), LicenseSource in-flight row mapping (recent frames reduce floating budgets, running hosts join seat sets, recently seated hosts cancel out of the cap), determineFrameState license-denied behaviour incl. the requeue cap, and the pack command flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughCuebot now polls license providers, calculates floating and host-based budgets, gates dispatch, supports host license packing, and requeues configured license-denied frame exits. ChangesLicense-aware dispatch
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant LicenseProvider
participant LicenseSource
participant LicenseBookingGate
participant CoreUnitDispatcher
participant FrameCompleteHandler
LicenseProvider->>LicenseSource: return timestamped license data
LicenseSource->>LicenseBookingGate: expose current budgets
CoreUnitDispatcher->>LicenseBookingGate: check layer capacity
LicenseBookingGate-->>CoreUnitDispatcher: allow or skip frame
FrameCompleteHandler->>FrameCompleteHandler: classify license-denied exit
FrameCompleteHandler-->>CoreUnitDispatcher: requeue eligible frame
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
cuebot/src/main/resources/conf/ddl/postgres/migrations/V46__Add_layer_env_key_index.sql (1)
7-7: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winUse a transactionless concurrent index for
layer_env.
CREATE INDEXtakes an exclusive lock onlayer_env, which blocks Cuebot writes during job launch. UseCREATE INDEX CONCURRENTLYand run this Flyway migration outside the default transaction, the same operational pattern documented for existing concurrent-style indexes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cuebot/src/main/resources/conf/ddl/postgres/migrations/V46__Add_layer_env_key_index.sql` at line 7, Update the V46 migration to create the layer_env index with CREATE INDEX CONCURRENTLY, and configure this migration to run outside Flyway’s default transaction using the existing concurrent-index migration pattern. Preserve the index name and columns (i_layer_env_str_key on str_key, pk_layer).Source: Linters/SAST tools
cuebot/src/main/java/com/imageworks/spcue/dispatcher/HostReportHandler.java (1)
358-358: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShare the
pack_jobs_maxdefault with the command.The default
5appears here and again inDispatchBookHostLicensePackline 114. If one default changes, the gate and the command disagree. Move the property name and the default into a shared constant, for example onDispatchBookHostLicensePack.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cuebot/src/main/java/com/imageworks/spcue/dispatcher/HostReportHandler.java` at line 358, The scheduler.license.pack_jobs_max property name and default value are duplicated between HostReportHandler and DispatchBookHostLicensePack. Define shared constants on DispatchBookHostLicensePack for the property key and default of 5, then update both the HostReportHandler gate and DispatchBookHostLicensePack command to reuse them.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@cuebot/src/main/java/com/imageworks/spcue/dispatcher/commands/DispatchBookHostLicensePack.java`:
- Around line 81-97: No code change is required in the fallback dispatch logic
around DispatchBookHostLicensePack and DispatchBookHost. Preserve the existing
nested DispatchCommandTemplate.execute() calls and separate per-pass
LicenseBookingGate.Session creation for each dispatchHost(host, packJob)
invocation.
In
`@cuebot/src/main/java/com/imageworks/spcue/dispatcher/FrameCompleteHandler.java`:
- Around line 267-279: Update the license-denied handling in
FrameCompleteHandler so eligibility is calculated before stopFrame, but
countLicenseDeniedRequeue is invoked only after stopFrame succeeds. Make the
counter increment atomic to prevent duplicate or concurrent reports from
consuming requeue budget when stopFrame returns false, and add a regression test
covering duplicate reports.
In `@cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseSource.java`:
- Around line 349-356: Rate-limit the unknown-license warning in snapshotBudgets
alongside the existing no-provider and stale warnings. Reuse the established
throttle mechanism and its identifying key for each missing license name, while
preserving the current fail-closed LicenseBudget result and control flow.
- Around line 424-451: Add a short-TTL cache for InFlight results used by
snapshotBudgets, keyed by the sample age and the wanted license set. Have
snapshotBudgets reuse a matching cached value before invoking readInFlight,
while preserving the existing inflightPadSeconds and padded-accounting behavior.
Ensure cache keys safely represent equivalent license sets and that
hostBasedLicensesRunning benefits through snapshotBudgets as well.
In `@cuebot/src/main/java/com/imageworks/spcue/dispatcher/LocalDispatcher.java`:
- Around line 109-110: Update dispatchHost(DispatchHost) to create one
LicenseBookingGate.Session before iterating LocalHostAssignment values, then
pass that session through the job, layer, and frame helper overloads instead of
creating per-assignment sessions. After each successful frame-partition booking,
invoke booked(dframe.getLayerId()) on the shared session, and add coverage for
multiple assignments sharing one available license seat.
---
Nitpick comments:
In `@cuebot/src/main/java/com/imageworks/spcue/dispatcher/HostReportHandler.java`:
- Line 358: The scheduler.license.pack_jobs_max property name and default value
are duplicated between HostReportHandler and DispatchBookHostLicensePack. Define
shared constants on DispatchBookHostLicensePack for the property key and default
of 5, then update both the HostReportHandler gate and
DispatchBookHostLicensePack command to reuse them.
In
`@cuebot/src/main/resources/conf/ddl/postgres/migrations/V46__Add_layer_env_key_index.sql`:
- Line 7: Update the V46 migration to create the layer_env index with CREATE
INDEX CONCURRENTLY, and configure this migration to run outside Flyway’s default
transaction using the existing concurrent-index migration pattern. Preserve the
index name and columns (i_layer_env_str_key on str_key, pk_layer).
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 659c0646-0bb5-4927-bca6-0965b5272c92
📒 Files selected for processing (18)
cuebot/src/main/java/com/imageworks/spcue/config/AppConfig.javacuebot/src/main/java/com/imageworks/spcue/config/LicenseConfig.javacuebot/src/main/java/com/imageworks/spcue/dispatcher/CoreUnitDispatcher.javacuebot/src/main/java/com/imageworks/spcue/dispatcher/FrameCompleteHandler.javacuebot/src/main/java/com/imageworks/spcue/dispatcher/HostReportHandler.javacuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseBookingGate.javacuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseSource.javacuebot/src/main/java/com/imageworks/spcue/dispatcher/LocalDispatcher.javacuebot/src/main/java/com/imageworks/spcue/dispatcher/commands/DispatchBookHostLicensePack.javacuebot/src/main/resources/conf/ddl/postgres/migrations/V46__Add_layer_env_key_index.sqlcuebot/src/main/resources/conf/spring/applicationContext-service.xmlcuebot/src/main/resources/opencue.propertiescuebot/src/test/java/com/imageworks/spcue/config/TestAppConfig.javacuebot/src/test/java/com/imageworks/spcue/dispatcher/FrameCompleteHandlerLicenseTests.javacuebot/src/test/java/com/imageworks/spcue/dispatcher/LicenseBookingGateTests.javacuebot/src/test/java/com/imageworks/spcue/dispatcher/LicenseSourceInFlightTests.javacuebot/src/test/java/com/imageworks/spcue/dispatcher/LicenseSourceTests.javacuebot/src/test/java/com/imageworks/spcue/dispatcher/commands/DispatchBookHostLicensePackTests.java
| for (JobInterface packJob : findPackJobs()) { | ||
| if (!host.hasAdditionalResources(Dispatcher.CORE_POINTS_RESERVED_MIN, | ||
| memReservedMin, Dispatcher.GPU_UNITS_RESERVED_MIN, memGpuReservedMin)) { | ||
| break; | ||
| } | ||
| dispatcher.dispatchHost(host, packJob); | ||
| } | ||
|
|
||
| /* | ||
| * Fall through to the exact booking this host would have received without packing | ||
| * (DispatchBookHost ends with the generic remaining-resources booking). | ||
| */ | ||
| if (preferredShow != null) { | ||
| new DispatchBookHost(host, preferredShow, dispatcher, env).run(); | ||
| } else { | ||
| new DispatchBookHost(host, dispatcher, env).run(); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the template used by both commands.
fd -a 'DispatchCommandTemplate.java' cuebot/src --exec cat -n {}
# Inspect how the public dispatchHost overloads create license sessions.
ast-grep outline cuebot/src/main/java/com/imageworks/spcue/dispatcher/CoreUnitDispatcher.java --items all
rg -nP -C6 'dispatchHost\(DispatchHost host, JobInterface job\)|licenseBookingGate\.(session|newSession|openSession)' cuebot/src/main/java/com/imageworks/spcue/dispatcher/CoreUnitDispatcher.javaRepository: AcademySoftwareFoundation/OpenCue
Length of output: 5457
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate files =="
fd -a 'DispatchBookHost.java|LicenseBookingGate.java|License' cuebot/src --max-depth 4 | sed -n '1,120p'
echo
echo "== DispatchBookHost outline =="
ast-grep outline cuebot/src/main/java/com/imageworks/spcue/dispatcher/commands/DispatchBookHost.java --view expanded || true
echo
echo "== DispatchBookHost run and class-local fields =="
sed -n '1,260p' cuebot/src/main/java/com/imageworks/spcue/dispatcher/commands/DispatchBookHost.java
echo
echo "== CoreUnitDispatcher dispatchHost and license session usage =="
sed -n '240,320p' cuebot/src/main/java/com/imageworks/spcue/dispatcher/CoreUnitDispatcher.java
echo
echo "== LicenseBookingGate implementation =="
sed -n '1,260p' cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseBookingGate.java
echo
echo "== licenseBookingGate sessions/canBook/book calls =="
rg -n -C 3 'newSession\(host\.getName\(\)\)|LicenseBookingGate\.Session|canBook\(|new LicenseBooking|bookingGate|licenseBookingGate\.' cuebot/src/main/java/com/imageworks/spcue/dispatcher -g '*.java'
echo
echo "== transaction/scheduler command invoke context =="
rg -n -C 4 'wrapDispatchCommand\(\)|execute\(\)|DispatchCommandTemplate|Dispatcher \)|coreUnit' cuebot/src/main/java/cuebot/src cuebot/src/main/java/com/imageworks/spcue -g '*.java' | sed -n '1,220p'Repository: AcademySoftwareFoundation/OpenCue
Length of output: 50389
No change needed. Nested DispatchCommandTemplate.execute() calls use instance templates and do not affect each other, and each dispatchHost(host, packJob) creates a separate per-pass LicenseBookingGate.Session, so existing pack jobs can overbook the host-based license for this pack pass.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@cuebot/src/main/java/com/imageworks/spcue/dispatcher/commands/DispatchBookHostLicensePack.java`
around lines 81 - 97, No code change is required in the fallback dispatch logic
around DispatchBookHostLicensePack and DispatchBookHost. Preserve the existing
nested DispatchCommandTemplate.execute() calls and separate per-pass
LicenseBookingGate.Session creation for each dispatchHost(host, packJob)
invocation.
| } else if (isLicenseDenied(exitStatus) | ||
| && underLicenseDeniedLimit(report.getFrame().getFrameId())) { | ||
| // A frame that died because no application license was free is | ||
| // requeued by determineFrameState above. Persist it as SKIP_RETRY so | ||
| // the retry counter is not incremented when it runs again (the | ||
| // increment reads the frame's STORED exit status, so recording the | ||
| // vendor's own code here would spend a retry on a queue wait). | ||
| // Bounded per frame: past the limit the vendor status is stored and | ||
| // ordinary retry accounting resumes (see licenseDeniedRequeueLimit). | ||
| logger.info("frame " + frame.getName() + " could not get a license (exit " | ||
| + exitStatus + "); requeueing without spending a retry"); | ||
| countLicenseDeniedRequeue(report.getFrame().getFrameId()); | ||
| exitStatus = FrameExitStatus.SKIP_RETRY_VALUE; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Count a license-denied requeue only after stopFrame succeeds.
This code increments licenseDeniedRequeues before line 282 persists the completion update. A duplicate or concurrent report can make stopFrame return false but still consume requeue budget. Later reports then bypass the license-denied path and can move the frame to DEAD.
Calculate eligibility before stopFrame. Increment the count only after stopFrame returns true. Make the increment atomic. Add a duplicate-report regression test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@cuebot/src/main/java/com/imageworks/spcue/dispatcher/FrameCompleteHandler.java`
around lines 267 - 279, Update the license-denied handling in
FrameCompleteHandler so eligibility is calculated before stopFrame, but
countLicenseDeniedRequeue is invoked only after stopFrame succeeds. Make the
counter increment atomic to prevent duplicate or concurrent reports from
consuming requeue budget when stopFrame returns false, and add a regression test
covering duplicate reports.
| if (st == null) { | ||
| // The layer asks for a license the provider does not report. We | ||
| // have no authority on it, so hold rather than assume it is free. | ||
| logger.warn("LicenseSource: no data for license '" + name | ||
| + "' requested by a layer; holding its frames"); | ||
| out.put(name, new LicenseBudget(name, false, 0, 0, Collections.emptySet(), true)); | ||
| continue; | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Throttle the unknown-license warning.
The no-provider warning (line 292) and the stale warning (line 316) are both rate limited. This warning is not. snapshotBudgets runs once per dispatch pass and once per host report that involves licensed layers, so one misspelled license name in one layer emits a WARN on every pass, on every Cuebot. The fail-closed result is correct; only the log volume is wrong.
♻️ Proposed fix: reuse the existing throttle pattern
+ private final Map<String, Long> lastUnknownWarnMs = new ConcurrentHashMap<>(); LicenseState st = s.licenses.get(name);
if (st == null) {
// The layer asks for a license the provider does not report. We
// have no authority on it, so hold rather than assume it is free.
- logger.warn("LicenseSource: no data for license '" + name
- + "' requested by a layer; holding its frames");
+ long nowMs = System.currentTimeMillis();
+ Long last = lastUnknownWarnMs.get(name);
+ if (last == null || nowMs - last > STALE_WARN_INTERVAL_MS) {
+ lastUnknownWarnMs.put(name, nowMs);
+ logger.warn("LicenseSource: no data for license '" + name
+ + "' requested by a layer; holding its frames");
+ }
out.put(name, new LicenseBudget(name, false, 0, 0, Collections.emptySet(), true));
continue;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (st == null) { | |
| // The layer asks for a license the provider does not report. We | |
| // have no authority on it, so hold rather than assume it is free. | |
| logger.warn("LicenseSource: no data for license '" + name | |
| + "' requested by a layer; holding its frames"); | |
| out.put(name, new LicenseBudget(name, false, 0, 0, Collections.emptySet(), true)); | |
| continue; | |
| } | |
| if (st == null) { | |
| // The layer asks for a license the provider does not report. We | |
| // have no authority on it, so hold rather than assume it is free. | |
| long nowMs = System.currentTimeMillis(); | |
| Long last = lastUnknownWarnMs.get(name); | |
| if (last == null || nowMs - last > STALE_WARN_INTERVAL_MS) { | |
| lastUnknownWarnMs.put(name, nowMs); | |
| logger.warn("LicenseSource: no data for license '" + name | |
| "' requested by a layer; holding its frames"); | |
| } | |
| out.put(name, new LicenseBudget(name, false, 0, 0, Collections.emptySet(), true)); | |
| continue; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseSource.java`
around lines 349 - 356, Rate-limit the unknown-license warning in
snapshotBudgets alongside the existing no-provider and stale warnings. Reuse the
established throttle mechanism and its identifying key for each missing license
name, while preserving the current fail-closed LicenseBudget result and control
flow.
| private InFlight readInFlight(long ageSeconds, Set<String> wanted) { | ||
| InFlight f = new InFlight(); | ||
| // One pass over running licensed frames. Driven from layer_env (indexed on | ||
| // the key) so a farm whose licensed layers are a small slice of the whole | ||
| // does not pay for the rest. | ||
| jdbc.query("SELECT le.str_value AS lic, f.str_host AS host, " | ||
| + " (f.ts_started > now() - CAST(? AS INTERVAL)) AS recent " + "FROM layer_env le " | ||
| + "JOIN frame f ON f.pk_layer = le.pk_layer " | ||
| + "WHERE le.str_key = ? AND f.str_state = 'RUNNING'", rs -> { | ||
| boolean recent = rs.getBoolean("recent"); | ||
| String host = rs.getString("host"); | ||
| for (String name : splitNames(rs.getString("lic"))) { | ||
| if (!wanted.contains(name)) | ||
| continue; | ||
| if (recent) | ||
| f.framesByLicense.merge(name, 1, Integer::sum); | ||
| if (host != null && !host.isEmpty()) { | ||
| String h = host.toLowerCase(); | ||
| f.hostsByLicense.computeIfAbsent(name, k -> new HashSet<>()).add(h); | ||
| if (recent) { | ||
| f.recentHostsByLicense.computeIfAbsent(name, k -> new HashSet<>()) | ||
| .add(h); | ||
| } | ||
| } | ||
| } | ||
| }, ageSeconds + " seconds", envKey); | ||
| return f; | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find every caller of snapshotBudgets and the dispatch/report entry points that reach it.
set -euo pipefail
rg -nP -C6 '\bsnapshotBudgets\s*\(' --type=java
# Where booking sessions and packing helpers are created (one per pass / per report).
rg -nP -C6 '\bnewSession\s*\(|\bhostBasedLicensesRunning\s*\(|\banyLicensedLayers\s*\(' --type=javaRepository: AcademySoftwareFoundation/OpenCue
Length of output: 171
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Tracked Java files matching License:"
git ls-files '*.java' | grep -i license || true
echo
echo "Locate LicenseSource.java and surrounding symbols:"
if [ -f cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseSource.java ]; then
wc -l cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseSource.java
sed -n '360,470p' cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseSource.java | cat -n -v
fi
echo
echo "Search snapshotBudgets/readInFlight/booking gates:"
rg -n -C4 'snapshotBudgets|readInFlight|hostBasedLicensesRunning|anyLicensedLayers|newSession|ensureBudgets|LicenseBookingGate' --type=java . || trueRepository: AcademySoftwareFoundation/OpenCue
Length of output: 50389
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "LicenseBookingGate relevant sections:"
wc -l cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseBookingGate.java
sed -n '1,260p' cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseBookingGate.java | cat -n
echo
echo "LicenseSource poll/snapshot relevant sections:"
sed -n '90,230p' cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseSource.java | cat -n
echo
echo "Session callers in production + tests, focused:"
rg -n -C3 'newSession\s*\(|hostBasedLicensesRunning\s*\(|findPackableJobs\s*\(|ensureBudgets\s*\(|snapshotBudgets\s*\(' cuebot/src/main cuebot/src/test/java/com/imageworks/spcue/dispatcher || trueRepository: AcademySoftwareFoundation/OpenCue
Length of output: 50389
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "LicenseSource snapshotBudgets and related fields:"
sed -n '180,340p' cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseSource.java | cat -n
echo
echo "LicenseBookingGate host-based/packing section:"
sed -n '250,315p' cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseBookingGate.java | cat -n
echo
echo "Production dispatch/report host booking callers around gate:"
rg -n -C4 'newSession\s*\(|canBook\s*\(|hostBasedLicensesRunning\s*\(|findPackableJobs\s*\(' cuebot/src/main/java com --glob '*.java' || trueRepository: AcademySoftwareFoundation/OpenCue
Length of output: 30401
Memoize the in-flight DB correction before every snapshot.
snapshotBudgets builds the DB correction from readInFlight each time it is called with a fresh sample and inflightPadSeconds is positive. Dispatch sessions are recreated per host, and their budget lookup uses a pass-local map, so one dispatch pass can call snapshotBudgets for every new license it needs. hostBasedLicensesRunning also calls it during host packing. A short TTL cache for InFlight keyed by the sample age and wanted license set folds these DB trips without changing the padded accounting semantics.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseSource.java`
around lines 424 - 451, Add a short-TTL cache for InFlight results used by
snapshotBudgets, keyed by the sample age and the wanted license set. Have
snapshotBudgets reuse a matching cached value before invoking readInFlight,
while preserving the existing inflightPadSeconds and padded-accounting behavior.
Ensure cache keys safely represent equivalent license sets and that
hostBasedLicensesRunning benefits through snapshotBudgets as well.
| LicenseBookingGate.Session licenseSession = | ||
| licenseBookingGate == null ? null : licenseBookingGate.newSession(host.getName()); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Use one LicenseBookingGate.Session for the complete local host pass.
dispatchHost(DispatchHost) can process multiple LocalHostAssignment values. These methods create separate sessions, so each assignment can start from a separate license budget snapshot. This can overbook a floating license shared by local assignments on the same host.
Create one session before the assignment loop. Pass it to the job, layer, and frame helper overloads. After a successful frame-partition booking, call booked(dframe.getLayerId()) on that shared session. Add a test with multiple assignments and one available license seat.
Also applies to: 207-208, 311-314
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cuebot/src/main/java/com/imageworks/spcue/dispatcher/LocalDispatcher.java`
around lines 109 - 110, Update dispatchHost(DispatchHost) to create one
LicenseBookingGate.Session before iterating LocalHostAssignment values, then
pass that session through the job, layer, and frame helper overloads instead of
creating per-assignment sessions. After each successful frame-partition booking,
invoke booked(dframe.getLayerId()) on the shared session, and add coverage for
multiple assignments sharing one available license seat.
The license gate ships blind: an operator cannot see which pools Cuebot knows about, how fresh the sample is, or why a licensed layer is being held. Surface the cached sample as a read-only inspection path ending in a CueGUI view alongside Limits. Proto: new license.proto with LicenseInterface (GetAll, Find). No mutations by design -- the numbers belong to the license server and the tuning (provider, headroom, poll cadence) is an opencue.properties concern. Licenses have no database id; the pool name is the identity. Cuebot: LicenseSource.describe() returns the poller state plus every license in the sample, each with its resolved headroom and this deploy's own usage (running licensed frames and distinct hosts, one indexed pass over layer_env/frame -- the same shape booking already pays per tick). Unlike snapshotBudgets it reports everything, counts all running frames rather than the in-flight window, and still lists licenses when the sample is stale: an operator debugging a dead provider needs the last numbers, not an empty table. The booking path is untouched. The new ManageLicense servant converts the view to protos; Find normalizes case and answers NOT_FOUND for pools outside the sample. The provider string is redacted before the wire (URL userinfo stripped greedily to the last @ bounded at /?#, query values masked) because the status travels to every CueGUI and sites embed credentials in license URLs; script command lines pass through and the docs say to keep secrets out of them. pycue: License and LicensingStatus wrappers plus api.getLicenses(), api.getLicensingStatus() and api.findLicense(); one GetAll round trip carries both the status and the licenses. CueGUI: Licenses view under Views/Plugins->Cuecommander. A status line states the poller's health in words (healthy, STALE -- licensed layers are held, waiting for first sample, provider not configured, cannot reach Cuebot) above a sortable read-only table: seats, availability, in-use, headroom, cue frames/hosts, provider hosts. A failed refresh clears the status with the rows it described, so a stale healthy line never sits over an empty table. Docs: configuring-application-licenseicensing feature (provider JSON contract, every scheduler.license.* property, denied-exit requeue, host-based packiote); monitoring-licenses.md covers the view; CueCommander references updated. Tests: 11 new Cuebot unit tests (describe states, headroom resolution, usage counting, servant conversion, Nction), none needing Spring or a database; 7 pycue wrapper/api tests; 7 CueGUI widget tests including the failed-ref Co-Authored-By: Claude Fable 5 <norep
Live application licensing with a read-only CueGUI Licenses view
A first take at managing host based and floating licenses on Opencue.
How it works
CUE_LICENSES=hengine,katana) — the declaration is the switch; undeclared layers book exactly as before.LicenseSourcepollsscheduler.license.provider(anhttp(s):endpoint orscript:wrapping a vendor CLI, both returning the same JSON) into an in-memory sample; every Cuebot polls, so failover standbys stay warm.LicenseBookingGateholds licensed frames in Java on the candidate frames the existing queries return — the sensitive dispatch SQL is untouched. Budget =available − in-flight − headroom; stale sample, unknown pool, or missing provider all fail closed.scheduler.license.denied_requeue_limit.Licenses view (new in this PR)
Read-only inspection surface ending in a Licenses view alongside Limits (Views/Plugins → Cuecommander): poller health status line plus per-pool seats, availability, in-use, headroom, and this deploy's own usage.
license.proto:LicenseInterfacewithGetAll/Findonly — no mutations; the numbers belong to the license server.LicenseSource.describe()+ManageLicenseservant; provider credentials are redacted before the wire.License/LicensingStatuswrappers,api.getLicenses()/getLicensingStatus()/findLicense().Docs
configuring-application-licenses.md(concept, provider JSON contract, allscheduler.license.*properties, troubleshooting) andmonitoring-licenses.md(the view), plus CueCommander reference updates.Deploy note
V46 adds
i_layer_env_str_keyonlayer_env; plainCREATE INDEXbriefly write-locks the table — pre-create withCONCURRENTLYon large installs.Testing
49 Cuebot licensing unit tests (no Spring/DB), 7 pycue wrapper/api tests, 7 CueGUI widget tests; pylint/spotless clean.
Summary by CodeRabbit