Skip to content

server2: start providers when a restart restore is late - #2900

Draft
hwanjjang wants to merge 18 commits into
feat/2614-kma-restart-observefrom
feat/2614-kma-restart-race
Draft

hwanjjang wants to merge 18 commits into
feat/2614-kma-restart-observefrom
feat/2614-kma-restart-race

Conversation

@hwanjjang

Copy link
Copy Markdown
Collaborator

Summary

Plan A for the first answer after a restart, which AK chose on 2026-11-07 ("권고대로"). The EC2 restart comparison found every first answer without yesterday values, past days and the history mapping; most also lacked current observations, and some lacked UV or the forecast narrative. The restores ran behind each other and behind CPU work. AK ruled that a part S3 holds but a restarted process has not restored yet is not an allowed omission.

  • Head start (src/resolver/race.rs): a source waits for its stored restore at most 700 ms into the KMA request. It then starts the owned acquisitions for what memory lacks, as on an empty S3, and the answer uses whatever is ready by the request bound. A restore that finishes in time makes no extra provider call.
  • Station pages: each acquisition is folded as it ends. After a restart only the hours of today and yesterday that neither S3 nor memory holds are acquired; D-2 to D-7 come only from S3 (the server2: restore station observations from day products after a restart #2872 day products). A newer page is never replaced.
  • ASOS history: the station comes from the default station list before any fold. The load starts with the request and the run starts at the head start.
  • Life indices: today's slots are restored before the rest of the window, because UV's provider needs 5 to 7 s.
  • Narrative: the read starts with the request as owned work. A waiter's timeout no longer sets the error gate. The race and the three-minute refresh share one fetch per office, and the newest announcement wins.
  • Warnings and lanes: the request reads its zone state on the request lane, alongside the warning cycle. Reads no request waits for use the bulk lane: the rest of the restart read, expansion, products, image production and the KASI pass.

Stacked on #2872 (which stacks on #2866). Grid forecasts are not raced; they are already read per request and were complete in every restart.

Checks

  • Every outcome has a test that failed before its change (Red logs recorded).
  • The full suite has 1,260 passed and 0 failed on 17e16ae, with fmt and clippy clean. After merging the server2: restore station observations from day products after a restart #2872 test fixes, the domestic suite has 272 passed.
  • The placement gate (KMA-RESTART-RACE.json) and the artifact policy pass.
  • Documents: server2.md (including data.go.kr units per quota owner) and server2-kma-data-design.md. The data design diagram was regenerated through Archify, and its note was checked in a browser.

Provider calls (only when a restore misses the head start)

  • data.go.kr, per raced station: 1 AsosHourly and 1 AsosDaily.
  • data.go.kr, per raced restart: 4 LivingWthrIdx (+1 per no-data slot), 3 HealthWthrIdx out of season, and 1 VilageFcstMsg per office.
  • Keyless KMA web pages: absent hours of today and yesterday only.
  • Grids, RSS, mid and AirKorea: none.

Not in this PR

🤖 Generated with Claude Code

hwanjjang and others added 11 commits November 7, 2026 20:26
KMA-RESTART-RACE (AK 2026-11-07, plan A). resolver::race holds the
700 ms head start: a restore not done by then is reported (one
operation.action, restore/timeout/fallback) and left running for the
caller, who starts the providers of what memory lacks beside it. A head
start no earlier than the end of the wait is none, so a source without
one keeps its waits.

IndexListing gains a bulk view for reads and writes no request waits
for: the bulk lane of a shared BoundedS3 admission, otherwise the same
transport. CatalogStore exposes its limits so a second store on that
view uses the same ones. The task declaration lists every path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A stored read of a forecast office now runs as owned work with its own
30 s deadline. A waiter that stops at its deadline neither cancels the
read nor sets the one-second error gate; the next request joins the
running read or answers from its outcome (KMA-RESTART-RACE, decision 6).
Only a completed fault of the owned read sets the gate.

A reader may prefetch an office for an answer that attaches the
narrative later: the service starts the read and, when it has no answer
at the given head start (still running or failed), fetches and
publishes that office once, single flight and at most once per office
and ten minutes. A newer published narrative stays the office's answer
when an older stored read ends after it. A read that ended with no
stored narrative fetches nothing.

The cache test that expected the gate after a waiter's timeout now
expects the next request to join the read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The observed source computes the 700 ms head start beside its wait; the
legacy source has none. A restore not done by then is raced by the owned
acquisitions of what memory lacks, as on an empty S3 (AK 2026-11-07,
plan A), and a restore done by then leaves them to what S3 lacks.

Station pages: the load is waited for only until the head start. A late
load is raced by the minute page, the latest hourly and city page, the
previous hourly page while a fetch now is within its ongoing reach, and
a fill of the hours of today and yesterday the day catalogs do not list
(each once per six hours; older days come from S3 alone). Whatever
arrives first is folded while the rest is waited for. Provider pages
are held newest-wins, so a late restore never replaces them, and a
product page that loses stays accounted for S16.

ASOS history starts with the request, its station from the current fold
or the default station list; the recovery run starts at the head start,
a run before the read creates the station entry, and the read loads both
kinds together.

Life: a process's first read restores today's partitions before the
rest of the window; flights are decided at the head start; a request
uses an acquisition's published records at once over the stored fold
(LifeOutcome::Published carries them), within its cut.

The backend prefetches the request's narrative office after prepare.
The request reads the warning zone state at its start through a
request-lane view of the process warning store and takes the first
state, the stored one or the one the cycle offers.

The rest of the restart read, full reads of reduced pages, day product
writes, the scheduled image production and the KASI capture pass use
the bulk lane.

Tests cover each family's race, the fast path, the fill, the default
station list, the needed-first life read, the narrative read beside the
snapshot, the zone-state read beside a cycle that never ends, the lanes
(also with a full bulk lane) and the whole first answer. The test
transport holds reads of stored keys for a while, tags the bulk lane
and can hold it full. A cycle that never ends no longer holds the
answer until the wait ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The architecture document gains the restart race section: the 700 ms
head start, what each family's restore is and what races it, the fill
of today's and yesterday's absent hours, the lanes, the provider calls
a raced restart adds and the failure paths. The restart read, the
warning offer, the shared warning store, the narrative supplement and
cache contract, and the S15b-2 acquisition table point to it. The KMA
data design describes restores with a head start. The task intent,
plan and spec (with the test of each criterion) are added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The restart note now says that stored folds restore first and that
providers fill what is still missing at 700 ms, with the other reads on
the bulk lane. Regenerated through Archify (validate, deliver, check and
browser-check pass; the note was inspected in a browser).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The restart-race tests slowed stored reads until 2.6 s after their setup
while the request waits 2.5 s from its own start: 100 ms of real time
decided whether the restore ended after the answer. With 200 ms of setup
before the request, life_s3::the_life_read_restores_todays_slots_first
failed (the older days were in the first answer).

Counted::hold now returns a gate: the reads of stored keys under its
prefixes wait until the test opens it, which it does right after the
answer the restore must follow (inside the 3 s S06 lookup deadline), and
the test checks that a read was held. The nine tests use it; with a 1 s
setup delay the life test still passes. The station gates hold the
station pages, products and seed only: the town RSS is read by every
request and is not raced. The 700 ms head start stays checked on a
paused clock (tests/resolver/race.rs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stations_by joined the minute, hourly and city acquisitions as one part,
so pages that arrived early waited, stored but unfolded, for the slowest
provider. With the restore later than the bound and the city provider
slow, that part ended at the bound, the last fold had no time left and
the first answer used the old fold or the seed alone: the new
observe_product::pages_that_arrive_before_a_slow_provider_reach_the_first_answer
failed 3/3 with "Fail to find stn Weather info of stnId=108".

Each owned acquisition is now a part of its own, so the fold runs when
any of them ends while the others are waited for. The test passes 5/5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tive-regression)

The office race installed its published narrative unconditionally, so an
acquisition that ended after the stored read had answered with a newer
publication replaced it (publish_once is per publication, so both stay
stored). The new
full_response::a_late_raced_acquisition_keeps_the_newer_stored_narrative
failed 3/3: the hour-old "acquired" narrative answered over the current
"stored" one.

Both the stored read and the race now install through
OfficeCache::install, which keeps an unexpired answer with a later
announcement, as the read already did. The test passes 5/5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ight)

