feat: SQL classifier, 7 structured log fields, 10 s admin_ajax ceiling, PHPUnit suite#40
Closed
noelsaw1 wants to merge 3 commits into
Closed
feat: SQL classifier, 7 structured log fields, 10 s admin_ajax ceiling, PHPUnit suite#40noelsaw1 wants to merge 3 commits into
noelsaw1 wants to merge 3 commits into
Conversation
…uite Motivation ---------- On 2026-05-30 a production WooCommerce store suffered a DB saturation incident via admin-ajax.php. A `wp_comments WHERE comment_ID IN (… 10,639 IDs …)` query fired repeatedly through order-note loading, consumed large DB time per execution, and generated repeated "MySQL server has gone away" errors. This commit reduces blast radius for that exact class of failure and adds the logging fields needed to identify it in future incidents. Changes ------- * admin_ajax ceiling: 20 s → 10 s Halves per-execution DB exposure when concurrent ajax workers pile up. Checkout (60 s) and wp_admin (45 s) are unchanged. Operators can relax specific actions via hypercart_query_guard_limit_ms filter. * classify_sql( $sql ): array — public static Cheap, safe SQL shape detection (one anchored preg_match + substr_count). Returns: table_hint, is_comment_query, has_large_in_list, estimated_in_list_size, is_probable_woocommerce, is_probable_order_note_query. Only classifies SELECT; writes return all-default values. Threshold for "large" IN list: 200 items (LARGE_IN_LIST_THRESHOLD const). * is_admin_ajax_request( $uri ): bool — public static Simple strpos check; public for testability. * Both slow_query and query_killed events carry all 7 new fields: is_admin_ajax + the 6 classifier fields. * hypercart_query_guard_log_payload filter added inside log() so callers can add custom fields, route to extra sinks, or intercept payloads in integration tests. * PHPUnit suite: phpunit.xml, tests/bootstrap.php (WP function stubs), tests/SqlClassifierTest.php (22 tests), tests/PayloadFieldsTest.php (13 tests). 35 tests, 117 assertions. * README: updated admin_ajax limit table entry, expanded Logging section with classification field reference, added enforce-mode rollout guide. * CHANGELOG: Unreleased block documenting all changes.
cbb68a0 to
875e551
Compare
…t first) - apply_filters null-return guard: replace bare (array) cast with is_array+!empty check so a hook callback that omits return $payload no longer silently reduces the log payload to []. - preg_match -> preg_match_all in classify_sql: scan every IN ( occurrence and keep the maximum list size, preventing a subquery IN earlier in the query from masking a large literal IN list. - tests/bootstrap.php: capture wp_json_encode argument in $GLOBALS['_qg_last_encoded'] for reliable emission-side assertions. - Add 3 regression tests: 2 for subquery masking (SqlClassifierTest), 1 for null-return filter guard (PayloadFieldsTest).
Contributor
|
Commits moved to a rebased development branch PR #41 |
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.
Motivation
On 2026-05-30 a production WooCommerce store suffered a DB saturation incident via
admin-ajax.php. Awp_comments WHERE comment_ID IN (… 10,639 IDs …)query fired repeatedly through order-note loading, consumed a large share of DB time per execution, and generated repeatedMySQL server has gone awayerrors. This PR reduces blast radius for that exact class of failure and adds the logging fields needed to identify it in future incidents.Changes
admin_ajaxceiling: 20 s → 10 sadmin-ajax.phpis the primary vector for runaway read queries (WC order-note loading, Facebook background sync, NoFraud). Halving the ceiling halves the per-execution DB exposure when concurrent workers pile up.checkout(60 s) andwp_admin(45 s) are unchanged. Thehypercart_query_guard_limit_msfilter allows operators to relax specific$_REQUEST['action']values without touching the global ceiling.classify_sql( $sql ): array— new public static methodCheap, safe SQL shape detection: one anchored
preg_matchlocates the firstIN (position, thensubstr_countcounts commas in the list body (O(n), no backtracking). Everything else isstripos. No SQL mutation.Returns six fields:
table_hint'wp_comments'when that table appears in the SQL, else''is_comment_querytruewhentable_hint === 'wp_comments'has_large_in_listtruewhen anIN (…)list has ≥ 200 items (LARGE_IN_LIST_THRESHOLDconst)estimated_in_list_sizeIN (…)found;0if noneis_probable_woocommercetruewhen WooCommerce table/keyword hints are presentis_probable_order_note_querytruewhen query targetswp_commentsand mentionsorder_noteOnly classifies
SELECT; writes return all-default values.is_admin_ajax_request( $uri ): bool— new public static helperSimple
strposon'admin-ajax.php'. Public for testability.7 new structured fields on
slow_queryandquery_killedeventsBoth events now carry
is_admin_ajax+ the 6 classifier fields. A killed incident query now looks like:{ "event": "query_killed", "context": "admin_ajax", "limit_ms": 10000, "last_query": "SELECT comment_ID … FROM wp_comments WHERE comment_ID IN (1, 2, …)[truncated]", "uri": "/wp-admin/admin-ajax.php?action=woocommerce_load_order_notes", "is_admin_ajax": true, "table_hint": "wp_comments", "is_comment_query": true, "has_large_in_list": true, "estimated_in_list_size": 10639, "is_probable_woocommerce": true, "is_probable_order_note_query": true }hypercart_query_guard_log_payloadfilterFires inside
log()before emission. Allows callers to add custom fields, route to additional sinks, or intercept payloads in integration tests.PHPUnit suite
phpunit.xml— test runner configtests/bootstrap.php— minimal WordPress function stubstests/SqlClassifierTest.php— 22 tests (classifier edge cases, write-query exclusion, WC heuristics, boundary conditions)tests/PayloadFieldsTest.php— 13 tests (bothquery_killedandslow_querypayload shapes, admin-ajax URI detection in context)35 tests, 117 assertions, all passing.
Non-goals (not changed)