Report execution at scale

Table of Contents

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:

  1. 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 ReportRun role. The grant is not a credential.
  2. 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 ReportRun permissions. 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.
  3. 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

  1. 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.
  2. 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.
  3. 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.
  4. Every step converges on a repeat. The engine delivers a step at least once. Idempotency in a workflow step states the contract.
  5. Every queue is fair across tenants. No queue serves work in plain arrival order across tenants.
  6. 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 to suspended. 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

report_execution_at_scale_sequence.png

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:

  1. It creates the grant through IAM, on behalf of the person.
  2. 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.
  3. 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.

report_definition_schedule_states.png

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:

  1. 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.
  2. 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.

report_admission_interaction.png

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.

report_instance_admission_states.png

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.

  1. Identity. It obtains the run token.
  2. The run document. It calls reporting.v1.run_documents.get for the definition, as of the instant. The answer names the configuration documents the run binds.
  3. 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.get and analytics.v1.todays_market_documents.get, each with the party and the instant.
  4. 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.
  5. The lookup. It asks storage for ore/inputs/<digest>.tar.gz. A complete object ends the step: the run reuses it.
  6. 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.
  7. 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.
  8. 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. The long run token
diagram 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 in ores.ore, which owns the mapping.
  • Quotas queue a run and never fail it. The existing queued state 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 queued and 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

Emacs 29.3 (Org mode 9.6.15)