The request race limited only races (cache.raced) and the three-minute
refresh only its cycle, so both could fetch and publish the same office
at once. The new full_response::the_race_joins_the_refresh_fetch_of_the_office
holds the refresh's fetch of office 109 and starts a request's race: the
race fetched the office again (3/3).

One OwnedFlights fetch-and-publish per office now serves both; the one
that comes second joins the running fetch (or its result for 3 s), and
both install the publication with the newest-wins rule. The refresh used
to clear the office's answer instead, so a stored read that started
before its publication could answer the older narrative for 30 s after
it; it now installs what it published. The test passes 10/10, and the
narrative, cache and runtime-services suites pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…budget-accounting)

The restart-race section added logical acquisitions of five APIs into one
"at most 11 units of a key's 10,000" figure. Units are charged per key and
API (each with its own daily allowance), per page and per attempt, and a
page can take a funded second attempt, so 11 was neither the unit count
nor a bound.

The section now lists, per quota owner (AsosHourly, AsosDaily,
LivingWthrIdx, HealthWthrIdx, VilageFcstMsg), the acquisitions a raced
restart adds, their pages, the normal units and the bound with failed
pages, from DataGoKrApi::daily_units, the 999/1,000-row pages and their
8-page limit, FundedExecutor's two attempts per page and the narrative's
key loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hwanjjang

