Преглед на файлове

fix: Scope reference cycles leak every request graph

A scope owned three uncollectable reference cycles, so nothing resolved
from a request scope was ever freed even after the request ended:

- the scope registered ITSELF via a delegate factory (s => this): the
  factory closure captured the scope strongly and the resulting
  registration lived in the scope's own catalogue — scope ->
  registration -> closure -> scope. ScopeSelfFactory now holds only an
  unowned back-reference.

- every local registration connected an alias_added lambda that
  captured the registration itself; the signal's capture block keeps a
  strong reference to the emitter, so each registration self-cycled
  and leaked (along with whatever its factory closure captured —
  typically per-request context objects). Alias forwarding now goes
  through LocalAliasForwarder, which references both the registration
  and the catalogue unowned.

- scoped instances routinely hold strong references back to the scope
  (inject<Scope>() in routers/endpoints/actions), forming scope <->
  instance cycles that refcounting cannot collect. Scope.release()
  clears the instance and registration caches in one cut; owners of
  short-lived scopes (one per HTTP request in astralis) call it when
  the scope's work is done. Named release() rather than dispose()
  because a dispose() call compiled against an older vapi silently
  binds to g_object_run_dispose and does nothing.
clanker преди 1 месец
родител
ревизия
f66ed87b31
променени са 1 файла, в които са добавени 76 реда и са изтрити 11 реда
  1. 76 11
      src/Scope.vala

+ 76 - 11
src/Scope.vala

@@ -17,11 +17,62 @@ namespace Inversion {
      * within that scope (not inherited by child scopes). This is useful for
      * registering request-specific context objects in web applications.
      */
+    /**
+     * Factory that resolves the scope itself from the scope.
+     *
+     * Holds only an unowned back-reference: a delegate factory here
+     * (`s => this`) would capture the scope strongly, and because the
+     * resulting registration lives in the scope's own registration
+     * catalogue every scope would own a strong reference cycle
+     * (scope → registration → factory closure → scope) that refcounting
+     * can never collect.
+     */
+    internal class ScopeSelfFactory : Object, Factory {
+
+        private unowned Scope scope;
+
+        internal ScopeSelfFactory(Scope scope) {
+            this.scope = scope;
+        }
+
+        public Object create(Scope resolution_scope) throws Error {
+            return scope;
+        }
+    }
+
+    /**
+     * Forwards a registration's alias_added signal into a scope's local
+     * registration catalogue.
+     *
+     * A direct lambda (`registration.alias_added.connect(t => ...)`)
+     * captures the registration itself; the capture block keeps a strong
+     * reference to the emitting registration, forming a self-cycle that
+     * leaks the registration (and everything its factory closure
+     * captures — typically per-request context objects) permanently.
+     * This object references both the registration and the catalogue
+     * unowned, so no cycle is formed.
+     */
+    internal class LocalAliasForwarder : Object {
+
+        private unowned Registration registration;
+        private unowned Catalogue<Type, Registration> registrations;
+
+        internal LocalAliasForwarder(Registration registration, Catalogue<Type, Registration> registrations) {
+            this.registration = registration;
+            this.registrations = registrations;
+        }
+
+        internal void on_alias_added(Type alias) {
+            registrations.add(alias, registration);
+        }
+    }
+
     public class Scope : Object {
 
         public Container container { get; construct set; }
         private Dictionary<Registration, Object> scoped_instances;
         private Catalogue<Type, Registration> local_registrations;
+        private Dictionary<Registration, LocalAliasForwarder> alias_forwarders;
         public Lifecycle scope_lifecycle { get; construct set; }
 
         /**
@@ -33,14 +84,29 @@ namespace Inversion {
             Object(container: container);
             this.scoped_instances = new Dictionary<Registration, Object>();
             this.local_registrations = new Catalogue<Type, Registration>();
+            this.alias_forwarders = new Dictionary<Registration, LocalAliasForwarder>();
             this.scope_lifecycle = lifecycle;
 
-            if(lifecycle == Lifecycle.SCOPED) {
-                register_local_scoped<Scope>(s => this);
-            }
-            else {
-                register_local_transient<Scope>(s => this);
-            }
+            var self_lifecycle = lifecycle == Lifecycle.SCOPED ? Lifecycle.SCOPED : Lifecycle.TRANSIENT;
+            register_local_factory_type(typeof(Scope), new ScopeSelfFactory(this), self_lifecycle);
+        }
+
+        /**
+         * Releases everything this scope owns.
+         *
+         * Scoped instances and local registrations routinely hold strong
+         * references back to the scope (directly via `inject<Scope>()` or
+         * indirectly through other scoped instances), so an undisposed
+         * scope is an uncollectable reference cycle and everything it
+         * created leaks. Owners of short-lived scopes — one per HTTP
+         * request, for example — must call release() when the scope's work is
+         * done. After disposal the scope can still resolve, creating
+         * fresh instances and registrations as before.
+         */
+        public void release() {
+            alias_forwarders.clear();
+            scoped_instances.clear();
+            local_registrations.clear();
         }
 
 
@@ -62,11 +128,10 @@ namespace Inversion {
             var registration = new Registration(implementation_type, factory, lifecycle);
             local_registrations.add(implementation_type, registration);
 
-            // Add subsequent aliases added to this registration
-            registration.alias_added.connect(t => {
-                local_registrations.add(t, registration);
-            });
-            
+            var forwarder = new LocalAliasForwarder(registration, local_registrations);
+            alias_forwarders[registration] = forwarder;
+            registration.alias_added.connect(forwarder.on_alias_added);
+
             return registration;
         }