Repository navigation
1.9.79: the pending-click settler takes installs in turns, so installs that cannot settle no longer stop the rest - #309
Merged
Conversation
PendingClickSettler::run() selected the 500 oldest pending_click installs by received_at and left an install whose settle threw where it was, "to be retried next run". Some throws give the same answer every run: a stored token the install-token key no longer verifies (the key was replaced after the install arrived), or a stored body that no longer reads. 500 of those were the whole batch on every run, so no other app's install settled again; installs still waiting for a click that never came held the batch the same way for up to the 24-hour deadline. Measured: three runs of the cron over 501 such installs and one settleable install each failed 500 and settled nothing. - A queue on the install: settle_next_at (0 until tried) and settle_failures (tries in a row that failed), with KEY settle_due (match_state, settle_next_at). A run takes the due installs in that order (never tried first, then the turn that came first) and gives every install it takes its next turn: now for one still waiting for its click, backoff() after a failure (the next run, then 60 s doubling to an hour). The batch is read from the index: 500 rows read, no sort, where ordering by received_at too sorted every pending install (ANALYZE on MariaDB 10.11, 2,000 pending among 60,000). - Only a failure the stored row causes ends an install: a body that is not JSON or no longer an install, or a token the key that loaded no longer verifies, is settled as bad_token through InstallIntake::settle() at the 24 hours a missing click already has, with match_reason saying why. A missing or unreadable key, a database error or a lock is retried with backoff and never ends an install (CLAUDE.md #11). - Visible: settle_failures and settle_next_at on GET /apps/{id}/installs, pending_failing in GET /apps/report, a warning on Analyze > Mobile Apps, a note per install in Setup > Mobile Apps' recent installs, and the job's summary line counts "retired as unreadable". - The 1.9.78 -> 1.9.79 rung reconciles 202_app_installs (the two columns and the index). Tests: PendingClickQueueIntegrationTest seeds the cron's batch plus one of installs that cannot settle ahead of one that can (and the same with installs still waiting for their click); each fails on the old selection. It also holds the retirement at the deadline with its reasons, a missing key and a database error retried and never retired, and the counts in the report and the install list. Every check was planted out and its test failed. AttributedInstallHasConversionTest now lists the settler as a writer of its settle_* columns only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Contributor
Contributor
Brings in 1.9.79's new commits. The conflicts were in 202-config/changelogs.json, 202-config/functions-upgrade.php, changelogs.txt, where both sides added lines: the incoming lines are kept as they are and this branch's follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backlog: BR3T
What was wrong (measured)
PendingClickSettler::run()selected the 500 oldestpending_clickinstalls (ORDER BY received_at, install_row_id LIMIT 500) and left an install whose settle threw where it was, "retried next run". Some throws give the same answer on every run: a stored token the install-token key no longer verifies (the key was replaced after the install arrived), or a stored body that no longer reads. 500 of them were the whole batch every run, so no other app's install settled again. Installs still waiting for a click that never came held the batch the same way, for up to the 24-hour deadline.Measured on a live instance from
origin/1.9.79's settler: 501 such installs and one settleable install (its click recorded after it arrived), three runs of202-cronjobs/app-installs.php. Every run printedpending clicks: 500 examined, settled none, 0 still pending, 500 failedand exited 1. The settleable install stayedpending_clickwith no conversion.What changed
The decision is (c), both. The cursor is per row rather than one global position, and the failure count ends only a failure the row itself causes.
Turns (the cursor).
202_app_installsgainssettle_next_at(0 until tried) andsettle_failures(tries in a row that failed), plusKEY settle_due (match_state, settle_next_at).backoff(): the next run, then 60 s doubling to an hour.integrity_next_at).Retirement (the count), only for failures the code knows are the row's own.
stored()reads the install the way the intake read it. It answers with a reason, never a throw, in exactly three cases, and each is a pure function of the stored row and a key that did load:Such an install is retried until 24 hours after it arrived, the deadline a missing click already has, so a replaced key can be put back in that time. After that it is settled as
bad_tokenthroughInstallIntake::settle(), still the one writer of a match state, withmatch_reasonsaying why. Everything else is retried for as long as it lasts and never ends an install: a missing key, a key that cannot be read, a database error, a lock timeout, or any throw from the classification or the goal engine (CLAUDE.md Fix newline at end of index #11, Fix undefined mysql array keys #34).Visible where installs are read:
settle_failuresandsettle_next_atonGET /apps/{id}/installsand/installs/{uuid}, which the Go CLI prints as the API gives them;pending_failingbesidependinginGET /apps/report(groups and totals);… N failed, M retired as unreadable, with each retirement logged.Cost of the order. The batch is read from
settle_due: 500 rows read and no sort, byANALYZEon MariaDB 10.11 with 2,000 pending installs among 60,000. Ordering byreceived_atas well sorted every pending install (2,000 read, filesort).Schema. The columns and the index are in
AppTables::appInstalls()and in the 1.9.78 → 1.9.79 rung (self-contained:AppTables::appInstalls()added, with a one-line comment, to the rung's reconcile list). The settler cannot read the new columns before the upgrade has run, because it never runs then:connect.phpstops both cron entry points while an upgrade is needed (read in the code, not executed here). The API answers 503 naming the upgrade.No open 1.9.79 branch touches the settler. Three touch
InstallEventsIntake.phpand none touchPendingClickSettler.phporInstallIntake.php, so this is not stacked.Tests
tests/Apps/Android/PendingClickQueueIntegrationTest.php(new,@group integration):AndroidIntakeJob::runExclusive()'s own default) of installs whose tokens another key signed. These sit ahead of an install whose click arrives. That install settles on the second run.Plants, each run against a named test, each restored and verified with
cmp:ORDER BY received_at)testAMissingKeyIsRetriedAndNeverRetiresmatch_statedirectlyAttributedInstallHasConversionTestAttributedInstallHasConversionTestnow lists the settler as a writer of202_app_installsthat sets onlysettle_*columns directly.InstallIntakeIntegrationTest's shape assertions gainretired.Live pass
The instance was served from a copy of this branch's tree and installed with
install.php's own steps but not its licence check, because the stub is down.php -Sand the cron ran with every proxy variable at 127.0.0.1:9.POST /api/v3/apps/installsansweredpending_click), and its click row was then written by SQL. The 501 unverifiable installs were seeded by SQL.origin/1.9.79's settler and job in the same tree): three runs, each500 examined … 500 failed. The install stayed pending.500 examined, settled none, 0 still pending, 500 failed, 0 retired as unreadable.500 examined, settled attributed=1, … 499 failed. The install was attributed with its conversion.GET /apps/1/installs?match_state=pending_clickshowedsettle_failures: 1, andGET /apps/report?platform=androidshowedpending_failing: 501.500 examined, settled none, 0 still pending, 0 failed, 500 retired as unreadableand exited 0. The run after it retired the last one. Each install isbad_token, trusted 0, with the reason inmatch_reason.202-cronjobs/index.php(the default scheduler's job) printed the new summary line too.Verification
.claude/skills/p202-verify/scripts/verify.sh --since origin/1.9.79 --changed(schema tier on a scratch MariaDB 10.11 database), on the committed branch:The unit tier ran 6104 tests with 4 skipped (the documented "User class requires DB singleton" skips). Beyond the script:
@group integrationsuite (tests/run-integration-suites.sh, each on a fresh scratch database): 146 suites and 1371 tests passed, and 1 suite failed. The failure wastests/Repository/MysqlLookupRepositoriesIntegrationTest.php::testTheFactoryCachesThroughTheBootstrapHelpers, withClass "Memcached" not found, because this sandbox's PHP has no memcached extension. That run overlapped edits to the settler.--group integration tests/Apps/Android, rerun on the final code: 185 tests, OK. This includes the new suite and the Integrity suites.tests/live/upgrade-equals-install.sh(1.9.55 on PHP 7.4, upgraded through the upgrade page, compared with a fresh install): 25 passed, 0 failed. Its notable lines:The run used the copy described below.
What could not run, and what ran without the licence
install-instance.shand the Go CLI could not run against the live instance: the licence stub is down, and an instance without the licence answers the CLI 402. The live pass drove the API with curl and the cron jobs on the CLI.tests/live/android-intake.shwas not run because it uses the Go CLI.tests/live/upgrade-equals-install.shran through a copy whose fresh install (step 2) usedinstall.php's steps without the licence check, the same scratch installer as the live pass, instead ofinstall-instance.sh. It also used one port for both phases. Steps 1 (1.9.55 installed on PHP 7.4), 3 (the upgrade page) and 4–5 (schema and rows) are the script's own. Result: 25 passed, 0 failed.For the reviewer
bad_token(refuted, never paid) followsOrphanedPendingClicks, which ends a pending click that can no longer be settled the same way. No new match state was added.stored()names. Every other failure is retried indefinitely, at most once an hour, and is visible aspending_failing.settle_failuresis left as it was when an install settles, so a retired install shows how many tries failed.pending_failingcounts only installs stillpending_click.Generated by Claude Code