Skip to content

Commit ee88776

Browse files
feat(api): soft-delete build layers in DB on user delete (#3121)
This switches user delete to soft-delete the build layers in DB: - New `deleted` build status, mapped to a dedicated `deleted` status_group (migration updates `compute_status_group`). - New `MarkExclusiveTemplateBuildsDeleted` query that marks builds assigned only to this template as `deleted`, reusing the exclusivity predicate so builds shared with other templates are untouched. - Template delete and snapshot/paused delete now run the mark + delete in one transaction. The `env_builds` rows (and their team/env attribution) are preserved so a future GC can reclaim storage. Mirrors how `failed` keeps attribution. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 9b162bf commit ee88776

65 files changed

Lines changed: 466 additions & 263 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/api/internal/cache/templates/cache.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ func (c *TemplateCache) fetchTemplateWithBuild(templateID string, tag *string) f
175175
}
176176

177177
build := &result.EnvBuild
178-
template := result.Env
178+
template := result.ActiveEnv
179179
clusterID := clusters.WithClusterFallback(template.ClusterID)
180180

181181
tagValue := sharedUtils.DerefOrDefault(tag, id.DefaultTag)

packages/api/internal/cache/templates/template_build.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,12 @@ func (c *TemplatesBuildCache) fetchFromDB(templateID string, buildID uuid.UUID)
8484
}
8585

8686
return TemplateBuildInfo{
87-
TeamID: result.Env.TeamID,
88-
TemplateID: result.Env.ID,
87+
TeamID: result.ActiveEnv.TeamID,
88+
TemplateID: result.ActiveEnv.ID,
8989
BuildStatus: result.EnvBuild.StatusGroup,
9090
Reason: result.EnvBuild.Reason,
9191
Version: result.EnvBuild.Version,
92-
ClusterID: clusters.WithClusterFallback(result.Env.ClusterID),
92+
ClusterID: clusters.WithClusterFallback(result.ActiveEnv.ClusterID),
9393
NodeID: result.EnvBuild.ClusterNodeID,
9494
}, nil
9595
}

packages/api/internal/handlers/deprecated_template_request_build.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func (a *APIStore) PostTemplatesTemplateID(c *gin.Context, rawTemplateID api.Tem
107107
return
108108
}
109109

