Forum teuk.org

🧙 Mediabot v3 — Trivia Enters the Shared AsyncWorker Passage

in Mediabot · started by TeuK · 6d ago

TeuK · 6d ago

Commit: f5128cb
Version: 3.4dev-20260817_100647

MB653 completes the second real migration onto Mediabot::AsyncWorker, and this time the target is one of Mediabot’s more demanding asynchronous consumers: Trivia.

The goal was deliberately narrow:

remove Trivia’s private subprocess lifecycle without changing Trivia’s behaviour.

No database change. No service change. No systemd change. No big-bang migration of the remaining consumers.


🧱 What changed

Before MB653, Trivia still owned its own copy of the usual asynchronous machinery:

pipe
fork
watch_process
IO::Async::Stream
timeout timers
TERM/KILL escalation
EOF handling
callback finalisation

That plumbing is now delegated to:

Mediabot::AsyncWorker

Mediabot::UserCommands keeps only the policy that actually belongs to Trivia:

HTTP request/retry behaviour
Open Trivia DB parsing
rate-limit handling
stage diagnostics
hard request deadlines
Trivia-specific error vocabulary
question normalisation

That is the architectural split we wanted:

AsyncWorker = subprocess lifecycle
Trivia      = Trivia semantics

🛰️ Trivia needed more than a final result

The Version checker migrated in MB652 only needed a final child result.

Trivia is more demanding.

While its worker is running, it emits useful diagnostic stages such as:

http_client_start
http_client_ready
http_get_start
http_get_done
api_parse_start
api_parse_ok
rate_limit_wait

Those stages matter because when a worker times out, Mediabot must still be able to say where it got stuck.

So MB653 extends the shared worker protocol with bounded progress records:

child
  │
  ├── progress
  ├── progress
  ├── progress
  │
  └── final result

The records are transported through the same controlled child-to-parent channel rather than creating another Trivia-specific side channel.


🧭 Preserving last_stage

One of the main regression risks was losing Trivia’s existing diagnostics.

For example, a timeout after the HTTP request has started must still produce meaningful information:

error      = worker_timeout
stage      = async_timeout
last_stage = http_get_start

The shared worker therefore transports progress independently from the final result.

That allows the parent to retain the last confirmed stage even when the child is terminated before it can return normally.


🔒 Lifecycle remains bounded

Trivia keeps its existing operational protections:

bounded worker output
hard timeout
TERM escalation
KILL fallback
callback-once guarantee
HTTP request deadline
retry/rate-limit behaviour

The migration does not loosen those constraints.

Instead, responsibility for the generic process lifecycle is now concentrated in one tested implementation.


🧪 Focused regression

The focused Trivia + AsyncWorker regression suite covered:

Trivia guard behaviour
async Trivia fetch
answer boundaries
rate-limit retry
process-watch diagnostics
stage protocol deadlines
hint structure
accent boundaries
Version worker regression
shared AsyncWorker contract
MB652 Version migration
MB653 Trivia migration
module structure sanity

Result:

PASSED : 553/553

This was important because extending the AsyncWorker protocol for progress events also had to prove that the already-migrated Version checker remained intact.


🔥 Real IO::Async smoke

The adapter was exercised with the real IO::Async event loop without contacting Open Trivia DB.

Success path

SUCCESS:
  last_stage=api_parse_ok
  bytes=315
  callback_once=1

Forced timeout path

The child deliberately ignored TERM, forcing the complete escalation path:

TIMEOUT:
  error=worker_timeout
  stage=async_timeout
  last_stage=http_get_start
  signal=9
  callback_once=1

Result:

REAL_TRIVIA_ASYNCWORKER_SMOKE=OK

This confirms that the shared worker preserves both final results and in-flight diagnostic state.


🌍 Real Open Trivia DB validation

The migrated adapter was then executed against the real Open Trivia DB service.

Observed progression:

http_client_start
http_client_ready
http_get_start
http_get_done
api_parse_start
api_parse_ok

The remote call completed successfully:

OK         = 1
ERROR      = <none>
LAST_STAGE = api_parse_ok
ATTEMPTS   = 1
STATUS     = 200
CALLBACKS  = 1
REAL_TRIVIA_CHECK=OK
RC=0

The HTTP request completed in roughly 0.6 seconds and the final worker exited normally:

exit=0
signal=0
forced=0

So this was not merely a mocked migration: the new adapter successfully crossed the full real network path.


🧪 Full-suite validation

The entire Mediabot test suite passed after the migration:

PASSED : 13380/13380
RC=0

Runtime for this pass:

864s

That runtime is encouraging, but one run is not enough to claim a performance improvement. MB653’s acceptance criterion is correctness and architectural consolidation, not benchmark theatre.


🧹 Less duplicated async machinery

The resulting change is significant internally:

800 insertions
555 deletions

A large part of the removed code was duplicated lifecycle machinery from Mediabot::UserCommands.

The added code is primarily:

shared progress support
adapter logic
regression coverage
migration tests
documentation

So although MB653 is a substantial diff, its architectural direction is toward less duplicated behaviour, not more.


🚧 Consumers deliberately left alone

MB653 still does not migrate:

Mediabot/Achievements.pm
Mediabot/CommandAsync.pm
Mediabot/External/YouTube.pm

The migration remains incremental.

Version and Trivia have now proven two different AsyncWorker usage patterns:

Version -> simple final-result worker
Trivia  -> progress-aware worker

That gives the shared abstraction much stronger evidence than migrating several consumers at once and hoping for the best.


🪄 The small debugging detour

The first real-network probe failed instantly with:

error=http_setup
last_stage=http_client_failed

The failure was not in MB653.

The standalone validation process had loaded Mediabot::UserCommands but not Mediabot::External, which provides the normal HTTP client used by Trivia.

Once the real runtime dependency was loaded, the exact same migrated Trivia code immediately completed:

HTTP 200
api_parse_ok
callback_once=1

That was a useful reminder of the reason for doing real-environment validation after unit tests: sometimes the test is wrong, and the diagnostics need to be good enough to tell us that quickly.


🏰 Where the architecture stands

The shared worker migration now has two proven consumers:

MB651  Shared AsyncWorker contract
MB652  Version checker migration
MB653  Trivia migration

The abstraction has now demonstrated:

normal child completion
bounded JSON results
progress messages
timeouts
TERM/KILL escalation
process-watch integration
liveness handling
callback-once semantics
real network consumers

That is a much stronger base than the duplicated worker implementations Mediabot had before.


✨ Commit

f5128cb
🧙 Send Trivia Through the Shared AsyncWorker Passage

Version:

3.4dev-20260817_100647

Committed scope:

CHANGELOG.md
VERSION
Mediabot/AsyncWorker.pm
Mediabot/UserCommands.pm
t/cases/541_mb319_trivia_async_fetch.t
t/cases/613_mb395_trivia_process_watch_diagnostics.t
t/cases/614_mb396_trivia_stage_protocol_deadline.t
t/cases/833_mb651_asyncworker_contract.t
t/cases/835_mb653_trivia_asyncworker_migration.t

Two consumers have now crossed the passage.

The duplicated async labyrinth is getting smaller.

You must be logged in to reply.