Andi Miller
7th October 2026
Volume
| Events | 138k/s at daily peak |
| Datapoints | 7.5m/s at daily peak |
| Page views | 40bn over 30 days |
| Devices | 4.3bn over 30 days |
Dimensionality
| Dimensions | ~20 |
| Cardinality | min 4, median 24, max 10k |
| Lookback | 30 days to 1 year |
Nearly every metric is a Count Distinct, and at that scale we have to approximate them: Apache DataSketches
| Layer | Store | Data |
|---|---|---|
| Bronze | BigQuery | raw data |
| Silver | Iceberg | cleaned and organised |
| Gold | Apache Pinot | fully indexed |
We serve all insights from Pinot today, but it’s not feasible to generate full indexes for every query, and the unindexed queries can harm cluster performance.
Pinot: p99 of 30 seconds
BigQuery: p99 of… 30 minutes?
val results = mutable.Map.empty[String, Set[UserId]]
for id <- graph.topologicalSort do
results(id) = run(graph.nodeMap(id), inputs(results, id))val algebra: Algebra[QueryF, IO[Set[UserId]]] =
Algebra:
case QueryF.Query(sql) => pinot.query(sql)
case QueryF.Union(a, b) => (a, b).mapN(_ ++ _)
scheme.cata(algebra).apply(queryTree)DataGraph[Id, NodeData, EdgeData] — directed edges, data
on nodes and edges
enum Node:
case Query(sql: String)
case Union
val graph: DataGraph[String, Node, Unit] =
DataGraph
.empty[String, Node, Unit]
.addNode("cohort-a", Node.Query("select ..."))
.addNode("cohort-b", Node.Query("select ..."))
.addNode("union", Node.Union)
.addEdge("cohort-a", "union", ())
.addEdge("cohort-b", "union", ())type UserId = String
object QueryVisitor
extends SimpleDagVisitor[IO, String, Node, Set[UserId], Unit]:
def run(id: String, node: Node,
inputs: Map[String, Set[UserId]]): IO[Set[UserId]] =
node match
case Node.Query(sql) => pinot.query(sql)
case Node.Union => IO.pure(inputs.values.toSet.flatten)
val results: IO[DataGraph[String, Set[UserId], Unit]] =
DagVisitor.runConcurrent(QueryVisitor)(graph)While a run is in progress, every node is in one of two states:
Righttrait SimpleDagVisitor[F[_], Id, In, Out, Edge]:
def run(id: Id, node: In, inputs: Map[Id, Out]): F[Out]run must always return an
F[Out]Our Either can’t say “waiting”, so it grows a third
case:
enum NodeState[In, Out, Wait]:
case Pending(input: In)
case Waiting(input: In, handle: Wait)
case Done(output: Out)Waiting holds a handle to the external workRunning: a fibre doesn’t survive a
restartenum StepResult[Out, Wait]:
case Complete(output: Out)
case Suspend(handle: Wait)
trait SimpleSuspendableDagVisitor[F[_], Id, In, Out, Edge, Wait, Payload]:
def run(id: Id, node: In,
inputs: Map[Id, Out]): F[StepResult[Out, Wait]]
def resume(id: Id, node: In, handle: Wait, payload: Payload,
inputs: Map[Id, Out]): F[StepResult[Out, Wait]]run can now answer “not yet”: Suspend with
a handleresume is called when the external work delivers a
payloadimport StepResult.*
type JobId = String
object QueryVisitor extends SimpleSuspendableDagVisitor[
IO, String, Node, Set[UserId], Unit, JobId, Unit]:
def run(id: String, node: Node, inputs: Map[String, Set[UserId]]) =
node match
case Node.Pinot(sql) => pinot.query(sql).map(Complete(_))
case Node.BigQuery(sql) => bq.submitJob(sql).map(Suspend(_))
case Node.Union => IO.pure(Complete(inputs.values.toSet.flatten))
def resume(id: String, node: Node, handle: JobId, payload: Unit,
inputs: Map[String, Set[UserId]]) =
bq.readJobResult(handle).map(Complete(_)){
"version": 1,
"direction": "flow",
"nodes": [
{ "id": "cohort-a",
"state": { "type": "done", "output": ["device-1", "device-2"] } },
{ "id": "cohort-b",
"state": { "type": "waiting",
"input": { "type": "bigquery", "sql": "select ..." },
"handle": "bq-job-4ca1" } },
{ "id": "union",
"state": { "type": "pending", "input": { "type": "union" } } }
],
"edges": [
{ "from": "cohort-a", "to": "union", "data": {} },
{ "from": "cohort-b", "to": "union", "data": {} }
]
}def save(runId: RunId, run: RunResult[...]): IO[Unit] = run match
case Finished(results) => db.storeResults(runId, results)
case Suspended(snap) => db.storeSnapshot(runId, snap)
HttpRoutes.of[IO]:
case req @ POST -> Root / "query" =>
for
graph <- req.as[UserQuery].map(unfold)
runId <- RunId.random
run <- SuspendableDagVisitor.start(QueryVisitor)(graph)
_ <- save(runId, run)
resp <- Accepted(runId)
yield resp
case GET -> Root / "poll" / runId =>
db.readResults(runId).flatMap:
case Some(results) => Ok(results)
case None => Accepted("still running")
case POST -> Root / "bq_callback" / jobId =>
for
(runId, nodeId, snap) <- db.readSnapshotFor(jobId)
run <- SuspendableDagVisitor.resume(QueryVisitor)(snap, Map(nodeId -> ()))
_ <- save(runId, run)
resp <- Ok()
yield resp| Imperative Visitor | Catamorphism | Suspendable Visitor | |
|---|---|---|---|
| Scheduling | sequential | structural order | concurrent, by readiness |
| Run state | a map you could checkpoint | half-applied functions | a serialisable graph |
| Snapshots | ✓ between nodes | ✗ | ✓ |
| Dependencies | none | recursion schemes | cats-effect |