110-
templateDB, err := a.sqlcDB.GetTemplateByID(ctx, templateID)
110+
templateDB, err := a.sqlcDB.GetTemplateById(ctx, templateID)
111111
switch {
112112
case err == nil:
113113
if templateDB.TeamID != team.ID {

packages/api/internal/handlers/deprecated_template_start_build.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func (a *APIStore) PostTemplatesTemplateIDBuildsBuildID(c *gin.Context, template
120120
var team *types.Team
121121
// Check if the user has access to the template
122122
for _, t := range teams {
123-
if t.Team.ID == templateBuildDB.Env.TeamID {
123+
if t.Team.ID == templateBuildDB.ActiveEnv.TeamID {
124124
team = t.Team
125125

126126
break

packages/api/internal/handlers/sandbox_kill.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"github.com/e2b-dev/infra/packages/api/internal/sandbox"
1515
"github.com/e2b-dev/infra/packages/api/internal/utils"
1616
"github.com/e2b-dev/infra/packages/auth/pkg/auth"
17-
"github.com/e2b-dev/infra/packages/db/queries"
1817
"github.com/e2b-dev/infra/packages/shared/pkg/logger"
1918
"github.com/e2b-dev/infra/packages/shared/pkg/telemetry"
2019
)
@@ -25,10 +24,7 @@ func (a *APIStore) deleteSnapshot(ctx context.Context, sandboxID string, teamID
2524
return err
2625
}
2726

28-
aliasKeys, dbErr := a.sqlcDB.DeleteTemplate(ctx, queries.DeleteTemplateParams{
29-
TeamID: teamID,
30-
TemplateID: snapshot.TemplateID,
31-
})
27+
aliasKeys, dbErr := a.softDeleteTemplate(ctx, teamID, snapshot.TemplateID)
3228
if dbErr != nil {
3329
return fmt.Errorf("error deleting template from db: %w", dbErr)
3430
}

packages/api/internal/handlers/template_delete.go

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,54 @@ import (
66
"net/http"
77

88
"github.com/gin-gonic/gin"
9+
"github.com/google/uuid"
910
"go.opentelemetry.io/otel/attribute"
1011

1112
"github.com/e2b-dev/infra/packages/api/internal/api"
1213
"github.com/e2b-dev/infra/packages/api/internal/sandbox"
14+
"github.com/e2b-dev/infra/packages/db/pkg/dberrors"
1315
"github.com/e2b-dev/infra/packages/db/queries"
1416
"github.com/e2b-dev/infra/packages/shared/pkg/id"
1517
"github.com/e2b-dev/infra/packages/shared/pkg/logger"
1618
"github.com/e2b-dev/infra/packages/shared/pkg/telemetry"
1719
)
1820

21+
// softDeleteTemplate soft-deletes the env, releases its aliases, and clears its
22+
// active build rows, in a transaction. The env-locking soft-delete runs first so
23+
// a concurrent build registration commits before the alias/active-build cleanup,
24+
// whose fresh per-statement snapshots then see those rows. Returns the released
25+
// alias cache keys.
26+
func (a *APIStore) softDeleteTemplate(ctx context.Context, teamID uuid.UUID, templateID string) ([]string, error) {
27+
txDB, tx, err := a.sqlcDB.WithTx(ctx)
28+
if err != nil {
29+
return nil, fmt.Errorf("begin tx: %w", err)
30+
}
31+
defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }()
32+
33+
if _, err := txDB.SoftDeleteTemplate(ctx, queries.SoftDeleteTemplateParams{TemplateID: templateID, TeamID: teamID}); err != nil {
34+
if dberrors.IsNotFoundError(err) {
35+
return nil, nil // already deleted or not owned by the team
36+
}
37+
38+
return nil, fmt.Errorf("soft delete template: %w", err)
39+
}
40+
41+
aliasKeys, err := txDB.ReleaseTemplateAliases(ctx, templateID)
42+
if err != nil {
43+
return nil, fmt.Errorf("release aliases: %w", err)
44+
}
45+
46+
if err := txDB.DeleteActiveTemplateBuilds(ctx, templateID); err != nil {
47+
return nil, fmt.Errorf("delete active builds: %w", err)
48+
}
49+
50+
if err := tx.Commit(ctx); err != nil {
51+
return nil, fmt.Errorf("commit: %w", err)
52+
}
53+
54+
return aliasKeys, nil
55+
}
56+
1957
// DeleteTemplatesTemplateID serves to delete a template (e.g. in CLI)
2058
func (a *APIStore) DeleteTemplatesTemplateID(c *gin.Context, aliasOrTemplateID api.TemplateID) {
2159
ctx := c.Request.Context()
@@ -80,15 +118,7 @@ func (a *APIStore) DeleteTemplatesTemplateID(c *gin.Context, aliasOrTemplateID a
80118
return
81119
}
82120

83-
// Delete the template from DB (cascades to env_build_assignments, env_aliases, snapshot_templates).
84-
// Returns alias cache keys captured before cascade deletion for cache invalidation.
85-
// Build artifacts are intentionally NOT deleted from storage here because builds are layered diffs
86-
// that may be referenced by other builds' header mappings.
87-
// [ENG-3477] a future GC mechanism will handle orphaned storage.
88-
aliasKeys, err := a.sqlcDB.DeleteTemplate(ctx, queries.DeleteTemplateParams{
89-
TemplateID: templateID,
90-
TeamID: team.ID,
91-
})
121+
aliasKeys, err := a.softDeleteTemplate(ctx, team.ID, templateID)
92122
if err != nil {
93123
telemetry.ReportCriticalError(ctx, "error when deleting template from db", err)
94124
a.sendAPIStoreError(c, http.StatusInternalServerError, "Error when deleting template")

packages/api/internal/handlers/template_layer_files_upload.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ func (a *APIStore) GetTemplatesTemplateIDFilesHash(c *gin.Context, templateID ap
2626
return
2727
}
2828

29-
// Check if the user has access to the template
30-
templateDB, err := a.sqlcDB.GetTemplateByID(ctx, templateID)
29+
// Resolve via the active-envs view so a soft-deleted template is not found.
30+
templateDB, err := a.sqlcDB.GetTemplateById(ctx, templateID)
3131
if err != nil {
3232
a.sendAPIStoreError(c, http.StatusNotFound, fmt.Sprintf("Error when getting template: %s", err))
3333
telemetry.ReportCriticalError(ctx, "error when getting env", err, telemetry.WithTemplateID(templateID))

packages/api/internal/handlers/template_start_build_v2.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func (a *APIStore) PostV2TemplatesTemplateIDBuildsBuildID(c *gin.Context, templa
7171
return
7272
}
7373

74-
dbTeamID := templateBuildDB.Env.TeamID.String()
74+
dbTeamID := templateBuildDB.ActiveEnv.TeamID.String()
7575
team, apiErr := a.GetTeam(ctx, c, &dbTeamID)
7676
if apiErr != nil {
7777
a.sendAPIStoreError(c, apiErr.Code, apiErr.ClientMsg)
@@ -80,7 +80,7 @@ func (a *APIStore) PostV2TemplatesTemplateIDBuildsBuildID(c *gin.Context, templa
8080
return
8181
}
8282

83-
if team.ID != templateBuildDB.Env.TeamID {
83+
if team.ID != templateBuildDB.ActiveEnv.TeamID {
8484
a.sendAPIStoreError(c, http.StatusForbidden, "User does not have access to the template")
8585

8686
telemetry.ReportCriticalError(ctx, "user does not have access to the template", err, telemetry.WithTemplateID(templateID))

packages/api/internal/handlers/template_tags.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func (a *APIStore) PostTemplatesTags(c *gin.Context) {
103103
return
104104
}
105105

106-
template := result.Env
106+
template := result.ActiveEnv
107107
buildID := result.EnvBuild.ID
108108

109109
telemetry.SetAttributes(ctx,
@@ -129,7 +129,7 @@ func (a *APIStore) PostTemplatesTags(c *gin.Context) {
129129

130130
// Create the tag assignments
131131
for _, tag := range tags {
132-
err = client.CreateTemplateBuildAssignment(ctx, queries.CreateTemplateBuildAssignmentParams{
132+
rows, err := client.CreateTemplateBuildAssignment(ctx, queries.CreateTemplateBuildAssignmentParams{
133133
TemplateID: template.ID,
134134
BuildID: buildID,
135135
Tag: tag,
@@ -138,6 +138,11 @@ func (a *APIStore) PostTemplatesTags(c *gin.Context) {
138138
telemetry.ReportCriticalError(ctx, "error when creating tag assignment", err)
139139
a.sendAPIStoreError(c, http.StatusInternalServerError, "Error creating tag assignment")
140140

141+
return
142+
}
143+
if rows == 0 {
144+
a.sendAPIStoreError(c, http.StatusNotFound, fmt.Sprintf("Template '%s' not found", template.ID))
145+
141146
return
142147
}
143148
}

packages/api/internal/handlers/templates_list.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,19 @@ func (a *APIStore) GetTemplates(c *gin.Context, params api.GetTemplatesParams) {
6969
}
7070

7171
templates = append(templates, &api.Template{
72-
TemplateID: item.Env.ID,
72+
TemplateID: item.ActiveEnv.ID,
7373
BuildID: item.BuildID.String(),
7474
CpuCount: api.CPUCount(item.BuildVcpu),
7575
MemoryMB: api.MemoryMB(item.BuildRamMb),
7676
DiskSizeMB: api.DiskSizeMB(diskMB),
77-
Public: item.Env.Public,
77+
Public: item.ActiveEnv.Public,
7878
Aliases: item.Aliases,
7979
Names: item.Names,
80-
CreatedAt: item.Env.CreatedAt,
81-
UpdatedAt: item.Env.UpdatedAt,
82-
LastSpawnedAt: item.Env.LastSpawnedAt,
83-
SpawnCount: item.Env.SpawnCount,
84-
BuildCount: item.Env.BuildCount,
80+
CreatedAt: item.ActiveEnv.CreatedAt,
81+
UpdatedAt: item.ActiveEnv.UpdatedAt,
82+
LastSpawnedAt: item.ActiveEnv.LastSpawnedAt,
83+
SpawnCount: item.ActiveEnv.SpawnCount,
84+
BuildCount: item.ActiveEnv.BuildCount,
8585
BuildStatus: getCorrespondingTemplateBuildStatus(ctx, item.BuildStatus),
8686
CreatedBy: createdBy,
8787
EnvdVersion: envdVersion,

0 commit comments

Comments
 (0)