Copy link
Copy Markdown
Collaborator Author

Cross-provider review (Codex gpt-6.1-sol, high).

Round 1 (at 479ceb3, before this PR was opened): CHANGES_REQUESTED.

  • Required: RR2-page-progress, RR2-narrative-regression, RR2-narrative-singleflight.
  • Recommended (both selected): RR2-ci-clock, RR2-budget-accounting.

Round 2 (at 2651e22): CHANGES_REQUESTED. RR2-page-progress, RR2-narrative-singleflight, RR2-ci-clock and RR2-budget-accounting are resolved.

  • Required, still open: RR2-narrative-regression. Once the answer TTL expires, a slow raced fetch of an older announcement can still replace a newer one. A newest-announcement watermark separate from the TTL is being added, test-first.

hwanjjang and others added 2 commits November 7, 2026 23:23
…-regression)

OfficeCache::install compared an incoming narrative only with an
unexpired answer, so the protection ended with the 30 s TTL: a race
started at 0.7 s whose fetch answers an older announcement at 29 s and
whose publication takes 2 s ends at 31 s, after the newer stored answer
(installed at 0.8 s) expired, and installed the older one with a fresh
TTL. The new paused-clock test
cache::an_older_announcement_never_replaces_a_newer_one_after_the_ttl
(in-memory S3, tests/narrative/cache.rs) failed 3/3: "acquired" (older)
answered instead of "stored".

The office now keeps the latest announcement it answered apart from the
answer's TTL. An older narrative is not installed and the answer is left
as it is (an expired one is not extended), so the next request reads S3,
which holds the newer one. The test passes 5/5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hwanjjang

Copy link
Copy Markdown
Collaborator Author

Cross-provider review (Codex gpt-6.1-sol, high), round 3: PASS at ab0aba1. All five findings are resolved: RR2-page-progress, RR2-narrative-regression, RR2-narrative-singleflight, RR2-ci-clock and RR2-budget-accounting. Readiness waits for CI and for the response-first test failure seen on #2872's CI, which is being diagnosed.

hwanjjang and others added 3 commits November 8, 2026 00:48
The first life read listed the partitions of UV, oak, pine and weeds one
family after another before it read today's slots. On EC2 (race-r2,
2026-11-08) those eight LISTs took 0.25-0.68 s before UV's restore began,
and UV missed the first answer in every race run.

