stat-sync.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import { DateTime, Effect } from "effect"
  2. import { Resource } from "sst/resource"
  3. import { Athena, AthenaQueryError, AthenaQueryTimeoutError } from "./athena"
  4. import { DatabaseError } from "./database"
  5. import { GeoStatRepo, rowsFromAggregates as geoRowsFromAggregates } from "./domain/geo"
  6. import { buildStatsQuery, toGeoAggregate, toModelAggregate, toProviderAggregate } from "./domain/inference"
  7. import { ModelStatRepo, rowsFromAggregates as modelRowsFromAggregates } from "./domain/model"
  8. import { ProviderStatRepo, rowsFromAggregates as providerRowsFromAggregates } from "./domain/provider"
  9. import { startOfIsoWeek } from "./domain/stat"
  10. const DATALAKE_INGESTION_LAG_MS = 5 * 60_000
  11. const STATS_DATA_START_MS = new Date("2026-05-28T00:00:00.000Z").getTime()
  12. const WEEK_MS = 7 * 86_400_000
  13. const DISPLAY_WINDOW_MS = 56 * 86_400_000
  14. // Anchor incremental passes to the ISO week containing this lookback, so the pass
  15. // after a week boundary still recomputes the previous week's final aggregates even
  16. // if the boundary pass itself failed.
  17. const INCREMENTAL_LOOKBACK_MS = 2 * 3_600_000
  18. export type SyncStatsResult = { ok: true; rows: number; startedAt: string; periodStart: string; periodEnd: string }
  19. export type SyncStatsError = AthenaQueryError | AthenaQueryTimeoutError | DatabaseError
  20. export const syncStats: (options?: {
  21. full?: boolean
  22. }) => Effect.Effect<SyncStatsResult, SyncStatsError, Athena | ModelStatRepo | ProviderStatRepo | GeoStatRepo> =
  23. Effect.fn("StatSync.sync")(function* (options?: { full?: boolean }) {
  24. const startedAt = yield* DateTime.nowAsDate
  25. const periodEnd = new Date(Math.floor((startedAt.getTime() - DATALAKE_INGESTION_LAG_MS) / 60_000) * 60_000)
  26. const periodStart = options?.full ? fullPeriodStart(periodEnd) : incrementalPeriodStart(periodEnd)
  27. const athena = yield* Athena
  28. const modelStats = yield* ModelStatRepo
  29. const providerStats = yield* ProviderStatRepo
  30. const geoStats = yield* GeoStatRepo
  31. yield* logRuntimeCheck()
  32. const rows = yield* athena.query(buildStatsQuery(periodStart, periodEnd))
  33. const modelRows = modelRowsFromAggregates(rows.filter((row) => row.dimension === "model").flatMap(toModelAggregate))
  34. const providerRows = providerRowsFromAggregates(
  35. rows.filter((row) => row.dimension === "provider").flatMap(toProviderAggregate),
  36. )
  37. const geoRows = geoRowsFromAggregates(
  38. rows.filter((row) => row.dimension === "geo" || row.dimension === "geo_model").flatMap(toGeoAggregate),
  39. )
  40. yield* Effect.all([modelStats.upsert(modelRows), providerStats.upsert(providerRows), geoStats.upsert(geoRows)], {
  41. concurrency: "unbounded",
  42. discard: true,
  43. })
  44. yield* Effect.all(
  45. [
  46. modelStats.deleteRetiredDimensions(modelRows),
  47. providerStats.deleteRetiredDimensions(providerRows),
  48. geoStats.deleteRetiredDimensions(geoRows),
  49. ],
  50. { concurrency: "unbounded", discard: true },
  51. )
  52. yield* Effect.logInfo(
  53. `stats sync complete ${JSON.stringify({
  54. startedAt: startedAt.toISOString(),
  55. periodStart: periodStart.toISOString(),
  56. periodEnd: periodEnd.toISOString(),
  57. rows: modelRows.length,
  58. providerRows: providerRows.length,
  59. geoRows: geoRows.length,
  60. stage: Resource.App.stage,
  61. })}`,
  62. )
  63. return {
  64. ok: true,
  65. rows: modelRows.length,
  66. startedAt: startedAt.toISOString(),
  67. periodStart: periodStart.toISOString(),
  68. periodEnd: periodEnd.toISOString(),
  69. }
  70. })
  71. // May 27 was partial, so keep Athena stats anchored at the first complete day.
  72. function fullPeriodStart(periodEnd: Date) {
  73. return new Date(
  74. Math.max(
  75. Math.min(startOfIsoWeek(periodEnd).getTime() - WEEK_MS, periodEnd.getTime() - DISPLAY_WINDOW_MS),
  76. STATS_DATA_START_MS,
  77. ),
  78. )
  79. }
  80. // Events are append-only, so completed periods never change once synced; hourly
  81. // passes only recompute the periods the current ISO week can still touch. The daily
  82. // full pass refreshes the whole display window (normalization changes, retired
  83. // dimension cleanup).
  84. function incrementalPeriodStart(periodEnd: Date) {
  85. return new Date(
  86. Math.max(startOfIsoWeek(new Date(periodEnd.getTime() - INCREMENTAL_LOOKBACK_MS)).getTime(), STATS_DATA_START_MS),
  87. )
  88. }
  89. function logRuntimeCheck() {
  90. return Effect.logInfo(
  91. `athena stats runtime check ${JSON.stringify({
  92. catalog: Resource.InferenceEvent.catalog,
  93. database: Resource.InferenceEvent.database,
  94. dataset: Resource.StatsSyncConfig.dataset,
  95. table: Resource.InferenceEvent.table,
  96. workgroup: Resource.InferenceEvent.workgroup,
  97. region: Resource.InferenceEvent.region,
  98. stage: Resource.App.stage,
  99. })}`,
  100. )
  101. }