Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions cmd/ateapi/internal/controlapi/actor.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,20 @@ func (s *ServiceImpl) CreateActor(ctx context.Context, inActor *ateapipb.Actor)
}

// Resolve the explicit tag, or freeze the template's current golden default.
goldenDefault := inActor.GetSourceTag() == nil
tagRef := inActor.GetSourceTag()
if tagRef == nil {
if goldenDefault {
tagRef = template.GetStatus().GetGoldenSnapshotStatus().GetGoldenTag()
if tagRef != nil {
// Default the request's source tag to the golden tag it borrows
// from, so the stored Actor carries the reference itself. The
// template is otherwise the only record of it, and deleting the
// template strands actors that have not resumed yet. Stamp a copy
// of the request: the caller's message, which the RPC interceptor
// logs after the handler returns, keeps reflecting what was sent.
inActor = proto.CloneOf(inActor)
inActor.SourceTag = proto.CloneOf(tagRef)
}
} else {
for _, volume := range template.GetVolumes() {
if volume.GetExternalVolumeTemplate() != nil {
Expand All @@ -105,7 +116,7 @@ func (s *ServiceImpl) CreateActor(ctx context.Context, inActor *ateapipb.Actor)
if err != nil {
return nil, err
}
if inActor.GetSourceTag() == nil {
if goldenDefault {
if err := validateGoldenSnapshotFidelity(sourceTag.GetStatus().GetSnapshot()); err != nil {
return nil, err
}
Expand Down
12 changes: 12 additions & 0 deletions cmd/ateapi/internal/controlapi/actor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/peer"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/timestamppb"
"k8s.io/apimachinery/pkg/util/validation/field"
Expand Down Expand Up @@ -852,7 +853,11 @@ func TestCreateActor_GoldenTagDefault(t *testing.T) {
actor.SourceTag = &ateapipb.ObjectRef{Atespace: ref.Atespace, Name: "explicit"}
}
svc := &ServiceImpl{store: persistence}
original := proto.CloneOf(actor)
created, err := svc.CreateActor(ctx, actor)
if diff := cmp.Diff(original, actor, protocmp.Transform()); diff != "" {
t.Fatalf("CreateActor mutated input (-want +got):\n%s", diff)
}
if apierror.Code(err) != wantCode {
t.Fatalf("CreateActor = %v, want %v", err, wantCode)
}
Expand All @@ -862,6 +867,13 @@ func TestCreateActor_GoldenTagDefault(t *testing.T) {
if got := created.GetStatus(); got.GetExternalSnapshot().GetSnapshotUri() != tag.GetStatus().GetSnapshot().GetSnapshotUri() || got.GetExternalSnapshot().GetActorTemplateUid() != tmpl.GetMetadata().GetUid() {
t.Fatalf("incorrect initial status: %v", got)
}
wantTag := ref
if scenario == "explicit tag" {
wantTag = actor.GetSourceTag()
}
if got := created.GetSourceTag(); got.GetAtespace() != wantTag.GetAtespace() || got.GetName() != wantTag.GetName() {
t.Fatalf("source tag = %v, want %v", got, wantTag)
}
if scenario == "own snapshot" {
uri, err := resources.NewActorSnapshotURI(tmpl.GetSnapshotConfig().GetStorageLocation(), "team-a", created.GetMetadata().GetUid(), "snapshot")
if err != nil {
Expand Down
6 changes: 6 additions & 0 deletions cmd/ateapi/internal/controlapi/functionaltest/actor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func TestCreateActor_Success(t *testing.T) {
want := &ateapipb.Actor{
Metadata: &ateapipb.ResourceMetadata{Name: "id1", Atespace: testAtespace, Version: 1},
ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"},
SourceTag: &ateapipb.ObjectRef{Atespace: resources.GoldenActorAtespace, Name: tmpl.GetMetadata().GetUid()},
Status: &ateapipb.ActorStatus{
State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED,
ExternalSnapshot: &ateapipb.ExternalSnapshot{SnapshotUri: goldenSnapshotURI(t), Fidelity: ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, ActorTemplateUid: tmpl.GetMetadata().GetUid()},
Expand Down Expand Up @@ -698,6 +699,7 @@ func TestUpdateActor_Success(t *testing.T) {
wantActor := &ateapipb.Actor{
Metadata: &ateapipb.ResourceMetadata{Name: "id1", Atespace: testAtespace, Version: 2},
ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"},
SourceTag: &ateapipb.ObjectRef{Atespace: resources.GoldenActorAtespace, Name: tmpl.GetMetadata().GetUid()},
Status: &ateapipb.ActorStatus{
State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED,
ExternalSnapshot: &ateapipb.ExternalSnapshot{SnapshotUri: goldenSnapshotURI(t), Fidelity: ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, ActorTemplateUid: tmpl.GetMetadata().GetUid()},
Expand Down Expand Up @@ -844,6 +846,7 @@ func TestUpdateActor(t *testing.T) {
wantActor := &ateapipb.Actor{
Metadata: &ateapipb.ResourceMetadata{Name: "id1", Atespace: testAtespace, Version: 2},
ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"},
SourceTag: &ateapipb.ObjectRef{Atespace: resources.GoldenActorAtespace, Name: tmpl.GetMetadata().GetUid()},
Status: &ateapipb.ActorStatus{
State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED,
ExternalSnapshot: &ateapipb.ExternalSnapshot{SnapshotUri: goldenSnapshotURI(t), Fidelity: ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, ActorTemplateUid: tmpl.GetMetadata().GetUid()},
Expand Down Expand Up @@ -2907,6 +2910,7 @@ func TestResumeActor(t *testing.T) {
want := &ateapipb.Actor{
Metadata: &ateapipb.ResourceMetadata{Name: name, Atespace: testAtespace},
ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"},
SourceTag: &ateapipb.ObjectRef{Atespace: resources.GoldenActorAtespace, Name: tmpl.GetMetadata().GetUid()},
Status: &ateapipb.ActorStatus{
State: ateapipb.ActorState_ACTOR_STATE_RUNNING,
AssignedNode: "node1",
Expand Down Expand Up @@ -3746,6 +3750,7 @@ func TestSuspendActor(t *testing.T) {
want := &ateapipb.Actor{
Metadata: &ateapipb.ResourceMetadata{Name: name, Atespace: testAtespace},
ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"},
SourceTag: &ateapipb.ObjectRef{Atespace: resources.GoldenActorAtespace, Name: tmpl.GetMetadata().GetUid()},
Status: &ateapipb.ActorStatus{
State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED,
ExternalSnapshot: &ateapipb.ExternalSnapshot{
Expand Down Expand Up @@ -4038,6 +4043,7 @@ func TestPauseActor(t *testing.T) {
want := &ateapipb.Actor{
Metadata: &ateapipb.ResourceMetadata{Name: name, Atespace: testAtespace},
ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"},
SourceTag: &ateapipb.ObjectRef{Atespace: resources.GoldenActorAtespace, Name: tmpl.GetMetadata().GetUid()},
Status: &ateapipb.ActorStatus{
State: ateapipb.ActorState_ACTOR_STATE_PAUSED,
AssignedNode: "node1",
Expand Down
2 changes: 1 addition & 1 deletion docs/api-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ When an `ActorTemplate` is created:
3. It creates a published tag named after the template UID, copying the snapshot into tag-owned storage.
4. It deletes the golden actor and records the tag reference in the template status.

`CreateActor` uses an explicit `sourceTag` when supplied; otherwise it resolves the template's golden tag and records that snapshot on the new actor. If the golden tag is not ready yet, the actor starts without a snapshot and cold-boots even if the tag becomes ready before its first resume. The default does not populate the caller-owned `sourceTag` field. Deleting the template collects its golden tag and any unfinished golden actor.
`CreateActor` uses an explicit `sourceTag` when supplied; otherwise it resolves the template's golden tag, records that snapshot on the new actor, and stamps the golden tag into the actor's `sourceTag`. If the golden tag is not ready yet, the actor starts without a snapshot and cold-boots even if the tag becomes ready before its first resume. Deleting the template collects its golden tag and any unfinished golden actor.

### Resumption Lifecycle
Once a template is `Ready`, creating an actor logically (via `kubectl ate create actor`) allows it to be resumed instantly on any free worker in the referenced `WorkerPool`. Substrate bypasses the standard container boot and restores the process directly from its last saved state.
Expand Down
7 changes: 4 additions & 3 deletions pkg/proto/ateapipb/ateapi.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions pkg/proto/ateapipb/ateapi.proto
Original file line number Diff line number Diff line change
Expand Up @@ -398,9 +398,10 @@ message Actor {
// +k8s:optional
Selector worker_selector = 5;

// The tag specified by the caller at CreateActor to seed this Actor from an
// ActorSnapshot. Unset if the Actor was not created from a snapshot. Set
// once at creation and immutable afterward.
// The tag this Actor was seeded from, an ActorSnapshot's tag. Set at
// CreateActor: to the caller-specified tag when one is supplied, otherwise
// defaulted to the template's golden tag when the template has one. Unset
// if the Actor was not created from a snapshot. Immutable afterward.
//
// +k8s:optional
// +k8s:subfield(atespace)=+k8s:required
Expand Down
Loading