[ENG-45879] Classify control-plane "table is deleted" as a skip, not an API failure - #197
Open
suryadanny wants to merge 1 commit into
Open
[ENG-45879] Classify control-plane "table is deleted" as a skip, not an API failure#197suryadanny wants to merge 1 commit into
suryadanny wants to merge 1 commit into
Conversation
…an API failure
The initialize-tables call returns HTTP 200 with a per-table `error` string
of "Table can be skipped as it is deleted" once a table is deleted control-plane
side. That is an advisory ("nothing to upload for this table"), not a failure,
but any non-blank per-table error was being counted as API_FAILURE_USER_ERROR
and logged at ERROR -- twice per table, per cycle.
Found while investigating PD Q043G44USWZC7F, where an org with 198 of 199
tables deleted produced 9,528 counted "failures" in ~9.5h on a completely
healthy extractor (Running, 1/1, restartCount 0, discovery succeeding
throughout). The reason code also reads as connectivity/credentials, so it
actively misdirects triage toward expired API tokens.
- MetricsConstants: new TableSkipReasons enum, deliberately separate from
MetadataUploadFailureReasons so the failure counter keeps meaning
"something went wrong"
- MetadataExtractorUtils.isTableDeletedError: classifies the advisory,
alongside the existing getMetadataExtractorFailureReason helper
- LakeViewExtractorMetrics.incrementTableSkippedCounter: new
lakeView_table_skipped counter tagged table_skip_reason
- TableMetadataUploaderService: skip quietly at INFO instead of ERROR
Real per-table errors are unaffected and still counted as
API_FAILURE_USER_ERROR -- covered by a regression test.
Matched on message text because InitializeSingleTableMetricsCheckpointResponse
carries only a free-text `error` field; the durable fix is a structured
error/skip code on that response, noted in the javadoc.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Summary
When a table is deleted control-plane side,
POST /v1/community/initialize-tablesreturns HTTP 200 with a per-tableerrorstring:That is an advisory — "there is nothing to upload for this table" — not a failure. But the call site treated any non-blank per-table
errorasAPI_FAILURE_USER_ERRORand logged atERROR, twice per table per cycle (once at the call site, once insideincrementTableMetadataProcessingFailureCounter).Found while investigating PD
Q043G44USWZC7F: an org with 198 of 199 tables deleted produced 9,528 counted "failures" in ~9.5 hours on a completely healthy extractor —Running,1/1,restartCount: 0,lakeView_table_discovery_success_totalincrementing +12/hr throughout. Nothing was wrong with the extractor, the network, or the credentials.Two concrete harms:
API_FAILURE_USER_ERRORreads as connectivity or credentials — the same reason code is used for real401/403s fromOnehouseApiClient.emmitApiErrorMetric. The natural first move is to go chase an expired API token, when in fact no HTTP call failed.lakeView_table_metadata_processing_failure_total{metadata_upload_failure_reason="API_FAILURE_USER_ERROR"}could not distinguish "the customer deleted their tables" (no action) from "our API is rejecting us" (page someone).Changes
..._processing_failure{reason="API_FAILURE_USER_ERROR"}lakeView_table_skipped{table_skip_reason="DELETED"}ERROR×2 per table per cycleINFO×1MetricsConstants— newTableSkipReasons { DELETED }, deliberately separate fromMetadataUploadFailureReasonsso the failure counter keeps meaning "something went wrong".MetadataExtractorUtils.isTableDeletedError(String)— classifies the advisory, placed alongside the existinggetMetadataExtractorFailureReasonhelper.LakeViewExtractorMetrics.incrementTableSkippedCounter(...)— newlakeView_table_skippedcounter taggedtable_skip_reason. Deliberately does not log (unlike the failure method, which logs internally — that's the source of the second ERROR line); the caller logs at INFO with table context.TableMetadataUploaderService— branch on the classification before the failure path.Kept a counter rather than going silent:
lakeView_table_skipped{table_skip_reason="DELETED"}would have answered the whole investigation in one query.Technical details
Real errors are unaffected. Only the deleted-advisory is reclassified; every other per-table
errorstill incrementsAPI_FAILURE_USER_ERRORand logs atERROR. There is a regression test asserting exactly that.Behavior otherwise unchanged. The skip branch
continues exactly as the old error branch did, so control flow and the batch return value are untouched.Known limitation — matched on message text.
InitializeTableMetricsCheckpointResponse.InitializeSingleTableMetricsCheckpointResponsecarries only a free-texterrorfield; there is no structured error/status code to switch on. A control-plane reword could silently break the match, so I used two distinctive markers rather than the full sentence and documented the constraint in the javadoc. The durable fix is control-plane side: a structurederrorCode/skipReasonon that response, at which pointisTableDeletedErrorbecomes a code check. Happy to follow up if the API team wants to add one.Testing
./gradlew :lakeview:test— 42 tests across the three affected classes, all passing:MetadataExtractorUtilsTest(16) — the verbatim production string; the wrapped form as it reaches the call site; case-insensitivity; a reworded prefix; plus negative cases asserting401/403/Internal server errorand deletion-adjacent-but-real failures are not reclassified; and null/empty.TableMetadataUploaderServiceTest(13) — two new: a deleted table increments the skip counter, never the failure counter, and is never uploaded; a real error still counts asAPI_FAILURE_USER_ERRORand never as a skip.LakeViewExtractorMetricsTest(13) — the new counter's tag set, and that it never touches the failure counter../gradlew spotlessCheckpasses.main:AsyncHttpClientWithRetryTest.testMakeRequestWithRetryIOExceptionfails (1 of 322). Confirmed pre-existing by stashing this branch's changes and re-running it against cleanorigin/main— it fails identically there. Not introduced here, but it will make CI'sclean checkred until it's fixed separately.Noted, not changed
A real per-table init error still lets the batch report success — the error path
continues without adding a future, andallMatchon an empty stream returnstrue. That's correct for the deleted case (nothing to do), but arguably wrong for real errors. Left alone because fixing it changes behavior beyond classification; worth its own ticket.Related ticket
Q043G44USWZC7Fthat surfaced it🤖 Generated with Claude Code