Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ed5fe39
perf(backend): batch digest and push writes, read in parallel, purge …
Makisuo Nov 10, 2026
43fd814
chore(backend): share the bearer token and ingest key parsers, delete…
Makisuo Nov 10, 2026
4c5c0dd
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
95c8846
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
60effa0
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
01092dd
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
430eb2c
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
e356152
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
a36c9b7
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
ec6e225
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
639c6e5
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
7efcde4
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
6197992
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
8e61c4b
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
ca19b8e
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
4dec1dc
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
f00495a
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
0d1d10a
Merge branch 'fix/audit-agent-runtime' into perf/audit-backend-services
Makisuo Nov 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/backend/src/platform/Crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ export const parseBase64Aes256GcmKey = <E>(raw: string, onError: (message: strin
return Effect.succeed(decoded)
})

/** The same parse for `MAPLE_INGEST_KEY_ENCRYPTION_KEY`, with messages that name the variable. */
export const parseIngestKeyEncryptionKey = <E>(raw: string, onError: (message: string) => E) =>
parseBase64Aes256GcmKey(raw, (message) =>
onError(
message === "Expected a non-empty base64 encryption key"
? "MAPLE_INGEST_KEY_ENCRYPTION_KEY is required"
: message === "Expected base64 for exactly 32 bytes"
? "MAPLE_INGEST_KEY_ENCRYPTION_KEY must be base64 for exactly 32 bytes"
: message,
),
)

/**
* Optional additional authenticated data. AAD is authenticated but not stored,
* so it binds a ciphertext to the row it was written for: decryption only
Expand Down
7 changes: 4 additions & 3 deletions packages/backend/src/platform/invocation-context.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "@effect/vitest"
import { Context, Effect, Scope } from "effect"
import { captureReusableContext } from "./invocation-context"
import { withoutInvocationContext } from "./invocation-context"
import { DrainRunOnInterrupt, PgConnectionScope, type PgConnectionScopeApi } from "./pg-connection-scope"

class Greeting extends Context.Service<Greeting, { readonly text: string }>()("@maple/test/Greeting") {}
Expand All @@ -11,14 +11,15 @@ const scopeNamed = (name: string): PgConnectionScopeApi & { readonly name: strin
close: Effect.void,
})

describe("captureReusableContext", () => {
describe("withoutInvocationContext", () => {
it.effect("keeps services but lets the later invocation's own scope, drain flag and Scope win", () =>
Effect.gen(function* () {
const constructionScope = scopeNamed("construction")
const requestScope = scopeNamed("request")
const constructionResource = yield* Scope.make()

const captured = yield* captureReusableContext<Greeting>().pipe(
const captured = yield* Effect.context<Greeting>().pipe(
Effect.map(withoutInvocationContext),
Effect.provideService(Greeting, { text: "hello" }),
Effect.provideService(PgConnectionScope, constructionScope),
Effect.provideService(DrainRunOnInterrupt, true),
Expand Down
5 changes: 1 addition & 4 deletions packages/backend/src/platform/invocation-context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Context, Effect, Scope, Tracer } from "effect"
import { Context, Scope, Tracer } from "effect"
import { HttpServerRequest } from "effect/http"
import { DrainRunOnInterrupt, PgConnectionScope } from "./pg-connection-scope"

Expand All @@ -17,6 +17,3 @@ export const withoutInvocationContext = <R>(context: Context.Context<R>) =>
Tracer.ParentSpan,
HttpServerRequest.HttpServerRequest,
)(context)

/** `Effect.context`, safe to keep and provide on later invocations. */
export const captureReusableContext = <R>() => Effect.map(Effect.context<R>(), withoutInvocationContext)
Original file line number Diff line number Diff line change
Expand Up @@ -1547,11 +1547,9 @@ const make: Effect.Effect<
let openBudget = MAX_OPENS_PER_TICK

