فهرست منبع

adopt Scope.release() (inversion f66ed87) at every create_scope() site: Dispatcher.tick(), PushService.resolve_pending()/prune_subscription() and the main.vala startup scopes (sqlite pragmas, push keys, admin seed, dispatcher start) now release their scope in a finally/teardown path — scoped instances holding references back to their scope made every request/cycle/notification graph an uncollectable cycle, so the dispatch timer, push resolution and subscription pruning leaked one object graph per pass; the astralis Pipeline release-in-finally pattern applied; 13 bundles sequential 966/0

clanker 1 ماه پیش
والد
کامیت
76611653cc
3فایلهای تغییر یافته به همراه80 افزوده شده و 66 حذف شده
  1. 43 39
      src/Services/Dispatcher.vala
  2. 29 23
      src/Services/PushService.vala
  3. 8 4
      src/main.vala

+ 43 - 39
src/Services/Dispatcher.vala

@@ -108,52 +108,56 @@ namespace ClankerSaddle.Services {
           */
         public async void tick() throws Error {
             var scope = container.create_scope();
-            var tasks = scope.resolve<TaskService>();
-            var saddles = scope.resolve<SaddleService>();
-            var capabilities = scope.resolve<CapabilityService>();
-            var allocation = scope.resolve<AllocationService>();
+            try {
+                var tasks = scope.resolve<TaskService>();
+                var saddles = scope.resolve<SaddleService>();
+                var capabilities = scope.resolve<CapabilityService>();
+                var allocation = scope.resolve<AllocationService>();
 
-            if (LlmEndpoint.from_environment() == null) {
-                if (!warned_no_llm) {
-                    warning("[ClankerSaddle] SADDLE_LLM_BASE_URL is not set; task assignment is paused");
-                    warned_no_llm = true;
+                if (LlmEndpoint.from_environment() == null) {
+                    if (!warned_no_llm) {
+                        warning("[ClankerSaddle] SADDLE_LLM_BASE_URL is not set; task assignment is paused");
+                        warned_no_llm = true;
+                    }
+                } else {
+                    warned_no_llm = false;
+                    yield enforce_lost_wake_guard(saddles, tasks);
+                    yield wake_sleeping_saddles(saddles, capabilities, allocation);
                 }
-            } else {
-                warned_no_llm = false;
-                yield enforce_lost_wake_guard(saddles, tasks);
-                yield wake_sleeping_saddles(saddles, capabilities, allocation);
-            }
 
-            yield saddles.requeue_tasks_on_offline_saddles(tasks);
+                yield saddles.requeue_tasks_on_offline_saddles(tasks);
 
-            // Web Push retry tick (plan 11 task E): anything a failed
-            // send left queued retries here (best-effort delivery).
-            yield push.flush();
+                // Web Push retry tick (plan 11 task E): anything a failed
+                // send left queued retries here (best-effort delivery).
+                yield push.flush();
 
-            foreach (var saddle in yield saddles.list_busy_saddles()) {
-                // The derived busy pass (plan 16 item 23): each busy
-                // saddle's running work IS its open session on an
-                // assigned/working task — the ONE truth accessor
-                // resolves it (a saddle whose work closed between the
-                // two reads reads idle next tick; nothing to do).
-                var work = yield saddles.current_work_view(saddle);
-                if (work == null) {
-                    continue;
-                }
-                // Human comments ride POST /task-update to the RUNNING
-                // daemon (best effort — a transport failure stays
-                // undelivered for the next tick; the staleness probe
-                // owns the daemon's fate, not a strike counter).
-                try {
-                    yield saddles.deliver_pending_comments(saddle.id, ((!)work).task_id);
-                } catch (Error e) {
-                    warning(@"[ClankerSaddle] Comment delivery to saddle $(saddle.id) failed: $(e.message)");
-                }
+                foreach (var saddle in yield saddles.list_busy_saddles()) {
+                    // The derived busy pass (plan 16 item 23): each busy
+                    // saddle's running work IS its open session on an
+                    // assigned/working task — the ONE truth accessor
+                    // resolves it (a saddle whose work closed between the
+                    // two reads reads idle next tick; nothing to do).
+                    var work = yield saddles.current_work_view(saddle);
+                    if (work == null) {
+                        continue;
+                    }
+                    // Human comments ride POST /task-update to the RUNNING
+                    // daemon (best effort — a transport failure stays
+                    // undelivered for the next tick; the staleness probe
+                    // owns the daemon's fate, not a strike counter).
+                    try {
+                        yield saddles.deliver_pending_comments(saddle.id, ((!)work).task_id);
+                    } catch (Error e) {
+                        warning(@"[ClankerSaddle] Comment delivery to saddle $(saddle.id) failed: $(e.message)");
+                    }
 
-                if (poll_state.take_stale_probe(((!)work).session_id,
-                        SaddleService.stale_after_seconds() * 1000)) {
-                    yield saddles.probe_stale_saddle(saddle, tasks, ((!)work).task_id);
+                    if (poll_state.take_stale_probe(((!)work).session_id,
+                            SaddleService.stale_after_seconds() * 1000)) {
+                        yield saddles.probe_stale_saddle(saddle, tasks, ((!)work).task_id);
+                    }
                 }
+            } finally {
+                scope.release();
             }
         }
 

+ 29 - 23
src/Services/PushService.vala

@@ -419,31 +419,35 @@ namespace ClankerSaddle.Services {
 
             try {
                 var scope = container.create_scope();
-                var db = scope.resolve<OrmSession>();
-                var subscriptions = yield db.query<PushSubscription>().materialise_async();
-                var jobs = new GLib.Queue<SendJob>();
-                foreach (var target in targets) {
-                    foreach (var row in subscriptions) {
-                        if (!target.global
-                                && !(yield pref_held(db, row.user_id, target.task_id))) {
-                            continue;
+                try {
+                    var db = scope.resolve<OrmSession>();
+                    var subscriptions = yield db.query<PushSubscription>().materialise_async();
+                    var jobs = new GLib.Queue<SendJob>();
+                    foreach (var target in targets) {
+                        foreach (var row in subscriptions) {
+                            if (!target.global
+                                    && !(yield pref_held(db, row.user_id, target.task_id))) {
+                                continue;
+                            }
+                            jobs.push_tail(new SendJob() {
+                                endpoint = row.endpoint,
+                                p256dh = row.p256dh,
+                                auth = row.auth,
+                                payload = payload_json(target.notification)
+                            });
                         }
-                        jobs.push_tail(new SendJob() {
-                            endpoint = row.endpoint,
-                            p256dh = row.p256dh,
-                            auth = row.auth,
-                            payload = payload_json(target.notification)
-                        });
                     }
+                    if (jobs.is_empty()) {
+                        return;
+                    }
+                    queue_lock.lock();
+                    while (!jobs.is_empty()) {
+                        queued.push_tail(jobs.pop_head());
+                    }
+                    queue_lock.unlock();
+                } finally {
+                    scope.release();
                 }
-                if (jobs.is_empty()) {
-                    return;
-                }
-                queue_lock.lock();
-                while (!jobs.is_empty()) {
-                    queued.push_tail(jobs.pop_head());
-                }
-                queue_lock.unlock();
             } catch (Error e) {
                 warning("[ClankerSaddle] Push target resolution failed: %s", e.message);
                 // Targets stay lost this pass; the event is best-effort.
@@ -725,8 +729,8 @@ namespace ClankerSaddle.Services {
 
         /** Deletes subscriptions whose endpoint went 404/410 Gone. */
         private async void prune_subscription(string endpoint) {
+            var scope = container.create_scope();
             try {
-                var scope = container.create_scope();
                 var db = scope.resolve<OrmSession>();
                 foreach (var row in yield db.query<PushSubscription>()
                         .where(expr("endpoint == $0", elem(endpoint)))
@@ -737,6 +741,8 @@ namespace ClankerSaddle.Services {
                     endpoint.substring(0, int.min(endpoint.length, 72)));
             } catch (Error e) {
                 warning("[ClankerSaddle] Pruning push subscription failed: %s", e.message);
+            } finally {
+                scope.release();
             }
         }
 

+ 8 - 4
src/main.vala

@@ -34,10 +34,11 @@ int main(string[] args) {
         // are never blocked by) writers, and the busy timeout absorbs
         // the remaining writer-writer window.
         try {
-            var connection = application.container.create_scope()
-                .resolve<InvercargillSql.Connection>();
+            var connection_scope = application.container.create_scope();
+            var connection = connection_scope.resolve<InvercargillSql.Connection>();
             connection.execute("PRAGMA journal_mode = WAL");
             connection.execute("PRAGMA busy_timeout = 5000");
+            connection_scope.release();
         } catch (Error pragma_error) {
             print(@"[ClankerSaddle] sqlite pragmas failed: $(pragma_error.message)\n");
         }
@@ -82,8 +83,9 @@ int main(string[] args) {
         // generates + persists BEFORE the server accepts requests, so
         // every subscriber sees the same public key from the first
         // page render.
-        application.container.create_scope()
-            .resolve<PushService>().ensure_keys_from_config();
+        var key_scope = application.container.create_scope();
+        key_scope.resolve<PushService>().ensure_keys_from_config();
+        key_scope.release();
 
         var statum = application.configure_with<StatumConfigurator>();
         statum.global<ClankerSaddle.Services.PulsePublic>(StatePulse.SLOT_TYPE,
@@ -236,6 +238,7 @@ private async void seed_admin(Container container) {
         var scope = container.create_scope();
         yield SpryAuth.ensure_admin(scope.resolve<UserService>(),
             username, @"$(username)@example.com", password);
+        scope.release();
         print("[ClankerSaddle] Seeded admin account: %s\n", username);
     } catch (Error e) {
         printerr("[ClankerSaddle] Admin seeding failed: %s\n", e.message);
@@ -246,6 +249,7 @@ private async void start_dispatcher(Container container) {
     try {
         var scope = container.create_scope();
         scope.resolve<Dispatcher>().start();
+        scope.release();
     } catch (Error e) {
         printerr("[ClankerSaddle] Dispatcher startup failed: %s\n", e.message);
     }