The four listings now run together (one per family, the same reads);
today's slots are still read and folded first, and the read stays one
owned flight. The new paused-clock
life_s3::the_needed_first_read_lists_the_life_families_together (an
in-memory S3 whose LISTs take 300 ms) failed 3/3 with the sequential
listing (a UV acquisition started: today's UV was not folded by the
700 ms head start) and passes 5/5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hwanjjang

Copy link
Copy Markdown
Collaborator Author

Cross-provider review (Codex gpt-6.1-sol, high), round 4: PASS at 80bc152. This covers the merges of #2872's writer fixes and 80bc152, which lists the four life families together.

EC2 restart comparison, 2026-11-08 02:38–02:42 KST (t3.small, debug logging, binaries master 5533931 and this PR at 17e6a41). First Seoul answer after a restart vs the same process 5 s later:

master this PR
Missing past days (3 runs) 6 / 6 / 6 0 / 0 / 0
Missing fields (3 runs) 123 / 123 / 124 7 / 31 / 29
First-answer time 3.4–4.0 s 3.1–3.9 s
  • Yesterday's values and the history mapping are now present in the first answer.
  • The current station observation was still missing in 2 of 3 runs.
  • UV was missing in 3 of 3 runs.
  • Diagnosis: CPU saturation of the 2-vCPU host after a restart. Pages arrive by about 1.2 s, but parsing and folds queue on the shared blocking pool. UV's provider needs 7–10 s at night.
  • An info-level rerun is in progress before deciding on a blocking-pool admission.

hwanjjang and others added 2 commits November 8, 2026 05:14
…RST)

The first life read restored each needed catalog one S3 read at a time: on
EC2 after the restarts, today's UV partition (three 4-page acquisitions)
took 16 reads in a row and missed the life section. The needed partitions
of every family (today and the latest before) are now restored together,
with at most four S3 reads in flight shared among them (a third of the
process's S3 admission, so the station restore keeps the rest); single
flight, today first and the rest of the window after are unchanged.

Storage (opt-in per call): CatalogStore::lookup_wide reads a catalog's
group descriptors and records ahead, up to its width at a time, then
restores over them with the same checks and result as lookup. It waits for
one store I/O admission as lookup does and takes the others only if they
are free at once, so no other reader's lookup queues behind it. Only the
life read's needed partitions use it (Reader::acquisitions_wide); every
other reader still uses lookup.

Red: the_needed_first_read_restores_todays_records_together (paused clock,
170 ms per GET) had no UV on the first answer: 16 reads end at 2.72 s, past
the 2.5 s life section. Green: today's UV, at most 4 reads in flight, the
first answer at 850 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a_slow_life_read_starts_uv_at_the_head_start failed now and then (3/30 on
0d98504, 2/30 on 80bc152) with no UV or weeds on the first answer. The
process before the restart keeps its pooled connections until the S3 test
peer closes them 2 s after they opened, so the restarted process's inputs
(8 permits) could fill the peer's 16 connections (sampled at 14-16): the
quota reservation of the raced acquisition was refused at the head start
(connection_reset) and the acquisition failed.

A restarted process's inputs now admit 4 requests at once
(restarted_transport, all seven restarted processes in life_s3). With it
the peer peaked at 12-14 connections and the test passed 30/30.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hwanjjang

Copy link
Copy Markdown
Collaborator Author

Cross-provider review (Codex gpt-6.1-sol, high), round 5: CHANGES_REQUESTED at 7f35848. 0d98504 makes the life read restore today's records with 4 reads in flight; 7f35848 fixes the test server's connection limit.

Required

  • RR2-wide-control-budget: lookup_wide keeps read-ahead descriptors before the retained control byte budget applies.
  • RR2-wide-excluded-read: a group excluded at its first missing page can still hold the whole wide lookup to a timeout through its remaining reads.

Both are being fixed test-first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant