Persist the reclaimer's FX release so a reload cannot re-drive a spent record, and report the FX-restore drops it was swallowing
This commit is contained in:
+141
-3
@@ -2,13 +2,15 @@
|
||||
// loop is templated on its host precisely so the properties that make it safe to run in
|
||||
// the background are assertable here: the debounce, the per-tick budget's between-track
|
||||
// yield, the queue being a cache of intent that a model change discards, the user's own
|
||||
// offline plugins surviving an online, and — via a host that invalidates from inside a
|
||||
// write — that a re-entered invalidation reaches nothing the drain is using.
|
||||
// offline plugins surviving an online, the release reaching the persisted model, and —
|
||||
// via a host that invalidates from inside a write — that a re-entered invalidation
|
||||
// reaches nothing the drain is using.
|
||||
|
||||
#include "../src/shell/view/fx_reclaim.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -75,7 +77,23 @@ struct FakeHost {
|
||||
return true;
|
||||
}
|
||||
|
||||
void releaseFx(const std::string& guid) { released.insert(guid); }
|
||||
// `model` is the live host's session model and `persisted` stands in for view_state
|
||||
// on disk — the blob a reopen or an undo/redo loadFromProject rebuilds from. Set them
|
||||
// and the release actually lands, so a persist ordered BEFORE it would serialize a
|
||||
// record that is still held and fail the reload assertions.
|
||||
ViewModeModel* model = nullptr;
|
||||
std::string persisted;
|
||||
int persists = 0;
|
||||
|
||||
void releaseFx(const std::string& guid) {
|
||||
released.insert(guid);
|
||||
if (model) model->releaseSnapshotFx(guid);
|
||||
}
|
||||
|
||||
void persistModel() {
|
||||
++persists;
|
||||
if (model) persisted = model->serialize();
|
||||
}
|
||||
|
||||
bool offlineAt(const std::string& guid, std::size_t slot) const {
|
||||
auto it = chains.find(guid);
|
||||
@@ -491,6 +509,7 @@ static void testAnInvalidationDuringAnOnlineDropsThatTicksRelease() {
|
||||
CHECK(r.aborted);
|
||||
CHECK(r.writes == 2); // the item still completed — a step is atomic
|
||||
CHECK(host.released.empty()); // but its record was NOT spent
|
||||
CHECK(host.persists == 0); // and nothing durable claims it was
|
||||
|
||||
// Re-owed, not lost: the re-derived item finds the chain already where the record
|
||||
// wants it, writes nothing more, and releases then.
|
||||
@@ -499,6 +518,119 @@ static void testAnInvalidationDuringAnOnlineDropsThatTicksRelease() {
|
||||
CHECK(q.run(host).settled);
|
||||
CHECK(host.writeLog.size() == 2);
|
||||
CHECK(host.released.count("{T}") == 1);
|
||||
CHECK(host.persists == 1);
|
||||
}
|
||||
|
||||
// -- 6. the release has to outlive the session ------------------------------------
|
||||
|
||||
static void testACompletedOnlineIsPersistedSoAReloadPlansNoWork() {
|
||||
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"});
|
||||
vm.setActiveMode(kDesignModeId);
|
||||
vm.releaseSnapshotFlags("{T}");
|
||||
|
||||
// The bug this closes, stated as the baseline: the model as it stands BEFORE the
|
||||
// release re-derives the same Online work when reloaded, and re-driving it would
|
||||
// revert whatever per-FX offline the user set after the reclaim had finished.
|
||||
const std::optional<ViewModeModel> stale = ViewModeModel::deserialize(vm.serialize());
|
||||
CHECK(stale.has_value());
|
||||
CHECK(derive(*stale, {"{T}"}).size() == 1);
|
||||
|
||||
FakeHost host;
|
||||
host.model = &vm;
|
||||
host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}"});
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||
drainToSettled(q, host);
|
||||
CHECK(host.released.count("{T}") == 1);
|
||||
CHECK(host.persists == 1); // once per tick that released, not once per release
|
||||
|
||||
const std::optional<ViewModeModel> reloaded = ViewModeModel::deserialize(host.persisted);
|
||||
CHECK(reloaded.has_value());
|
||||
CHECK(reloaded->snapshot("{T}") == nullptr); // both halves spent ⇒ record erased
|
||||
CHECK(derive(*reloaded, {"{T}"}).empty());
|
||||
}
|
||||
|
||||
static void testATickThatSpendsNoRecordPersistsNothing() {
|
||||
// OFFLINE work spends no record, so there is nothing to make durable — and a
|
||||
// background reclaim must not dirty the project for work it can simply redo.
|
||||
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
|
||||
FakeHost host;
|
||||
host.model = &vm;
|
||||
host.chains["{T}"] = onlineChain({"{FX-A}"});
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||
q.run(host); // observes the hold at t=0
|
||||
host.clock += kParkHoldMs;
|
||||
drainToSettled(q, host);
|
||||
|
||||
CHECK(host.writeLog.size() == 1); // it did move the plugin
|
||||
CHECK(host.released.empty());
|
||||
CHECK(host.persists == 0);
|
||||
CHECK(host.persisted.empty());
|
||||
}
|
||||
|
||||
// -- 7. captured FX state the online direction cannot place -------------------------
|
||||
|
||||
static void testADropIsReportedOnceAcrossRepeatedRederivations() {
|
||||
// {FX-B} was deleted while the track was parked, so its captured state has nowhere
|
||||
// to land. Swallowing that leaves the user no trace of it at all.
|
||||
ViewModeModel vm;
|
||||
vm.membership().tag("{T}", kDesignModeId);
|
||||
vm.setActiveMode(kDesignModeId);
|
||||
vm.storeSnapshot("{T}", recordWithFx({{"{FX-A}", 0}, {"{FX-B}", 1}}));
|
||||
vm.releaseSnapshotFlags("{T}");
|
||||
|
||||
FakeHost host;
|
||||
host.model = &vm;
|
||||
host.chains["{T}"] = offlineChain({"{FX-A}"}); // {FX-B} gone from the chain
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||
host.reentrant = &q;
|
||||
host.invalidateAfterWrite = 1; // aborts, so the identical item re-derives next tick
|
||||
|
||||
const ReclaimRunResult first = q.run(host);
|
||||
CHECK(first.aborted);
|
||||
CHECK(first.drops.size() == 1);
|
||||
CHECK(first.drops[0].trackGuid == "{T}");
|
||||
CHECK(first.drops[0].drops.missingIdentity == 1);
|
||||
CHECK(first.drops[0].drops.total() == 1);
|
||||
CHECK(host.released.empty()); // the abort still refuses to spend the record
|
||||
|
||||
host.reentrant = nullptr;
|
||||
q.adopt(derive(vm, {"{T}"}));
|
||||
const ReclaimRunResult second = q.run(host);
|
||||
CHECK(second.settled);
|
||||
CHECK(second.drops == first.drops); // the re-derivation resolves the very same set
|
||||
|
||||
// The gate the shell's console report sits behind: first sighting prints, that
|
||||
// identical re-derivation does not, a different set does.
|
||||
const std::vector<TrackFxDrops> none;
|
||||
CHECK(shouldReportDrops(/*sameOwnerAsLast=*/false, none, first.drops));
|
||||
CHECK(!shouldReportDrops(/*sameOwnerAsLast=*/true, first.drops, second.drops));
|
||||
const std::vector<TrackFxDrops> elsewhere{TrackFxDrops{"{U}", FxRestoreDrops{1, 0, 0}}};
|
||||
CHECK(shouldReportDrops(/*sameOwnerAsLast=*/true, first.drops, elsewhere));
|
||||
|
||||
// The record IS spent with a drop outstanding: holding it for an FX the chain no
|
||||
// longer has would replan work that can never resolve.
|
||||
CHECK(host.released.count("{T}") == 1);
|
||||
CHECK(!host.offlineAt("{T}", 0)); // and the placeable half did land
|
||||
}
|
||||
|
||||
static void testAnOfflineTickReportsNoDrops() {
|
||||
// Offline targets are the live chain's own slots, so nothing can fail to resolve.
|
||||
// Keeps the drop assertions above honest: they must catch a real degrade, not an
|
||||
// accumulator that fills on every tick.
|
||||
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"});
|
||||
FakeHost host;
|
||||
host.chains["{T}"] = onlineChain({"{FX-A}", "{FX-B}"});
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||
q.run(host);
|
||||
host.clock += kParkHoldMs;
|
||||
const ReclaimRunResult r = q.run(host);
|
||||
CHECK(r.settled);
|
||||
CHECK(r.writes == 2);
|
||||
CHECK(r.drops.empty());
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -519,6 +651,12 @@ int main() {
|
||||
testAnInvalidationDuringTheLastItemLeavesTheTickUnsettled();
|
||||
testAnInvalidationDuringAnOnlineDropsThatTicksRelease();
|
||||
|
||||
testACompletedOnlineIsPersistedSoAReloadPlansNoWork();
|
||||
testATickThatSpendsNoRecordPersistsNothing();
|
||||
|
||||
testADropIsReportedOnceAcrossRepeatedRederivations();
|
||||
testAnOfflineTickReportsNoDrops();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user