Skip to content

[cuebot] Licensing limits - #2498

Draft
DiegoTavares wants to merge 4 commits into
AcademySoftwareFoundation:masterfrom
DiegoTavares:licensing-limits
Draft

[cuebot] Licensing limits#2498
DiegoTavares wants to merge 4 commits into
AcademySoftwareFoundation:masterfrom
DiegoTavares:licensing-limits

Conversation

@DiegoTavares

@DiegoTavares DiegoTavares commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

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

  • A layer declares its needs via layer environment (CUE_LICENSES=hengine,katana) — the declaration is the switch; undeclared layers book exactly as before.
  • LicenseSource polls scheduler.license.provider (an http(s): endpoint or script: wrapping a vendor CLI, both returning the same JSON) into an in-memory sample; every Cuebot polls, so failover standbys stay warm.
  • LicenseBookingGate holds 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.
  • Host-based licenses (one seat per machine) are packed: hosts already holding a license get first shot at work needing it.
  • License-denied exit codes requeue without burning a retry, bounded by 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: LicenseInterface with GetAll/Find only — no mutations; the numbers belong to the license server.
  • Cuebot: LicenseSource.describe() + ManageLicense servant; provider credentials are redacted before the wire.
  • pycue: License/LicensingStatus wrappers, api.getLicenses() / getLicensingStatus() / findLicense().

Docs

configuring-application-licenses.md (concept, provider JSON contract, all scheduler.license.* properties, troubleshooting) and monitoring-licenses.md (the view), plus CueCommander reference updates.

Deploy note

V46 adds i_layer_env_str_key on layer_env; plain CREATE INDEX briefly write-locks the table — pre-create with CONCURRENTLY on 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

  • New Features
    • Added application-license-aware scheduling to prevent dispatching frames when required licenses are unavailable or stale.
    • Supports live license availability from HTTP endpoints or site scripts, including floating and host-based licenses.
    • Prioritizes eligible license-dependent jobs on hosts already holding required licenses.
  • Bug Fixes
    • License-denied frames can be requeued with bounded retry handling, while preserving existing retry behavior for other failures.
  • Documentation
    • Added configuration options for license providers, polling, staleness, headroom, and denial handling.

DiegoTavares and others added 2 commits August 4, 2026 13:30
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>
@DiegoTavares
DiegoTavares marked this pull request as draft August 4, 2026 23:44
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Cuebot now polls license providers, calculates floating and host-based budgets, gates dispatch, supports host license packing, and requeues configured license-denied frame exits.

Changes

License-aware dispatch

Layer / File(s) Summary
License provider and budget source
cuebot/src/main/java/com/imageworks/spcue/config/*, cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseSource.java, cuebot/src/main/resources/opencue.properties, cuebot/src/test/java/com/imageworks/spcue/dispatcher/LicenseSource*Tests.java
Registers LicenseSource, polls HTTP or script providers, validates samples, calculates budgets, and accounts for in-flight usage.
License booking gate
cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseBookingGate.java, cuebot/src/main/resources/conf/ddl/postgres/migrations/V46__Add_layer_env_key_index.sql, cuebot/src/test/java/com/imageworks/spcue/dispatcher/LicenseBookingGateTests.java
Resolves layer license declarations, enforces floating and host-based seat limits, and finds packable jobs.
Dispatch gating integration
cuebot/src/main/java/com/imageworks/spcue/dispatcher/CoreUnitDispatcher.java, cuebot/src/main/java/com/imageworks/spcue/dispatcher/LocalDispatcher.java, cuebot/src/main/resources/conf/spring/applicationContext-service.xml
Applies license sessions and booking checks to host, job, layer, frame, and proc dispatch paths.
Host-based license packing
cuebot/src/main/java/com/imageworks/spcue/dispatcher/HostReportHandler.java, cuebot/src/main/java/com/imageworks/spcue/dispatcher/commands/DispatchBookHostLicensePack.java, cuebot/src/test/java/com/imageworks/spcue/dispatcher/commands/DispatchBookHostLicensePackTests.java
Dispatches matching jobs before normal host booking and falls back to standard booking when packing cannot proceed.
Denied frame requeue handling
cuebot/src/main/java/com/imageworks/spcue/dispatcher/FrameCompleteHandler.java, cuebot/src/test/java/com/imageworks/spcue/dispatcher/FrameCompleteHandlerLicenseTests.java
Requeues configured license-denied exits within a bounded per-frame limit without consuming retries.

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
Loading

Possibly related PRs

Suggested reviewers: lithorus, ramonfigueiredo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the pull request's main change: adding licensing limits to Cuebot.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Use a transactionless concurrent index for layer_env.

CREATE INDEX takes an exclusive lock on layer_env, which blocks Cuebot writes during job launch. Use CREATE INDEX CONCURRENTLY and 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 win

Share the pack_jobs_max default with the command.

The default 5 appears here and again in DispatchBookHostLicensePack line 114. If one default changes, the gate and the command disagree. Move the property name and the default into a shared constant, for example on DispatchBookHostLicensePack.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8dff166 and 68a587b.

📒 Files selected for processing (18)
  • cuebot/src/main/java/com/imageworks/spcue/config/AppConfig.java
  • cuebot/src/main/java/com/imageworks/spcue/config/LicenseConfig.java
  • cuebot/src/main/java/com/imageworks/spcue/dispatcher/CoreUnitDispatcher.java
  • cuebot/src/main/java/com/imageworks/spcue/dispatcher/FrameCompleteHandler.java
  • cuebot/src/main/java/com/imageworks/spcue/dispatcher/HostReportHandler.java
  • cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseBookingGate.java
  • cuebot/src/main/java/com/imageworks/spcue/dispatcher/LicenseSource.java
  • cuebot/src/main/java/com/imageworks/spcue/dispatcher/LocalDispatcher.java
  • cuebot/src/main/java/com/imageworks/spcue/dispatcher/commands/DispatchBookHostLicensePack.java
  • cuebot/src/main/resources/conf/ddl/postgres/migrations/V46__Add_layer_env_key_index.sql
  • cuebot/src/main/resources/conf/spring/applicationContext-service.xml
  • cuebot/src/main/resources/opencue.properties
  • cuebot/src/test/java/com/imageworks/spcue/config/TestAppConfig.java
  • cuebot/src/test/java/com/imageworks/spcue/dispatcher/FrameCompleteHandlerLicenseTests.java
  • cuebot/src/test/java/com/imageworks/spcue/dispatcher/LicenseBookingGateTests.java
  • cuebot/src/test/java/com/imageworks/spcue/dispatcher/LicenseSourceInFlightTests.java
  • cuebot/src/test/java/com/imageworks/spcue/dispatcher/LicenseSourceTests.java
  • cuebot/src/test/java/com/imageworks/spcue/dispatcher/commands/DispatchBookHostLicensePackTests.java

Comment on lines +81 to +97
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();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.java

Repository: 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.

Comment on lines +267 to +279
} 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Comment on lines +349 to +356
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
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.

Comment on lines +424 to +451
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 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=java

Repository: 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 . || true

Repository: 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 || true

Repository: 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' || true

Repository: 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.

Comment on lines +109 to +110
LicenseBookingGate.Session licenseSession =
licenseBookingGate == null ? null : licenseBookingGate.newSession(host.getName());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant