| 1 | package extension |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | "io" |
| 7 | "sync" |
| 8 | ) |
| 9 | |
| 10 | // RuntimeSet tracks live resources bound to one RuntimeSnapshot generation. |
| 11 | // It is separate from the immutable snapshot and holds an EffectScope so |
| 12 | // activation, receipts, and typed dispose share one owner. CloseIfGeneration |
| 13 | // keeps stale cleanup from closing a newer runtime's resources. |
| 14 | type RuntimeSet struct { |
| 15 | mu sync.Mutex |
| 16 | scope *LiveScope |
| 17 | // nextCloser sequences auto-generated closer ids for Add. |
| 18 | nextCloser int |
| 19 | } |
| 20 | |
| 21 | // NewRuntimeSet returns an empty set bound to a snapshot generation. |
| 22 | func NewRuntimeSet(generation uint64) *RuntimeSet { |
| 23 | return &RuntimeSet{scope: NewEffectScope(generation)} |
| 24 | } |
| 25 | |
| 26 | // Scope returns the underlying EffectScope for typed effect registration. |
| 27 | func (s *RuntimeSet) Scope() EffectScope { |
| 28 | if s == nil || s.scope == nil { |
| 29 | return nil |
| 30 | } |
| 31 | return s.scope |
| 32 | } |
| 33 | |
| 34 | // Generation returns the snapshot generation this set is bound to. |
| 35 | func (s *RuntimeSet) Generation() uint64 { |
| 36 | if s == nil || s.scope == nil { |
| 37 | return 0 |
| 38 | } |
| 39 | return s.scope.Generation() |
| 40 | } |
| 41 | |
| 42 | // Add registers closers as reversible effects. A closer added to an already |
| 43 | // closed set is closed immediately — the alternative (silently leaking it |
| 44 | // because the owner went away between activation and registration) is how |
| 45 | // sidecar processes outlive their session. |
| 46 | func (s *RuntimeSet) Add(closers ...io.Closer) { |
| 47 | if s == nil { |
| 48 | return |
| 49 | } |
| 50 | for _, c := range closers { |
| 51 | if c == nil { |
| 52 | continue |
| 53 | } |
| 54 | s.mu.Lock() |
| 55 | s.nextCloser++ |
| 56 | id := fmt.Sprintf("closer-%d", s.nextCloser) |
| 57 | s.mu.Unlock() |
| 58 | _ = s.scope.TrackCloser(id, c) |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | // Track registers a typed effect on the set's scope. |
| 63 | func (s *RuntimeSet) Track(e Effect) error { |
| 64 | if s == nil || s.scope == nil { |
| 65 | return fmt.Errorf("extension: nil RuntimeSet") |
| 66 | } |
| 67 | return s.scope.Track(e) |
| 68 | } |
| 69 | |
| 70 | // Len returns the number of registered effects that have not yet been disposed. |
| 71 | func (s *RuntimeSet) Len() int { |
| 72 | if s == nil || s.scope == nil { |
| 73 | return 0 |
| 74 | } |
| 75 | s.scope.mu.Lock() |
| 76 | defer s.scope.mu.Unlock() |
| 77 | return len(s.scope.effects) |
| 78 | } |
| 79 | |
| 80 | // Close releases every registered resource, in reverse registration order |
| 81 | // (later resources may depend on earlier ones). It is idempotent: concurrent |
| 82 | // or repeated calls after the first return nil without re-closing, because |
| 83 | // most closers are not safe to invoke twice. |
| 84 | func (s *RuntimeSet) Close() error { |
| 85 | if s == nil || s.scope == nil { |
| 86 | return nil |
| 87 | } |
| 88 | return s.scope.Dispose(context.Background()) |
| 89 | } |
| 90 | |
| 91 | // CloseIfGeneration closes only when gen matches; wrong generations leave |
| 92 | // resources untouched. Errors collapse to the bool (fire-and-forget cleanup). |
| 93 | func (s *RuntimeSet) CloseIfGeneration(gen uint64) bool { |
| 94 | if s == nil || s.scope == nil { |
| 95 | return false |
| 96 | } |
| 97 | if gen != s.scope.Generation() { |
| 98 | return false |
| 99 | } |
| 100 | _ = s.Close() |
| 101 | return true |
| 102 | } |
| 103 | |
| 104 | // Closed reports whether Close has run. |
| 105 | func (s *RuntimeSet) Closed() bool { |
| 106 | if s == nil || s.scope == nil { |
| 107 | return true |
| 108 | } |
| 109 | return s.scope.Closed() |
| 110 | } |
| 111 | |
| 112 | // Receipts returns effect receipts recorded by this generation. |
| 113 | func (s *RuntimeSet) Receipts() []EffectReceipt { |
| 114 | if s == nil || s.scope == nil { |
| 115 | return nil |
| 116 | } |
| 117 | return s.scope.Receipts() |
| 118 | } |
| 119 |