feat(dashboard): queue_on_property_save setting and the models waiting for their property to be saved

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-01 03:16:56 -05:00
parent 7729d14fa4
commit e42d44c804
5 changed files with 31 additions and 5 deletions

View File

@@ -473,6 +473,7 @@ namespace {
c.Add(Int(UGC, "worker_nice", "Worker priority", "Linux nice value of the workers: 0 normal, 19 only when nothing else wants the CPU.", "0", 0, 19));
c.Add(Unit(Int(UGC, "max_memory_mb", "Most memory for jobs", "Jobs whose estimated memory would go past this together wait for running ones; a bigger one runs alone. 0: no limit.", "0", 0, 1000000), "MB"));
c.Add(Unit(Int(UGC, "max_model_bricks", "Biggest model", "Models with more bricks fail with that reason. 0: no limit.", "0", 0, 1000000), "bricks"));
c.Add(Bool(UGC, "queue_on_property_save", "Only make models once their property is saved", "On: models already waiting (say, a server turning the UGC server on) aren't all made at once; a model is made once it is saved from Brick By Brick, made again by staff, or its property is saved. Off: every waiting model is made.", false));
c.Add(Text(UGC, "pause_hours", "Pause hours", "Local hours in which no new jobs start, e.g. 18-23 or 22-6. Empty: never.", ""));
c.Add(Unit(Int(UGC, "poll_interval_ms", "Look for new models every", "", "2000", 100, 600000, true), "ms"));
c.Add(Int(UGC, "poll_batch", "Models taken at once", "", "32", 1, 1000, true));

View File

@@ -264,6 +264,14 @@ namespace {
for (const auto& [state, count] : counts) out[StateName(state)] = count;
return out;
}
// The models' counts, with how many of the waiting ones wait for their property to be saved while
// queue_on_property_save is on (notQueued)
nlohmann::json ModelCounts() {
auto out = Counts(Database::Get()->GetUgcProcessCounts());
if (UgcSetting("queue_on_property_save").value_or("0") == "1") out["notQueued"] = Database::Get()->CountUnqueuedUgcModels();
return out;
}
}
namespace UgcRoutes {
@@ -385,7 +393,7 @@ namespace UgcRoutes {
}
if (searching) matches = { { "model", models(0, 1).second }, { "modular", total } };
}
JsonSuccess(reply, { { "counts", { { "model", Counts(Database::Get()->GetUgcProcessCounts()) }, { "modular", Counts(Database::Get()->GetModularBuildProcessCounts()) } } },
JsonSuccess(reply, { { "counts", { { "model", ModelCounts() }, { "modular", Counts(Database::Get()->GetModularBuildProcessCounts()) } } },
{ "totals", { { "model", Totals(Database::Get()->GetUgcProcessTotals(false)) }, { "modular", Totals(Database::Get()->GetUgcProcessTotals(true)) } } },
{ "items", items }, { "total", total }, { "page", page }, { "size", size }, { "more", static_cast<uint64_t>(page + 1) * size < total }, { "kinds", kinds },
{ "matches", matches }, { "ugcPublicUrl", Game::config->GetValue("ugc_public_url") }, { "canManage", Can(context, "ugc_manage") } });
@@ -530,7 +538,7 @@ namespace UgcRoutes {
const auto base = InternalUrl();
// The database only on this thread; the worker only fetches
nlohmann::json body = { { "success", true }, { "enabled", enabled }, { "online", online },
{ "counts", { { "model", Counts(Database::Get()->GetUgcProcessCounts()) }, { "modular", Counts(Database::Get()->GetModularBuildProcessCounts()) } } } };
{ "counts", { { "model", ModelCounts() }, { "modular", Counts(Database::Get()->GetModularBuildProcessCounts()) } } } };
Workers::Reply(reply, context, false, [base, enabled, body = std::move(body)](HTTPReply& out) mutable {
if (enabled) {
const auto fetched = CachedGet(base + "/status", eCache::STATUS);

View File

@@ -76,7 +76,9 @@
function countCard(title, c) {
return '<div class="col-md-6"><div class="card"><div class="card-body py-2"><div class="small text-body-secondary">' + esc(title) + '</div>' +
fmt.badge(c.done + ' made', 'success') + ' ' + fmt.badge(c.pending + ' waiting', 'secondary') + ' ' + fmt.badge(c.failed + ' failed', c.failed ? 'danger' : 'secondary') +
(c.empty ? ' ' + fmt.badge(c.empty + ' empty', 'light') : '') + '</div></div></div>';
(c.empty ? ' ' + fmt.badge(c.empty + ' empty', 'light') : '') +
(c.notQueued ? '<div class="small text-body-secondary mt-1" title="queue_on_property_save is on: a model is made once it is saved, made again, or its property is saved">' +
esc(Number(c.notQueued).toLocaleString()) + ' of the waiting ones wait for their property to be saved</div>' : '') + '</div></div></div>';
}
// What making them all took, and what it saved (the rows' recorded stats; ones made before a stat existed don't count)
function totalsCard(title, t) {

View File

@@ -842,10 +842,21 @@ need no change to have new models processed. Rows that failed are tried again up
`max_attempts` (default 3) times. Reprocessing (dashboard) sets rows back to `is_optimized = 0, process_attempts = 0`.
No master messages are needed for any of it.
### Queue on property save (`queue_on_property_save`)
Off by default. On, the UGC server only takes pending models with `ugc.queued = 1` (mysql 104 / sqlite 87), so a
server that turns the UGC server on with years of never-made models doesn't queue them all at once. `queued` is set by
`InsertNewUgcModel` and `UpdateUgcModelData` (a Brick By Brick save), by every reset (`ResetUgcModelProcessing`,
`ResetPropertyUgcModelProcessing`: the dashboard's Make again, `/reprocessproperty`), and for every model placed on a
property when the property is saved (`PropertyManagementComponent::Save` calls `QueuePropertyUgcModels`: finishing
building, placing, moving or picking up a model, a hand-off). Rows from before the column are 0. The UGC page counts
the waiting models that aren't queued apart. Turning it off makes every waiting model eligible again.
### Marking models for processing (for code that writes `ugc` rows)
* A new row needs nothing: `is_optimized` defaults to 0 (`InsertNewUgcModel` writes 0, and its `process_after`).
* When a model's LXFML changes, `UpdateUgcModelData` sets `is_optimized = 0, process_attempts = 0, process_error = ''`
* A new row needs nothing: `is_optimized` defaults to 0 (`InsertNewUgcModel` writes 0, its `process_after`, and
`queued = 1`).
* When a model's LXFML changes, `UpdateUgcModelData` sets `is_optimized = 0, process_attempts = 0, process_error = '', queued = 1`
in the same statement. Anything that writes `ugc.lxfml` another way must do the same, or call
`ResetUgcModelProcessing(id, false)`.
* Deleting the row is enough when a model is deleted; its files are left until the storage cap removes them.

View File

@@ -30,11 +30,15 @@ worker_threads=0
# bigger than it alone runs when nothing else does (0: no limit).
# max_model_bricks: models with more bricks fail with that reason instead of being made (0: no limit).
# pause_hours: local hours in which no new jobs start, e.g. 18-23 or 22-6 (empty: never).
# queue_on_property_save: 1: only models that were saved from Brick By Brick, made again by staff, or whose property
# was saved are made, so the models already waiting (a server turning the UGC server on) aren't all made at once.
# 0: every waiting model is made.
max_cpu_percent=0
worker_nice=0
max_memory_mb=0
max_model_bricks=0
pause_hours=
queue_on_property_save=0
# How often to look for models to make (milliseconds), how many to take at once, and how often a model may fail
# before it's given up on (the dashboard can try again)