History

Backfill last_seen_at from connection activity too (#1493)

Rhys Sullivan committed 3 months ago

Commit 8369cdd1 parent c333ea21 file · +32 −11Browse files

Showing the changes to apps/cloud/scripts (1 of 1 file). Show all

Unified · Split

apps/cloud/scripts/backfill-subjects.ts+32−11
@@ -14,13 +14,17 @@ · 13 unmodified lines
14//14//
15// created_at ← the tenant+subject's oldest connection, the closest honest15// created_at ← the tenant+subject's oldest connection, the closest honest
16// answer to "when did this user first appear".16// answer to "when did this user first appear".
17// last_seen_at ← NULL. The column means "seen on a request", no request has17// last_seen_at ← the tenant+subject's newest connection `updated_at`, the
18// been recorded, and the console renders NULL as "Never" —18// latest activity storage can attest to. Rendering every
19// which is true since the sighting code deployed.19// pre-0012 user as "Never" was literally true of the sighting
20// column but reads as "inactive user" to an operator, which
21// is the wrong answer for a tenant full of active people. The
22// first real sighting overwrites this with request truth.
20//23//
21// Idempotent — ON CONFLICT on the (tenant, external_id) unique index leaves24// Idempotent — the insert's ON CONFLICT on the (tenant, external_id) unique
22// rows the runtime has since minted (or a prior run inserted) untouched, so25// index leaves rows the runtime has since minted (or a prior run inserted)
23// re-running is safe and never rewinds a live row's fields.26// untouched, and the last_seen_at repair only fills NULLs — so re-running is
27// safe and never rewinds a live row's fields.
24// Pass --dry-run to print the plan without writing.28// Pass --dry-run to print the plan without writing.
25// ---------------------------------------------------------------------------29// ---------------------------------------------------------------------------
26 30
@@ -37,8 +41,12 @@ if (!connectionString) { · 10 unmodified lines
37 41
38const sql = postgres(connectionString, { max: 1, prepare: false, ssl: "require" });42const sql = postgres(connectionString, { max: 1, prepare: false, ssl: "require" });
39 43
40const rows = await sql<{ tenant: string; subject: string; first_connection_at: Date }[]>`44const rows = await sql<
41 SELECT tenant, subject, MIN(created_at) AS first_connection_at45 { tenant: string; subject: string; first_connection_at: Date; last_activity_at: Date }[]
46>`
47 SELECT tenant, subject,
48 MIN(created_at) AS first_connection_at,
49 MAX(updated_at) AS last_activity_at
42 FROM connection50 FROM connection
43 WHERE subject <> ''51 WHERE subject <> ''
44 GROUP BY tenant, subject52 GROUP BY tenant, subject
@@ -47,20 +55,33 @@ const rows = await sql<{ tenant: string; subject: string; first_connection_at: D · 2 unmodified lines
47console.log(`${rows.length} (tenant, subject) pair(s) with connections`);55console.log(`${rows.length} (tenant, subject) pair(s) with connections`);
48 56
49let inserted = 0;57let inserted = 0;
58let repaired = 0;
50for (const row of rows) {59for (const row of rows) {
51 console.log(60 console.log(
52 `${row.tenant} ${row.subject} first connection ${row.first_connection_at.toISOString()}`,61 `${row.tenant} ${row.subject} first connection ${row.first_connection_at.toISOString()} last activity ${row.last_activity_at.toISOString()}`,
53 );62 );
54 if (dryRun) continue;63 if (dryRun) continue;
64 const lastSeenMs = row.last_activity_at.getTime();
55 const result = await sql`65 const result = await sql`
56 INSERT INTO subject (row_id, tenant, external_id, created_at, last_seen_at, status)66 INSERT INTO subject (row_id, tenant, external_id, created_at, last_seen_at, status)
57 VALUES (${createId()}, ${row.tenant}, ${row.subject}, ${row.first_connection_at}, NULL, NULL)67 VALUES (${createId()}, ${row.tenant}, ${row.subject}, ${row.first_connection_at}, ${lastSeenMs}, NULL)
58 ON CONFLICT (tenant, external_id) DO NOTHING68 ON CONFLICT (tenant, external_id) DO NOTHING
59 `;69 `;
60 inserted += result.count;70 inserted += result.count;
71 if (result.count > 0) continue;
72 // The row predates this run but may still carry NULL from a run of the
73 // pre-repair version of this script. Fill it in; a sighted row keeps its
74 // request-truth timestamp.
75 const repair = await sql`
76 UPDATE subject SET last_seen_at = ${lastSeenMs}
77 WHERE tenant = ${row.tenant} AND external_id = ${row.subject} AND last_seen_at IS NULL
78 `;
79 repaired += repair.count;
61}80}
62 81
63console.log(82console.log(
64 dryRun ? `dry run — would insert up to ${rows.length} row(s)` : `inserted ${inserted} row(s)`,83 dryRun
84 ? `dry run — would insert up to ${rows.length} row(s)`
85 : `inserted ${inserted} row(s), repaired last_seen_at on ${repaired} row(s)`,
65);86);
66await sql.end();87await sql.end();