返回 DeepSeek-Reasonix
lifecycle.go
根目录 / internal / extension / lifecycle.go
1 package extension
2
3 import (
4 "fmt"
5 "sync"
6 "time"
7 )
8
9 // LifecycleRegistry tracks component lifecycle states for one published
10 // generation. It is diagnostic-first: doctor and UI read it; activation code
11 // advances states through Transition.
12 type LifecycleRegistry struct {
13 mu sync.Mutex
14 generation uint64
15 states map[ComponentID]*ComponentStatus
16 }
17
18 // NewLifecycleRegistry returns an empty registry for generation.
19 func NewLifecycleRegistry(generation uint64) *LifecycleRegistry {
20 return &LifecycleRegistry{
21 generation: generation,
22 states: make(map[ComponentID]*ComponentStatus),
23 }
24 }
25
26 // Generation returns the bound generation.
27 func (r *LifecycleRegistry) Generation() uint64 {
28 if r == nil {
29 return 0
30 }
31 return r.generation
32 }
33
34 // Ensure registers id in Inactive if missing.
35 func (r *LifecycleRegistry) Ensure(id ComponentID) {
36 if r == nil || id == "" {
37 return
38 }
39 r.mu.Lock()
40 defer r.mu.Unlock()
41 if _, ok := r.states[id]; !ok {
42 r.states[id] = &ComponentStatus{
43 ID: id,
44 State: ComponentInactive,
45 Generation: r.generation,
46 UpdatedAt: time.Now().UTC(),
47 }
48 }
49 }
50
51 // Transition advances a component to next when the transition is legal.
52 // Illegal transitions return an error and leave state unchanged.
53 func (r *LifecycleRegistry) Transition(id ComponentID, next ComponentState, diag string) error {
54 if r == nil {
55 return fmt.Errorf("extension: nil lifecycle registry")
56 }
57 r.mu.Lock()
58 defer r.mu.Unlock()
59 cur, ok := r.states[id]
60 if !ok {
61 cur = &ComponentStatus{ID: id, State: ComponentInactive, Generation: r.generation}
62 r.states[id] = cur
63 }
64 if !legalTransition(cur.State, next) {
65 return fmt.Errorf("extension: illegal lifecycle transition %s: %s -> %s", id, cur.State, next)
66 }
67 cur.State = next
68 cur.UpdatedAt = time.Now().UTC()
69 if diag != "" {
70 cur.Diagnostics = append(cur.Diagnostics, diag)
71 }
72 if next == ComponentFailed && diag != "" {
73 cur.Error = diag
74 }
75 return nil
76 }
77
78 // Fail marks the component Failed with error text.
79 func (r *LifecycleRegistry) Fail(id ComponentID, err error) {
80 msg := ""
81 if err != nil {
82 msg = err.Error()
83 }
84 _ = r.Transition(id, ComponentFailed, msg)
85 }
86
87 // Status returns a copy of one component status.
88 func (r *LifecycleRegistry) Status(id ComponentID) (ComponentStatus, bool) {
89 if r == nil {
90 return ComponentStatus{}, false
91 }
92 r.mu.Lock()
93 defer r.mu.Unlock()
94 s, ok := r.states[id]
95 if !ok {
96 return ComponentStatus{}, false
97 }
98 cp := *s
99 cp.Diagnostics = append([]string(nil), s.Diagnostics...)
100 return cp, true
101 }
102
103 // All returns a snapshot of every component status.
104 func (r *LifecycleRegistry) All() []ComponentStatus {
105 if r == nil {
106 return nil
107 }
108 r.mu.Lock()
109 defer r.mu.Unlock()
110 out := make([]ComponentStatus, 0, len(r.states))
111 for _, s := range r.states {
112 cp := *s
113 cp.Diagnostics = append([]string(nil), s.Diagnostics...)
114 out = append(out, cp)
115 }
116 return out
117 }
118
119 // RuntimeStatus builds the host-facing status document.
120 func (r *LifecycleRegistry) RuntimeStatus(plan *RuntimePlan, receipts []EffectReceipt) *RuntimeStatus {
121 if r == nil {
122 return nil
123 }
124 return &RuntimeStatus{
125 PublishedGeneration: r.generation,
126 Components: r.All(),
127 Plan: PlanView(plan),
128 Receipts: receipts,
129 }
130 }
131
132 func legalTransition(from, to ComponentState) bool {
133 if from == to {
134 return true
135 }
136 switch from {
137 case ComponentInactive:
138 return to == ComponentPreparing || to == ComponentFailed
139 case ComponentPreparing:
140 return to == ComponentActive || to == ComponentFailed || to == ComponentInactive
141 case ComponentActive:
142 return to == ComponentDraining || to == ComponentFailed
143 case ComponentDraining:
144 return to == ComponentInactive || to == ComponentFailed
145 case ComponentFailed:
146 return to == ComponentInactive || to == ComponentPreparing
147 default:
148 return false
149 }
150 }
151
151 lines GO