Skip to content

rollup_minute() and rollup_hour() return per-database row counts, not processed grains #191

Description

@samo-agent

Summary

On current main (85402194873ccc607630e68b225e142649f2cd3b), both public rollup functions violate their documented return-value contracts when one time grain contains more than one database:

  • ash.rollup_minute(integer) is documented as “Returns minutes processed.”
  • ash.rollup_hour() is documented as “Returns hours processed.”

Each instead returns the number of (time grain, datid) rows upserted. One completed minute/hour with two databases returns 2, although exactly one minute/hour was processed.

Found while filling the systematic behavioral release-gate coverage requested by #160.

Exact reproduction

Run this after a fresh install in a database other than postgres so the fixture has two distinct database OIDs:

truncate table ash.sample;
truncate table ash.rollup_1m, ash.rollup_1h;
truncate table ash.wait_event_map restart identity;

insert into ash.wait_event_map (state, type, event)
values ('active', 'CPU*', 'CPU*');

update ash.config
set current_slot = 0,
    sample_interval = interval '1 second',
    last_rollup_1m_ts = ash.ts_from_timestamptz(
      date_trunc('hour', statement_timestamp()) - interval '1 hour'
    ),
    last_rollup_1h_ts = null
where singleton;

with anchor as (
  select ash.ts_from_timestamptz(
    date_trunc('hour', statement_timestamp()) - interval '1 hour'
  ) as sample_ts
), database_ids as (
  select oid as datid
  from pg_database
  where datname in (current_database(), 'postgres')
), wait_id as (
  select id
  from ash.wait_event_map
  where state = 'active' and type = 'CPU*' and event = 'CPU*'
)
insert into ash.sample (sample_ts, datid, active_count, data, slot)
select anchor.sample_ts,
       database_ids.datid,
       1,
       array[-wait_id.id::int, 1, 0]::integer[],
       0
from anchor
cross join database_ids
cross join wait_id;

select count(distinct datid) as distinct_datids_in_one_minute
from ash.sample;

select 1 as expected_documented_minutes_processed,
       ash.rollup_minute(1) as actual_rollup_minute_return;
select count(*) as minute_rows_written from ash.rollup_1m;

update ash.config
set last_rollup_1m_ts = ash.ts_from_timestamptz(
      date_trunc('hour', statement_timestamp())
    ),
    last_rollup_1h_ts = ash.ts_from_timestamptz(
      date_trunc('hour', statement_timestamp()) - interval '1 hour'
    )
where singleton;

select 1 as expected_documented_hours_processed,
       ash.rollup_hour() as actual_rollup_hour_return;
select count(*) as hour_rows_written from ash.rollup_1h;

Captured on PostgreSQL 17.9:

distinct_datids_in_one_minute = 2
expected_documented_minutes_processed = 1
actual_rollup_minute_return = 2
minute_rows_written = 2

expected_documented_hours_processed = 1
actual_rollup_hour_return = 2
hour_rows_written = 2

The minute watermark advances by exactly 60 seconds and the hour watermark by exactly 3600 seconds, confirming that one time grain was processed in each call.

Root cause

Both functions accumulate the DML row count:

get diagnostics v_count = row_count;
v_total := v_total + v_count;

Because the rollup keys include datid, ROW_COUNT measures (grain, datid) rows rather than processed grains.

Expected behavior

Either:

  1. return the documented number of distinct minute/hour grains processed, independent of database count; or
  2. deliberately redefine and document the return value as rollup rows written, with callers and tests updated accordingly.

The first option matches the current catalog comments and the batch-limit meaning of rollup_minute(batch_minutes).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions