Report execution at scale
Table of Contents
- 1. Summary
- 2. Detail
- 2.1. What this page adds, and what it defers
- 2.2. The participants
- 2.3. The rules every part follows
- 2.4. The identity of a run
- 2.5. The life of a report, end to end
- 2.6. Thousands of runs at once
- 2.7. Failure and recovery
- 2.8. Observability
- 2.9. Decisions this design takes
- 2.10. What this design does not settle
- 3. Patterns used
- 4. See also
1. Summary
This page states the target design. It does not describe what is built.
A report belongs to a tenant and a party. Row-level security uses the same pair. A scheduled run has no caller, so it has no token to forward. The scheduler and the step services act as themselves, and a service identity has no tenant and no party. This page closes that gap and shows the whole path. It covers every service from the creation of a report to its completed instance.
The design has three parts:
- A run grant. When a person schedules a report, IAM records their consent.
The grant says that runs of this definition may act in this tenant and this
party with a narrow role, the
ReportRunrole. The grant is not a credential. - A run token. A step service exchanges its own token and the grant id for
a short-lived token. The token carries the tenant, the party and the
ReportRunpermissions. The service keeps it in a memory cache and exchanges again while the work continues, so a run of hours lives through many tokens, and the workflow state never holds it. The owners read and write as that tenant and that party, and their handlers do not change. - Fair shares. Three queues hold the work of many tenants: admission in reporting, step concurrency in each step service, and dispatch in the compute grid. Each queue gives every tenant a share, so one tenant with many reports cannot hold back the others.
The first two parts are the Scheduled Work Acting as a Tenant pattern, and IAM implements them as Run grants and run tokens. This page is the end-to-end view. The pages it links own the detail of each part, and this page does not restate them.
2. Detail
2.1. What this page adds, and what it defers
The report processing cluster describes the phases of a run. The workflow cluster describes the engine. The scheduler cluster describes the firing. This page adds the parts that no other page holds, and it links the rest.
| Concern | Owner of the detail | What this page adds |
|---|---|---|
| The definition, the schedule and the instance states | Report definition and schedule | the run grant on the definition; admission by quota |
| The phases of a run | Report execution pipeline | which service performs each phase, and with which identity |
| The engine's contract with a step | Report execution orchestration, The step protocol | the run fields in the start message |
| The firing and the trigger | Scheduling and workflows | nothing; the trigger is unchanged |
| The input archive and its digest | Report pre-processing | the owners' operations that supply it, and the as-of instant |
| Submission and the workunit | Report compute handoff | fair dispatch across tenants |
| The result store | Report post-processing | nothing; its owner stays open |
| The identity of a run | Scheduled Work Acting as a Tenant, Run grants and run tokens | the role, the audience and the moments reporting grants and revokes |
| Every subject and storage key | Report processing interfaces | the IAM subjects below |
2.2. The participants
| Service | Owns | Part in a run | Identity it uses in a run |
|---|---|---|---|
ores.iam |
accounts, roles, run grants, the signing key | creates and revokes grants; exchanges a grant for a run token | itself |
ores.scheduler |
job definitions and firings | fires the job and calls the trigger | itself |
ores.reporting |
definitions, run documents, instances, results | admits the run, starts it, collects results, finalises | itself to admit; the run token for steps |
ores.workflow |
workflow instances and steps | dispatches each step, enforces deadlines, compensates | itself |
ores.ore |
the mapping to the engine's documents | prepares the input archive | the run token |
ores.refdata, ores.analytics |
the configuration documents | serve the documents of the party | the caller's: the run token |
ores.trading |
trades | serves the scope read | the caller's: the run token |
ores.marketdata |
market data | resolves what the trades need | the caller's: the run token |
ores.storage |
objects | holds the input, the package and the output | the caller's |
ores.compute |
batches, workunits, results, hosts | creates the batch and dispatches it fairly | the run token to create; itself to dispatch |
| a worker node | nothing | runs the engine | the node identity |
2.3. The rules every part follows
- A run acts as its tenant and its party. Every read and every write of a run goes through row-level security for that pair. No step uses the system tenant to reach tenant data.
- No credential in durable state. The workflow request, the step results, the instance and the logs hold the grant id. They never hold a token.
- Each service owns its tables. A step reads another component's data through that component's operations. The rule is recorded in the memory No service reads or writes another component's tables.
- Every step converges on a repeat. The engine delivers a step at least once. Idempotency in a workflow step states the contract.
- Every queue is fair across tenants. No queue serves work in plain arrival order across tenants.
- The cost of a run does not grow with the number of tenants. No step scans all tenants, and no service holds a standing session per tenant.
2.4. The identity of a run
A scheduled run has no caller, and the scheduler and the step services act as themselves, with no tenant and no party. Reporting therefore uses the Scheduled Work Acting as a Tenant pattern: a run grant that a person gives when they schedule, and a run token that each step service exchanges for the grant and keeps in a cache. Run grants and run tokens is IAM's implementation, and holds the grant entity, the exchange checks, the token's claims, the cache rules and the trust boundaries. This section holds only reporting's choices.
2.4.1. The grants reporting creates
| Field | Reporting's value |
|---|---|
resource |
reporting.report_definition/<id> |
role |
ReportRun |
audience |
ores.reporting.service, ores.ore.service, ores.compute.service |
max_runs, not_after |
empty for a schedule; one run, and the run's longest duration, for a manual run |
Reporting stores the grant id on the definition beside the scheduler job id. That grant is the standing consent for the schedule. When a trigger admits a run, reporting copies the grant id onto the report instance, so the run acts under the grant that was live at admission. A later schedule, unschedule or party change therefore cannot change the authority of a run that is already in flight.
2.4.2. When reporting grants and revokes
- Schedule. A person asks reporting to schedule a definition. Reporting creates the grant on behalf of the person, with their token, and then puts the scheduler job.
- Manual run. A person triggers a run. Reporting creates a grant for that one run on behalf of that person, so the run acts with the authority of the person who triggered it, and not with the schedule's.
- Change of party. A change to the definition's party revokes the grant and creates a new one. The person who makes the change gives the new consent.
- Unschedule or delete. Reporting deletes the job and revokes the grant.
- Lapse. A step reports
grant_lapsed. Reporting fails the run and moves the definition tosuspended. A tenant administrator, or another person of the party, schedules the definition again, which is a new consent.
2.4.3. The ReportRun role
The role holds only what a run needs. Each owner seeds its own permission codes, and IAM seeds the role.
| Owner | Permissions | Why the run needs them |
|---|---|---|
| reporting | read run documents and definitions; write instance progress and results | the run document; collection; finalisation |
| refdata | read curve configuration and conventions documents | the engine's configuration |
| analytics | read pricing engines and today's market documents | the engine's configuration |
| trading | the scope read | the trades in scope |
| marketdata | the resolution read | the market data the trades need |
| storage | write and read under ore/ and reporting/ run keys; read under compute/output/ |
the input archive and the output |
| compute | create a batch and its workunits | submission |
The role has no write on reference data, trades or market data. A run that tries one is refused by the owner's permission check.
2.4.4. The exchange load of reporting
At the design targets below, 1,000 runs in flight for thirty minutes each make about four exchanges a second, and 2,000 runs that start in one minute make about 100 exchanges in that minute. Run grants and run tokens states why one IAM replica serves this.
2.5. The life of a report, end to end
Figure 1: A scheduled report run: the definition and its grant, the firing and admission, and the four steps of the run, each acting as the tenant and the party.
Source: report_execution_at_scale_sequence.puml. Regenerate with
plantuml -tpng doc/knowledge/architecture/report_execution_at_scale_sequence.puml.
2.5.1. Create the definition
A person of the party creates the definition in reporting, with their own
token. The definition holds the report type, the scope, the concurrency policy
and the cron expression. The engine's configuration lives with its owners. The
person imports it with ore.v1.ops.import_run_configuration. ORE runs that import
as a saga over the owners: reporting saves the run document, and refdata and
analytics save the configuration documents. Reporting binds each document to
the definition. Every write uses the person's token, so every row belongs to
the party.
No grant exists yet. A definition without a schedule cannot run, except by a manual trigger.
2.5.2. Schedule it
The person asks reporting to schedule the definition. Reporting performs three actions, in this order:
- It creates the grant through IAM, on behalf of the person.
- It puts the scheduler job. The job name is
report_definition.<id>. The job holds the tenant and the party, so the firings belong to the tenant, and a tenant administrator can see them. - It records the grant id and the job id on the definition.
Each action is idempotent. A repeated grant create for the same definition and grantor returns the active grant. The job name is its identity, so a repeated put changes nothing. A failure after step 1 leaves a grant that no job uses. The next schedule request adopts it.
Reconciliation is as described on the definition page, with two additions. A definition with a job and no active grant is suspended. A job whose definition is gone is deleted, and its grant is revoked.
Figure 2: The schedule of a definition: unscheduled, scheduled under a grant, and suspended when the grant lapses. The definition's own lifecycle is active in the first two and suspended in the third.
Source: report_definition_schedule_states.puml. Regenerate with
plantuml -tpng doc/knowledge/architecture/report_definition_schedule_states.puml.
2.5.3. Fire
The scheduler loop fires the job at its minute. It writes the firing record and then sends the trigger to reporting as an authenticated request with its own identity. The trigger carries the definition, the tenant and the firing id. The scheduler's part ends with the reply.
The trigger needs no tenant identity. Reporting checks that the caller holds the trigger permission, then acts on the definition as itself. That is reporting's own work on its own table. The run's tenant identity starts with the first step.
2.5.4. Admit
Reporting decides the state the instance is born in. Two checks apply, in this order:
- The definition's concurrency policy. It applies when another run of the same definition is in flight: skip, fail or queue, as the definition page describes.
- The quotas. A tenant has a limit on its running instances, and a party has
a limit inside its tenant. A run over either limit is born
queued.
The quotas are configuration in reporting, per tenant, with a platform default. A firing never fails because of a quota. It queues.
Reporting promotes queued runs when a run of the tenant reaches a terminal
state. Promotion takes the oldest queued run of the tenant whose party is under
its limit, moves it to pending, and starts its workflow. Promotion uses a
row lock on the oldest candidate, so two replicas cannot promote the same run.
A sweep every minute promotes any run that an event missed.
Two replicas can admit runs of one tenant at the same time. Admission therefore takes a transaction-scoped advisory lock on the tenant before it counts. The lock serialises admission inside one tenant and never across tenants.
Figure 3: Admission and promotion: a tenant that fires 500 jobs at one minute runs 20 at a time, and a tenant that fires three runs all three at once.
Source: report_admission_interaction.puml. Regenerate with
plantuml -tpng doc/knowledge/architecture/report_admission_interaction.puml.
Figure 4: A report instance: the state it is born in, promotion from queued, and the terminal states. The states are those of report_instance_lifecycle; promotion is the edge from queued to pending.
Source: report_instance_admission_states.puml. Regenerate with
plantuml -tpng doc/knowledge/architecture/report_instance_admission_states.puml.
The trigger is idempotent on the firing id. A repeated trigger finds the instance for that firing and returns it.
2.5.5. Start the run
Reporting creates the instance and publishes workflow.v1.ops.start_workflow. The workflow
instance id is created by reporting and recorded on the instance. A repeated
start therefore opens the same run. The instance records the grant id it was
admitted under, and the workflow request carries that same id, so a step service
exchanges it for a run token without reading reporting. The request carries:
| Field | Why the run needs it |
|---|---|
| the report instance id | the run id; the run_id claim of the token |
| the tenant and the party | the step request; the check at exchange, and the scope of the step's own reads and writes |
| the grant id | the exchange |
| the as-of instant | the instant of the firing; every bitemporal read of the run uses it |
| the run configuration | the chain the engine builds |
| the definition id | the run document to read |
The as-of instant fixes the shape of the run. The definition page requires that an edit during a run does not change that run. The steps read every bitemporal document as of this instant, so an edit after the firing does not reach the run. The start message therefore carries a reference and an instant, and not a copy of the documents.
The chain for a run that executes both ends has four steps:
| # | Step | Subject | Service | Deadline |
|---|---|---|---|---|
| 1 | prepare_ore_package |
ore.v1.ops.prepare_ore_package |
ores.ore | data_step_timeout |
| 2 | submit_compute |
compute.v1.ops.submit_compute |
ores.compute | the grid budget |
| 3 | collect_compute_results |
reporting.v1.ops.collect_compute_results |
ores.reporting | data_step_timeout |
| 4 | finalise |
reporting.v1.ops.finalise_report |
ores.reporting | write_step_timeout |
Pre-processing is one step, as the pre-processing page requires. The
substituted chains replace step 1 or step 3 as
Report execution substitution
describes. Every step compensates with reporting.v1.ops.fail_report.
2.5.6. Prepare the input
ores.ore owns this step. It owns the mapping to the engine's documents, so
the archive has one author.
- Identity. It obtains the run token.
- The run document. It calls
reporting.v1.run_documents.getfor the definition, as of the instant. The answer names the configuration documents the run binds. - The configuration. It calls the owners' gets in parallel:
refdata.v1.curve_configuration_documents.get,refdata.v1.conventions_documents.get,analytics.v1.pricing_engines_documents.getandanalytics.v1.todays_market_documents.get, each with the party and the instant. - The digest. It computes the digest of the tenant, the party, the version of each document, the scope, the as-of instant, the market data instant and the engine version. The tenant and the party are in the digest, so two tenants never share an archive.
- The lookup. It asks storage for
ore/inputs/<digest>.tar.gz. A complete object ends the step: the run reuses it. - The data. It calls the trading scope read and the market data resolution, as Report pre-processing specifies. Each owner writes its batch to storage under the run's key with the run token and answers with the key. Bulk data does not travel in a message.
- Map and package. It streams the batches through the mappers into the archive and writes it under the digest key. A write goes to a temporary key and is then renamed, so a reader never sees a partial archive.
- Answer. It completes the step with the key, the digest, and whether it generated the archive or found it.
The step converges on a repeat. A repeat finds the archive under the digest, or it generates it again under the same key.
2.5.7. Submit to the grid
ores.compute owns this step. It obtains the run token and creates the batch
in the tenant, with the report instance id as the batch's external reference.
A repeat finds the batch by that reference and adopts it, so a repeat never
runs the engine twice. The workunits name the input key, the package of the
tenant's engine version, and the output key.
The step does not complete at submission. Compute records a link between the batch and the step. When the batch closes, compute completes the step with the batch id and the output keys. The step's deadline is the grid budget, which covers the queue wait and the engine time.
Compute dispatches with its own identity. Dispatch is compute's own work on its own table, and the fair share below decides its order.
2.5.8. Execute
A worker node takes an assignment from the queue of its platform. The worker
holds no run token. An engine run can last longer than any token, and a node
serves many tenants. The node uses its own identity. That identity reads only
compute/packages/ and ore/inputs/ and writes only compute/output/. Each
assignment names the exact keys. The node runs the engine and writes the output
archive, then submits the result on compute.v1.ops.submit_result.
draws this path beside a step service that keeps exchanging.
Compute job lifecycle describes the
node's work.
2.5.9. Collect the results
Reporting obtains the run token and reads the output archive from storage. It
uses the parser that ores.ore supplies as a library, and it bulk loads the
results into the result store as
Report post-processing describes.
Each row carries the instance id, the tenant and the party. A repeat replaces
the rows of the instance and does not add to them.
2.5.10. Finalise
Reporting moves the instance to completed and records the output key, the
row counts and which phases were substituted. It then promotes the next queued
run of the tenant. The engine publishes the instance status event. Every step
service drops the run's cache entries when it receives that event.
2.6. Thousands of runs at once
2.6.1. Design targets
| Quantity | Target |
|---|---|
| Tenants | 1,000 |
| Scheduled definitions | 20,000 |
| Runs that start in the busiest minute | 2,000 |
| Runs in flight | 1,000 |
| Default running runs per tenant | 20 |
| Default running runs per party | 5 |
These numbers are targets for capacity tests. They are not measurements.
2.6.2. Where the load lands, and what absorbs it
| Service | Load at the busiest minute | What absorbs it |
|---|---|---|
| ores.scheduler | 2,000 firings | the loop does not wait for an action; each firing is one row and one request |
| ores.reporting, trigger | 2,000 triggers | each trigger is one instance row and one publish; replicas share a queue group |
| ores.reporting, admission | 2,000 decisions | a count of running instances per tenant, from an index on tenant and state |
| ores.workflow | 2,000 starts; the steps in flight | replicas share a durable consumer; the deadline pass reads only dispatched steps |
| ores.iam | about 2,000 exchanges | the grant cache; one signature per exchange |
| the owners | parallel reads per run | bulk reads by scope; queue groups; replicas |
| ores.ore | the packaging of new digests | a worker pool per replica; the digest cache |
| storage | archives in and out | streaming; the backend's throughput |
| ores.compute | the workunits of 1,000 runs | the fair-share dispatcher; the node pool |
| ores.reporting, collect | result files | bulk load |
A busy minute costs little at the front, because the trigger and the start do no heavy work. The heavy work waits in the step queues and in the grid, and those queues are fair.
2.6.3. Three fair queues
- Admission, in reporting. The quotas cap the running runs of each tenant and each party. Promotion is first in, first out inside a tenant. A tenant that schedules 500 reports at the same minute runs 20 at a time. The other tenants are not blocked behind it.
- Step concurrency, in each step service. Each replica has a fixed pool of workers for heavy steps. It also limits the steps of one tenant that run at the same time on the replica. A step over the tenant limit waits in a local queue, and the replica serves its local queues round robin across tenants. The wait counts against the step's deadline, so the limits must keep the wait well inside the budget.
- Dispatch, in compute. Compute holds the workunits and releases them to the platform queue by weighted fair share across tenants. A tenant has a weight and a cap on its assignments in flight. A tenant with dedicated nodes has a pool of its own, and its work does not enter the shared pool.
The engine time of a run is the largest cost, so the grid's fair share decides
most of the waiting. Admission keeps the grid queue short, so a run waits in
queued, where a person can see it, and not in a hidden queue.
2.6.4. Isolation between services
Each service has its own replicas, pools and limits. A slow owner delays only the step that calls it. The step's deadline bounds that delay, and the run fails with a message that names the service, as Failure, compensation and retry describes. A slow tenant cannot take all the workers of a replica, because of the tenant limit in the step pool.
2.6.5. Caches
| Cache | Where | Key | Removes |
|---|---|---|---|
| The input archive | storage | the digest | packaging for a repeated run of the same inputs |
| The run token | each step service, in memory | grant id and run id | an exchange per request |
| Grants and permissions | IAM, in memory | grant id; account and party | a database read per exchange |
| The engine package | each worker node, on disk | the application version | a download per run |
None of these caches holds data across tenants. The input digest includes the tenant and the party, and the token cache key includes the grant.
2.7. Failure and recovery
The engine's rules apply unchanged. The table lists the failures that this design adds or makes specific.
| Failure | Detected by | Outcome |
|---|---|---|
| The grant lapsed or was revoked | the exchange answers forbidden, grant_lapsed |
the step fails; the run fails; reporting suspends the definition and tells the party |
| IAM does not answer | the exchange times out | the step retries the exchange with back-off inside its budget, then fails |
| The token expires during a call | the owner answers token_expired |
one exchange and one retry |
An owner refuses with forbidden |
the owner's permission check | the step fails; the error names the permission; this is a seeding defect |
| An owner does not answer | the step's deadline | the run stops and names the service |
| The grid is busy | nothing fails; the run waits | the grid budget bounds the wait |
| A worker dies | JetStream redelivers the assignment | another node runs it |
| A step service restarts | the engine re-dispatches the step | the step converges; the cache refills |
| A quota is reached | admission | the run is born queued |
A failed run compensates through reporting.v1.ops.fail_report. The archive, the
batch and the output stay in place. A person reads the failure on the instance
and triggers another run, which reuses the archive by its digest.
2.8. Observability
Every message of a run carries one correlation id: the id the scheduler's firing created, or the person's action for a manual run. The run stores it, and every step carries it. Every token carries the run id. A log line of any service in the run can therefore be joined to the instance.
| Measure | Labels | What it tells |
|---|---|---|
| Instances by state | tenant | the queue a tenant waits in |
Time in queued |
tenant | whether the quotas are right |
| Step duration and step wait | step, tenant | where a run spends its time |
| Exchanges, cache hits and exchange latency | service | whether the token cache works |
| Refused exchanges | reason | lapsed grants; a misconfigured audience |
| Assignments in flight and queue depth | tenant, platform | whether the grid is fair and big enough |
Report processing data characteristics lists the measures of the phases themselves. The IAM issue log is the audit record of every token. It records the grant, the run, the service, the time and the computed permissions.
2.9. Decisions this design takes
- A run uses Scheduled Work Acting as a Tenant. Reporting creates a run grant when a person schedules, and every step exchanges it for a run token. The mechanism, its decisions and its trust boundaries are on Run grants and run tokens.
- A run reads its documents as of its firing. The start message carries an instant and not a copy.
- Pre-processing is one step in
ores.ore. The reporting gather and assemble steps leave the chain. The pre-processing page leaves the owner of packaging open, and this design places it inores.ore, which owns the mapping. - Quotas queue a run and never fail it. The existing
queuedstate carries the wait. - The grid dispatches by fair share. Tenants share platform pools, with a weight and a cap each.
- Worker nodes hold no run token. The node identity reaches only the keys an assignment names.
2.10. What this design does not settle
- The owner of the result store. Report post-processing holds the question.
- The tenancy of storage, a bucket per tenant or keys per tenant. Object Storage holds the question. The node identity's key rules depend on the answer.
- The values of the quotas and weights. Capacity tests against the targets above decide them.
- How a party is told that a grant lapsed. The notification channel belongs to the user journeys.
3. Patterns used
- Scheduled Work Acting as a Tenant — every scheduled run acts as its tenant and party through it.
- Grant Management — reporting creates a run grant when a person schedules a definition, and revokes it when the definition is unscheduled or deleted.
- Correlation Identifier — every message of a run carries the id the firing created.
- Bulkhead — per-tenant step limits in each replica, and dedicated compute pools.
- Queue-Based Load Leveling — a run
over its quota is born
queuedand promoted as runs finish. - Throttling — quotas cap the running runs of each tenant and party.
- Fair Share — admission, step concurrency and grid dispatch each share capacity across tenants.
4. See also
- Report Processing — the structure note that orders this cluster, and where to read this page in it.
- Identity and Access — the structure note for the run grant and the token exchange.
- Internal actor impersonation — the pattern the exchange serves over the bus.
- Service-to-service auth — the two flows that do not cover a scheduled run.
- Scheduler — the job and the firing.
- Workflows — the engine that runs the four steps.
- Report execution — the workflow page for the chain.
- RFC 8693, OAuth 2.0 Token Exchange — the exchange and the actor claim.