// Detector-state writes are accumulated here and flushed as one multi-row
// upsert after the loop, rather than one `dbExecute` per decision. Under
// `DatabasePgLive` each execute dials and tears down its own postgres.js
// client, so the handshake count is what costs, not the statement count —
// same trade as `error-tick-persistence.ts`. This loop averaged ~70 dials
// per org tick and peaked at 628.
// upsert after the loop, rather than one `dbExecute` per decision: that was
// a round trip per row, ~70 per org tick on average and 628 at peak. The
// invocation's pool is reused across executes, so it saves round trips, not dials.
//
// Keyed by `detectorKey` rather than a plain list: the consolidated-incident
// refcount below has to see this tick's own buffered writes, and last-wins
Expand Down
40 changes: 22 additions & 18 deletions packages/backend/src/services/alerts/OnCallSchedulesService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,24 +309,28 @@ export class OnCallSchedulesService extends Context.Service<
const nowMs = yield* runtime.now
const [from, to] = nowWindow(nowMs)
const [rows, nearby, active, chains] = yield* dbExecute((db) =>
Effect.all([
db.run(
PG.from(OnCallSchedules)
.select()
.where(($) => [$.orgId.eq(orgId)])
.orderBy(["name", "asc"])
.limit(MAX_ONCALL_SCHEDULES_PER_ORG),
),
overlappingOverrides(db, orgId, from, to),
// The org-wide window above is capped, so the overrides that decide who is on
// call now are read on their own: at most the upcoming cap per schedule.
db.run(
PG.from(OnCallOverrides)
.select()
.where(($) => [$.orgId.eq(orgId), $.startsAt.lte(nowMs), $.endsAt.gt(nowMs)]),
),
chainIdsBySchedule(db, orgId),
]),
Effect.all(
[
db.run(
PG.from(OnCallSchedules)
.select()
.where(($) => [$.orgId.eq(orgId)])
.orderBy(["name", "asc"])
.limit(MAX_ONCALL_SCHEDULES_PER_ORG),
),
overlappingOverrides(db, orgId, from, to),
// The org-wide window above is capped, so the overrides that decide who is on
// call now are read on their own: at most the upcoming cap per schedule.
db.run(
PG.from(OnCallOverrides)
.select()
.where(($) => [$.orgId.eq(orgId), $.startsAt.lte(nowMs), $.endsAt.gt(nowMs)]),
),
chainIdsBySchedule(db, orgId),
],
// Four independent reads, so they share the pool instead of queueing.
{ concurrency: "unbounded" },
),
)
const overrides = Arr.dedupeWith([...active, ...nearby], (a, b) => a.id === b.id)
yield* Effect.annotateCurrentSpan("result.rowCount", rows.length)
Expand Down
75 changes: 41 additions & 34 deletions packages/backend/src/services/alerts/alert-escalation-runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,27 +328,31 @@ export const loadStepRecipients = (
const scheduleIds = Arr.dedupe(
Arr.flatMap(step.targets, (target) => (target.type === "schedule" ? [target.scheduleId] : [])),
)
const [schedules, overrides] = yield* Effect.all([
scheduleIds.length === 0
? Effect.succeed([])
: db.run(
PG.from(OnCallSchedules)
.select("id", "timeZone", "layersJson")
.where(($) => [$.orgId.eq(orgId), $.id.in_(...scheduleIds)]),
),
scheduleIds.length === 0
? Effect.succeed([])
: db.run(
PG.from(OnCallOverrides)
.select("id", "scheduleId", "userId", "startsAt", "endsAt", "createdAt")
.where(($) => [
$.orgId.eq(orgId),
$.scheduleId.in_(...scheduleIds),
$.endsAt.gt(nowMs - ONCALL_LOOKAROUND_MS),
$.startsAt.lt(nowMs + ONCALL_LOOKAROUND_MS),
]),
),
])
// Both pairs below are independent reads: they share the pool instead of queueing.
const [schedules, overrides] = yield* Effect.all(
[
scheduleIds.length === 0
? Effect.succeed([])
: db.run(
PG.from(OnCallSchedules)
.select("id", "timeZone", "layersJson")
.where(($) => [$.orgId.eq(orgId), $.id.in_(...scheduleIds)]),
),
scheduleIds.length === 0
? Effect.succeed([])
: db.run(
PG.from(OnCallOverrides)
.select("id", "scheduleId", "userId", "startsAt", "endsAt", "createdAt")
.where(($) => [
$.orgId.eq(orgId),
$.scheduleId.in_(...scheduleIds),
$.endsAt.gt(nowMs - ONCALL_LOOKAROUND_MS),
$.startsAt.lt(nowMs + ONCALL_LOOKAROUND_MS),
]),
),
],
{ concurrency: "unbounded" },
)
const shifts = Arr.map(scheduleIds, (scheduleId) => {
const schedule = Arr.findFirst(schedules, (row) => row.id === scheduleId)
if (Option.isNone(schedule)) return { scheduleId, userId: null }
Expand All @@ -368,19 +372,22 @@ export const loadStepRecipients = (
)
const base = { userIds, gapScheduleIds, onCall, missingUserIds: [] }
if (userIds.length === 0) return { ...base, recipients: [] } satisfies StepRecipients
const [identities, workspaces] = yield* Effect.all([
db.run(
PG.from(ChatIdentities)
.select("id", "userId", "connector", "externalUserId", "createdAt")
.where(($) => [$.orgId.eq(orgId), $.userId.in_(...userIds)])
.orderBy(["createdAt", "asc"], ["id", "asc"]),
),
db.run(
PG.from(ChatWorkspaces)
.select("connector")
.where(($) => [$.orgId.eq(orgId)]),
),
])
const [identities, workspaces] = yield* Effect.all(
[
db.run(
PG.from(ChatIdentities)
.select("id", "userId", "connector", "externalUserId", "createdAt")
.where(($) => [$.orgId.eq(orgId), $.userId.in_(...userIds)])
.orderBy(["createdAt", "asc"], ["id", "asc"]),
),
db.run(
PG.from(ChatWorkspaces)
.select("connector")
.where(($) => [$.orgId.eq(orgId)]),
),
],
{ concurrency: "unbounded" },
)
const linked = new Set(Arr.map(workspaces, (row) => row.connector))
// A connector that cannot open a DM, or with no workspace to send through, pages nobody.
const chatRecipients = Arr.flatMap(identities, (identity): ReadonlyArray<MemberRecipient> =>
Expand Down
17 changes: 0 additions & 17 deletions packages/backend/src/services/alerts/alert-templates/index.ts

This file was deleted.

This file was deleted.

Loading
Loading