Skip to content

Commit 9219790

Browse files
charlie-e2be2b-bot[bot]
authored andcommitted
fix(db): fill build env_id and team_id in one primary-key update
GitOrigin-RevId: dad6453e9f325fb456180359902d82ad5ad6faa0
1 parent ec15029 commit 9219790

2 files changed

Lines changed: 175 additions & 0 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
-- +goose Up
2+
-- The two AFTER INSERT triggers on env_build_assignments each filled one
3+
-- env_builds column with `UPDATE ... WHERE id = NEW.build_id AND <col> IS NULL`.
4+
-- When statistics under-count the rows where team_id is NULL, the planner can
5+
-- drive that UPDATE from a team_id-leading index instead of the primary key, and
6+
-- every insert into env_build_assignments then walks the index's NULL entries,
7+
-- including dead ones that vacuum cannot remove yet. The cost per insert grows
8+
-- with that backlog.
9+
--
10+
-- One function now fills both columns in a single UPDATE whose only indexable
11+
-- predicate is the primary key: num_nulls() cannot be used as an index
12+
-- condition. Each column is still filled only while it is NULL, from the first
13+
-- assignment, and an insert writes one env_builds row version instead of two.
14+
-- The second function becomes a no-op; both triggers stay in place.
15+
--
16+
-- CREATE OR REPLACE keeps each function's oid, owner and ACL, so the triggers
17+
-- pick up the new bodies at commit. No lock is taken on either table.
18+
SET LOCAL lock_timeout = '2s';
19+
20+
-- +goose StatementBegin
21+
CREATE OR REPLACE FUNCTION public.backfill_env_id_from_assignment()
22+
RETURNS trigger
23+
LANGUAGE plpgsql
24+
AS $$
25+
BEGIN
26+
UPDATE public.env_builds
27+
SET env_id = COALESCE(env_id, NEW.env_id),
28+
team_id = COALESCE(team_id, (SELECT e.team_id FROM public.envs e WHERE e.id = NEW.env_id))
29+
WHERE id = NEW.build_id
30+
AND num_nulls(env_id, team_id) > 0;
31+
RETURN NEW;
32+
END;
33+
$$;
34+
-- +goose StatementEnd
35+
36+
-- +goose StatementBegin
37+
CREATE OR REPLACE FUNCTION public.backfill_team_id_from_assignment()
38+
RETURNS trigger
39+
LANGUAGE plpgsql
40+
AS $$
41+
BEGIN
42+
-- backfill_env_id_from_assignment() fills team_id as well.
43+
RETURN NEW;
44+
END;
45+
$$;
46+
-- +goose StatementEnd
47+
48+
-- +goose Down
49+
-- The bodies from 20260204172712_remove_build_assignment_triggers.sql and
50+
-- 20260218120000_add_team_id_to_env_builds.sql.
51+
SET LOCAL lock_timeout = '2s';
52+
53+
-- +goose StatementBegin
54+
CREATE OR REPLACE FUNCTION public.backfill_env_id_from_assignment()
55+
RETURNS trigger
56+
LANGUAGE plpgsql
57+
AS $$
58+
BEGIN
59+
UPDATE env_builds SET env_id = NEW.env_id WHERE id = NEW.build_id AND env_id IS NULL;
60+
RETURN NEW;
61+
END;
62+
$$;
63+
-- +goose StatementEnd
64+
65+
-- +goose StatementBegin
66+
CREATE OR REPLACE FUNCTION public.backfill_team_id_from_assignment()
67+
RETURNS trigger
68+
LANGUAGE plpgsql
69+
AS $$
70+
BEGIN
71+
UPDATE env_builds
72+
SET team_id = (SELECT team_id FROM envs WHERE id = NEW.env_id)
73+
WHERE id = NEW.build_id AND team_id IS NULL;
74+
RETURN NEW;
75+
END;
76+
$$;
77+
-- +goose StatementEnd
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package tests
2+
3+
import (
4+
"database/sql"
5+
"testing"
6+
7+
"github.com/google/uuid"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/e2b-dev/infra/packages/db/pkg/testutils"
11+
)
12+
13+
// Inserting into env_build_assignments fills env_builds.env_id and team_id
14+
// from the first assignment, only while they are NULL, in a single UPDATE.
15+
func TestBuildAssignmentBackfillsBothColumnsInOneUpdate(t *testing.T) {
16+
t.Parallel()
17+
18+
db := testutils.SetupDatabase(t)
19+
ctx := t.Context()
20+
sqlDB, err := sql.Open("pgx", db.ConnStr())
21+
require.NoError(t, err)
22+
t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
23+
24+
team, otherTeam := testutils.CreateTestTeam(t, db), testutils.CreateTestTeam(t, db)
25+
template := testutils.CreateTestTemplate(t, db, team)
26+
otherTemplate := testutils.CreateTestTemplate(t, db, otherTeam)
27+
28+
insertBuild := func(t *testing.T, envID *string, teamID *uuid.UUID) uuid.UUID {
29+
t.Helper()
30+
id := uuid.New()
31+
_, err := sqlDB.ExecContext(ctx, `INSERT INTO public.env_builds
32+
(id, env_id, team_id, status, vcpu, ram_mb, free_disk_size_mb, kernel_version, firecracker_version, cluster_node_id, created_at, updated_at)
33+
VALUES ($1, $2, $3, 'success', 2, 2048, 512, '6.1.0', '1.4.0', 'test-node', NOW(), NOW())`, id, envID, teamID)
34+
require.NoError(t, err)
35+
36+
return id
37+
}
38+
// assign inserts one assignment and returns how many env_builds row
39+
// versions the insert wrote. The xact counters also carry this backend's
40+
// unflushed counts from earlier transactions, so take the difference around
41+
// the insert.
42+
assign := func(t *testing.T, envID string, buildID uuid.UUID, tag string) int64 {
43+
t.Helper()
44+
tx, err := sqlDB.BeginTx(ctx, nil)
45+
require.NoError(t, err)
46+
defer func() { _ = tx.Rollback() }()
47+
updates := func() int64 {
48+
var n int64
49+
require.NoError(t, tx.QueryRowContext(ctx,
50+
`SELECT coalesce((SELECT n_tup_upd FROM pg_stat_xact_user_tables WHERE relid = 'public.env_builds'::regclass), 0)`).Scan(&n))
51+
52+
return n
53+
}
54+
before := updates()
55+
_, err = tx.ExecContext(ctx, `INSERT INTO public.env_build_assignments (env_id, build_id, tag, source, created_at)
56+
VALUES ($1, $2, $3, 'app', NOW())`, envID, buildID, tag)
57+
require.NoError(t, err)
58+
written := updates() - before
59+
require.NoError(t, tx.Commit())
60+
61+
return written
62+
}
63+
read := func(t *testing.T, buildID uuid.UUID) (*string, *uuid.UUID) {
64+
t.Helper()
65+
var envID *string
66+
var teamID *uuid.UUID
67+
require.NoError(t, sqlDB.QueryRowContext(ctx, `SELECT env_id, team_id FROM public.env_builds WHERE id = $1`, buildID).Scan(&envID, &teamID))
68+
69+
return envID, teamID
70+
}
71+
72+
preset := "preset-env"
73+
fresh := insertBuild(t, nil, nil)
74+
envOnly := insertBuild(t, &preset, nil)
75+
both := insertBuild(t, &preset, &otherTeam)
76+
77+
for _, tc := range []struct {
78+
name string
79+
envID string
80+
buildID uuid.UUID
81+
tag string
82+
wantUpdates int64
83+
wantEnv string
84+
wantTeam uuid.UUID
85+
}{
86+
{"fills both columns", template, fresh, "default", 1, template, team},
87+
{"keeps a set env_id and fills team_id", template, envOnly, "default", 1, preset, team},
88+
{"leaves both set columns alone", template, both, "default", 0, preset, otherTeam},
89+
{"first assignment wins", otherTemplate, fresh, "other", 0, template, team},
90+
} {
91+
require.Equal(t, tc.wantUpdates, assign(t, tc.envID, tc.buildID, tc.tag), tc.name)
92+
envID, teamID := read(t, tc.buildID)
93+
require.NotNil(t, envID, tc.name)
94+
require.NotNil(t, teamID, tc.name)
95+
require.Equal(t, tc.wantEnv, *envID, tc.name)
96+
require.Equal(t, tc.wantTeam, *teamID, tc.name)
97+
}
98+
}

0 commit comments

Comments
 (0)