Task: Name a slow statement in the service log

Table of Contents

This page documents a task in the Run a report end to end on a synthetic bond population story. It captures the goal, current status, acceptance, and any notes or results.

1. Goal

A read that takes 250 ms or more names itself and its duration in the service log, with where the time went.

2. Status

Field Value
State DONE
Parent story Run a report end to end on a synthetic bond population
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-10-23

3. Acceptance

  • A read of 250 ms or more logs a warning with its description and duration.
  • The executor that opens its own connection logs the connect, session and statement parts.
  • A read below the threshold logs at debug level only.

4. Plan

The task before this one removed the slow export and inferred its cause from the plan of one statement. This task measures the cause. The two executors that run a read, execute_read_query and execute_parameterized_multi_column_query, time each read. A read of 250 ms or more logs a warning with its description and duration. The executor that opens its own connection also splits the time into the connection, the session settings and the statement. A read below the threshold logs at debug level.

5. Measurements

Every number this task claims about a tree, with the command that produced it and the commit it was measured at. A number without both is not a measurement, because a clean-up record describes a tree and trunk moves under a branch: a consumer count, a gate's component count and a suite's size all change with no code changing, and the reader of a bare number cannot tell which tree it came from. Where the raw output matters, put it in an evidence/ file beside the task and name that file in the Command column.

What Value Command Commit
Chunks of the observation table 163, of which ACME's rows sit in 13 compass db sql on timescaledb_information.chunks 2a0b529f5a
Share of observation rows from other tenants 483,790 of 485,767 (99.6%), 22 tenants, 2016-01 to 2035-03 compass db sql, group by tenant_id = ACME 2a0b529f5a
One-row read, as the log now splits it 13 to 16 ms connect, 2 ms session, 32 to 73 ms statement ORES_TEST_LOG_ENABLED=true ORES_TEST_LOG_LEVEL=debug ores.marketdata.core.tests read_as_of_all_gives_the_manual_point_the_read_for_one_series_gives this branch
Statements of the old export about 918 (3 for each of 306 series) the export loop before the previous task 2a0b529f5a
Time of the old export, estimated from the two rows above about 42 s 918 x (14 + 2 + about 30 ms)  
Time of the old export, observed 38 s the service log of instance 18fe9858 2a0b529f5a

6. Notes

The cause of the 38 seconds is now measured. A statement of the old export took 32 to 73 ms in the database even for one row, because the planner works through every chunk of the observation table, and the table has 163 of them. The executor also opened a connection for each statement, 13 to 16 ms. A statement for each series, three per series, over 306 series, is about 42 seconds. The export took 38.

The chunk count is not ACME's. ACME's rows sit in 13 monthly chunks. 22 other tenants hold 99.6% of the rows, and their dates run from 2016 to 2035. They are the rows the C++ test suites write into this database, with random dates. So the same code on a database without that residue plans over 13 chunks, and the old export would likely have passed. This is an inference and has not been run. The code still did a statement for each series, so the test residue exposed a defect that was already there.

Seen on the way and handed on:

  • The usage recorder, the feed ingest loop and the usage planner write modified_by as the text ores.marketdata.service. After bootstrap the database accepts only the name of an account. The service account is ores_swift_curie_marketdata_service, which the service already holds as db_opts.user. So the recorder fails every flush, with 19,910 delivery counts waiting.
  • gather_trades and gather_market_data have no consumes, so they run side by side. When one fails, the other can already have started, and it logs "cannot move from failed to running". It is by design and harmless, but the log says ERROR.
  • The database schema fingerprint differs from the checkout, so the database needs a recreate before the soak.

7. Test Scenarios

Manual QA scenarios (scaffolded via compass add test_scenario) that verify this task. Link new ones here as they're created; the scenario doc itself links back via its "Verifies task" field.

Scenario State Notes
     

8. PRs

PR Title
3692 Name a slow statement in the service log

9. Review

# Comment summary File Decision Notes
1 Use a std::chrono::milliseconds constant for the threshold. bitemporal_operations.hpp Skipped The header is the base of almost every component, so a change to it rebuilds the product. The constant is documented in milliseconds and a long long matches the clock count.
2 The statement time includes the copy of the rows. bitemporal_operations.cpp Fixed The comment says so.
3 A slow failed statement is not logged with its timing. bitemporal_operations.cpp Fixed The failure log of the executor that opens its own connection states how long the statement ran before it failed. The sqlgen executor throws through ensure_success and is not changed.
4 The 250 ms threshold is hard-coded. bitemporal_operations.hpp Skipped A constant is fine until it proves noisy.
5 A scoped timer helper if a third place needs one. bitemporal_operations.cpp No change needed Two places time a read, and they report different parts.

10. Result

A read of 250 ms or more now names itself and its duration in the service log. The executor that opens its own connection says how the time split. A run of the NPV report on the instrumented services completed in 12 s with the export at 214 ms, and no read crossed the threshold. The timing line was seen at debug level in a test, so the warning level is by the same code path but has not been seen to fire.

Emacs 29.3 (Org mode 9.6.15)