fix(registry): heartbeat promotes provisioning → online atomically (#2500) #2562
@@ -702,6 +702,7 @@ func (h *RegistryHandler) Heartbeat(c *gin.Context) {
|
||||
uptime_seconds = $5,
|
||||
current_task = $6,
|
||||
monthly_spend = $7,
|
||||
status = CASE WHEN status = 'provisioning' THEN 'online' ELSE status END,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND status != 'removed'
|
||||
`, payload.WorkspaceID, payload.ErrorRate, payload.SampleError,
|
||||
@@ -716,6 +717,7 @@ func (h *RegistryHandler) Heartbeat(c *gin.Context) {
|
||||
active_tasks = $4,
|
||||
uptime_seconds = $5,
|
||||
current_task = $6,
|
||||
status = CASE WHEN status = 'provisioning' THEN 'online' ELSE status END,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND status != 'removed'
|
||||
`, payload.WorkspaceID, payload.ErrorRate, payload.SampleError,
|
||||
|
||||
@@ -197,6 +197,7 @@ const registerUpsertSQL = `
|
||||
const heartbeatUpdateSQL = `
|
||||
UPDATE workspaces SET
|
||||
last_heartbeat_at = now(),
|
||||
status = CASE WHEN status = 'provisioning' THEN 'online' ELSE status END,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND status != 'removed'
|
||||
`
|
||||
@@ -285,6 +286,45 @@ func TestIntegration_RegistryRowState_HeartbeatUpdatesLiveWorkspace(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegration_RegistryRowState_HeartbeatPromotesProvisioningToOnline(t *testing.T) {
|
||||
conn := integrationAuthDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
id := insertWorkspace(t, conn, "provisioning-ws", "provisioning", "")
|
||||
|
||||
if _, err := conn.ExecContext(ctx, heartbeatUpdateSQL, id); err != nil {
|
||||
t.Fatalf("heartbeat update: %v", err)
|
||||
}
|
||||
|
||||
if got := statusOf(t, conn, id); got != "online" {
|
||||
t.Fatalf("provisioning workspace not promoted to online by heartbeat: status=%q, want 'online'", got)
|
||||
}
|
||||
|
||||
var hb sql.NullTime
|
||||
if err := conn.QueryRowContext(ctx,
|
||||
`SELECT last_heartbeat_at FROM workspaces WHERE id = $1`, id).Scan(&hb); err != nil {
|
||||
t.Fatalf("read last_heartbeat_at: %v", err)
|
||||
}
|
||||
if !hb.Valid {
|
||||
t.Fatalf("provisioning workspace heartbeat did NOT bump last_heartbeat_at")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegration_RegistryRowState_HeartbeatProvisioningAlreadyOnlineUnchanged(t *testing.T) {
|
||||
conn := integrationAuthDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
id := insertWorkspace(t, conn, "online-ws", "online", "")
|
||||
|
||||
if _, err := conn.ExecContext(ctx, heartbeatUpdateSQL, id); err != nil {
|
||||
t.Fatalf("heartbeat update: %v", err)
|
||||
}
|
||||
|
||||
if got := statusOf(t, conn, id); got != "online" {
|
||||
t.Fatalf("online workspace status changed unexpectedly by heartbeat: status=%q, want 'online'", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2 — wsauth.ValidateToken A↔B binding (the cross-tenant non-leak boundary).
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user