LibJS: Requeue aborted FinalizationRegistry cleanup
When an automatic cleanup job aborts after invoking a callback, requeue the registry if records with empty targets remain. This keeps later held values from being stranded until another target dies. Add test-js coverage that drains the automatic cleanup queue after two records die and the first cleanup callback throws.
This commit is contained in:
parent
0e8bc451a7
commit
841fae0d57
5 changed files with 39 additions and 1 deletions
|
|
@ -51,6 +51,15 @@ bool FinalizationRegistry::remove_by_token(Cell& unregister_token)
|
|||
return removed;
|
||||
}
|
||||
|
||||
bool FinalizationRegistry::has_empty_cells() const
|
||||
{
|
||||
for (auto& record : m_records) {
|
||||
if (!record.target)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void FinalizationRegistry::remove_dead_cells(Badge<GC::Heap>)
|
||||
{
|
||||
auto any_cells_were_removed = false;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ public:
|
|||
void add_finalization_record(Cell& target, Value held_value, Cell* unregister_token);
|
||||
bool remove_by_token(Cell& unregister_token);
|
||||
ThrowCompletionOr<void> cleanup(GC::Ptr<JobCallback> = {});
|
||||
bool has_empty_cells() const;
|
||||
|
||||
virtual Cell const& owner_cell(Badge<GC::Heap>) const override { return *this; }
|
||||
virtual void remove_dead_cells(Badge<GC::Heap>) override;
|
||||
|
|
|
|||
|
|
@ -488,7 +488,9 @@ void VM::run_queued_finalization_registry_cleanup_jobs()
|
|||
while (!m_finalization_registry_cleanup_jobs.is_empty()) {
|
||||
auto registry = m_finalization_registry_cleanup_jobs.take_last();
|
||||
// FIXME: Handle any uncatched exceptions here.
|
||||
(void)registry->cleanup();
|
||||
auto result = registry->cleanup();
|
||||
if (result.is_error() && registry->has_empty_cells())
|
||||
m_finalization_registry_cleanup_jobs.append(registry);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,26 @@ test("cleanup can process multiple dead records from one garbage collection", ()
|
|||
expect(heldValues).toEqual(["first", "second"]);
|
||||
});
|
||||
|
||||
test("automatic cleanup continues after a callback throws", () => {
|
||||
var heldValues = [];
|
||||
var registry = new FinalizationRegistry(value => {
|
||||
heldValues.push(value);
|
||||
if (value === "first") throw new Error("expected abrupt completion");
|
||||
});
|
||||
|
||||
evaluateSource("var __finalizationRegistryCleanupAfterThrowTarget1 = {};");
|
||||
evaluateSource("var __finalizationRegistryCleanupAfterThrowTarget2 = {};");
|
||||
registry.register(globalThis.__finalizationRegistryCleanupAfterThrowTarget1, "first");
|
||||
registry.register(globalThis.__finalizationRegistryCleanupAfterThrowTarget2, "second");
|
||||
markAsGarbage("__finalizationRegistryCleanupAfterThrowTarget1");
|
||||
markAsGarbage("__finalizationRegistryCleanupAfterThrowTarget2");
|
||||
gc();
|
||||
|
||||
runQueuedFinalizationRegistryCleanupJobs();
|
||||
|
||||
expect(heldValues).toEqual(["first", "second"]);
|
||||
});
|
||||
|
||||
test("cleanup helper errors", () => {
|
||||
var registry = new FinalizationRegistry(() => {});
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ TESTJS_GLOBAL_FUNCTION(run_queued_promise_jobs, runQueuedPromiseJobs)
|
|||
return JS::js_undefined();
|
||||
}
|
||||
|
||||
TESTJS_GLOBAL_FUNCTION(run_queued_finalization_registry_cleanup_jobs, runQueuedFinalizationRegistryCleanupJobs)
|
||||
{
|
||||
vm.run_queued_finalization_registry_cleanup_jobs();
|
||||
return JS::js_undefined();
|
||||
}
|
||||
|
||||
TESTJS_GLOBAL_FUNCTION(get_weak_set_size, getWeakSetSize)
|
||||
{
|
||||
auto object = TRY(vm.argument(0).to_object(vm));
|
||||
|
|
|
|||
Loading…
Reference in a new issue