-
Notifications
You must be signed in to change notification settings - Fork 137
Expand file tree
/
Copy pathworkflow_pause.go
More file actions
240 lines (209 loc) · 8.69 KB
/
Copy pathworkflow_pause.go
File metadata and controls
240 lines (209 loc) · 8.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controlapi
import (
"context"
"crypto/rand"
"errors"
"fmt"
"log/slog"
"time"
"github.com/agent-substrate/substrate/cmd/ateapi/internal/store"
"github.com/agent-substrate/substrate/internal/proto/ateletpb"
atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1"
listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1"
"github.com/agent-substrate/substrate/pkg/proto/ateapipb"
"k8s.io/apimachinery/pkg/util/wait"
)
// PauseInput holds the immutable parameters requested by the client.
type PauseInput struct {
ActorID string
Atespace string
}
// PauseState holds the mutable state loaded and modified during execution.
type PauseState struct {
Actor *ateapipb.Actor
ActorTemplate *atev1alpha1.ActorTemplate
}
type LoadActorForPauseStep struct {
store store.Interface
actorTemplateLister listersv1alpha1.ActorTemplateLister
}
func (s *LoadActorForPauseStep) Name() string { return "LoadActorForPause" }
func (s *LoadActorForPauseStep) IsComplete(ctx context.Context, input *PauseInput, state *PauseState) (bool, error) {
// Always run to get the freshest state
return false, nil
}
func (s *LoadActorForPauseStep) Execute(ctx context.Context, input *PauseInput, state *PauseState) error {
actor, err := s.store.GetActor(ctx, input.Atespace, input.ActorID)
if err != nil {
return err
}
state.Actor = actor
actorTemplate, err := s.actorTemplateLister.ActorTemplates(actor.GetActorTemplateNamespace()).Get(actor.GetActorTemplateName())
if err != nil {
return fmt.Errorf("while getting ActorTemplate: %w", err)
}
state.ActorTemplate = actorTemplate
return nil
}
func (s *LoadActorForPauseStep) RetryBackoff() *wait.Backoff { return nil }
type MarkPausingStep struct {
store store.Interface
}
func (s *MarkPausingStep) Name() string { return "MarkPausing" }
func (s *MarkPausingStep) IsComplete(ctx context.Context, input *PauseInput, state *PauseState) (bool, error) {
// Fast forward if we've already marked our intent or if we are further along.
return state.Actor.GetStatus() == ateapipb.Actor_STATUS_PAUSING || state.Actor.GetStatus() == ateapipb.Actor_STATUS_PAUSED, nil
}
func (s *MarkPausingStep) Execute(ctx context.Context, input *PauseInput, state *PauseState) error {
if state.Actor.GetStatus() != ateapipb.Actor_STATUS_RUNNING {
return nil
}
state.Actor.Status = ateapipb.Actor_STATUS_PAUSING
state.Actor.InProgressSnapshot = fmt.Sprintf("%s-%s-%s", state.Actor.GetActorId(), time.Now().Format(time.RFC3339), rand.Text())
return s.store.UpdateActor(ctx, state.Actor, state.Actor.GetVersion())
}
func (s *MarkPausingStep) RetryBackoff() *wait.Backoff { return nil }
type CallAteletPauseStep struct {
dialer *AteletDialer
}
func (s *CallAteletPauseStep) Name() string { return "CallAteletPause" }
func (s *CallAteletPauseStep) IsComplete(ctx context.Context, input *PauseInput, state *PauseState) (bool, error) {
// If we are already PAUSED, we've already called Atelet
return state.Actor.GetStatus() == ateapipb.Actor_STATUS_PAUSED, nil
}
func (s *CallAteletPauseStep) Execute(ctx context.Context, input *PauseInput, state *PauseState) error {
if state.Actor.GetAteomPodNamespace() == "" {
return fmt.Errorf("actor is in PAUSING state but has no active worker")
}
ateletConn, err := s.dialer.DialForWorker(state.Actor.GetAteomPodNamespace(), state.Actor.GetAteomPodName())
if err != nil {
if errors.Is(err, ErrWorkerPodNotFound) {
slog.Warn("Skipping pause for dangling worker pod", "namespace", state.Actor.GetAteomPodNamespace(), "pod", state.Actor.GetAteomPodName())
return nil
}
return fmt.Errorf("while getting atelet conn for worker pod: %w", err)
}
client := ateletpb.NewAteomHerderClient(ateletConn)
workloadSpec := workloadSpecFromActorTemplate(state.ActorTemplate)
// Checkpoint does not carry the sandbox config: atelet uses the version the
// actor is currently running (recorded on-node at Run/Restore) and pins it
// into the snapshot manifest.
req := &ateletpb.CheckpointRequest{
TargetAteomUid: state.Actor.GetAteomPodUid(),
Atespace: state.Actor.GetAtespace(),
ActorId: state.Actor.GetActorId(),
ActorTemplateNamespace: state.Actor.GetActorTemplateNamespace(),
ActorTemplateName: state.Actor.GetActorTemplateName(),
Spec: workloadSpec,
Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL,
Config: &ateletpb.CheckpointRequest_LocalConfig{
LocalConfig: &ateletpb.LocalCheckpointConfiguration{
SnapshotPrefix: state.Actor.InProgressSnapshot,
},
},
Scope: toAteletSnapshotScope(state.ActorTemplate.Spec.SnapshotsConfig.OnPause),
}
_, err = client.Checkpoint(ctx, req)
if err != nil {
return fmt.Errorf("while checkpointing workload: %w", err)
}
return nil
}
func (s *CallAteletPauseStep) RetryBackoff() *wait.Backoff { return nil }
type FinalizePausedStep struct {
store store.Interface
}
func (s *FinalizePausedStep) Name() string { return "FinalizePaused" }
func (s *FinalizePausedStep) IsComplete(ctx context.Context, input *PauseInput, state *PauseState) (bool, error) {
// The workflow is completely done ONLY if the status is PAUSED *and* we've successfully freed the worker.
return state.Actor.GetStatus() == ateapipb.Actor_STATUS_PAUSED && state.Actor.GetAteomPodNamespace() == "", nil
}
func (s *FinalizePausedStep) Execute(ctx context.Context, input *PauseInput, state *PauseState) error {
latestActor, err := s.store.GetActor(ctx, input.Atespace, input.ActorID)
if err != nil {
return err
}
// 1. Free the worker (if it hasn't been freed yet)
if latestActor.GetAteomPodNamespace() != "" {
workerNs := latestActor.GetAteomPodNamespace()
workerPod := latestActor.GetAteomPodName()
workerPool := latestActor.GetWorkerPoolName()
worker, err := s.store.GetWorker(ctx, workerNs, workerPool, workerPod)
nodeName := ""
if err != nil {
if !errors.Is(err, store.ErrNotFound) {
return fmt.Errorf("while getting worker for release: %w", err)
}
slog.Warn("Worker already gone during finalize pause, skipping release", "worker", workerPod)
} else {
// TODO(dberkov) - what if worker does not belong to this actor?
nodeName = worker.GetNodeName()
// Only free it if it still belongs to us
if wass := worker.Assignment; wass != nil {
if wass.Actor.Name == input.ActorID {
worker.Assignment = nil
err = s.store.UpdateWorker(ctx, worker, worker.Version)
if err != nil {
return err
}
}
}
}
// 2. Safely clear ActiveWorker now that the worker object in DB is freed
latestActor, err = s.store.GetActor(ctx, input.Atespace, input.ActorID)
if err != nil {
return err
}
latestActor.Status = ateapipb.Actor_STATUS_PAUSED
// TODO(dberkov) - what if we still don't know the node name? Maybe move to CRASHED status?
if nodeName == "" {
slog.Warn("Node name not found during finalize pause", "actor", input.ActorID)
}
// TODO(dberkov) - what if InProgressSnapshot is empty? That shouldn't be possible.
if latestActor.InProgressSnapshot != "" {
// If the worker record was already gone we don't know which node
// holds the checkpoint. Record no locality rather than an empty
// node name: findFreeWorker treats any non-empty restriction list
// as a hard filter, and no worker has an empty node name, so [""]
// would make the actor permanently unschedulable.
var nodesWithSnapshot []string
if nodeName != "" {
nodesWithSnapshot = []string{nodeName}
}
latestActor.LatestSnapshotInfo = &ateapipb.SnapshotInfo{
Type: ateapipb.SnapshotType_SNAPSHOT_TYPE_LOCAL,
Data: &ateapipb.SnapshotInfo_Local{
Local: &ateapipb.LocalSnapshotInfo{
SnapshotPrefix: latestActor.InProgressSnapshot,
NodeVmsWithLocalSnapshots: nodesWithSnapshot,
},
},
}
latestActor.InProgressSnapshot = ""
}
latestActor.AteomPodNamespace = ""
latestActor.AteomPodName = ""
latestActor.AteomPodIp = ""
latestActor.WorkerPoolName = ""
err = s.store.UpdateActor(ctx, latestActor, latestActor.GetVersion())
if err != nil {
return err
}
}
state.Actor = latestActor
return nil
}
func (s *FinalizePausedStep) RetryBackoff() *wait.Backoff { return nil }