Skip to content

Latest commit

 

History

History
349 lines (309 loc) · 19.5 KB

error_codes.md

File metadata and controls

349 lines (309 loc) · 19.5 KB

Error codes

Ape-DTS task-runtime errors have a stable five-character condition code. The code identifies what happened; runtime location is recorded separately as a stage. In schema v1, both dt-main and dtscli render failures through the same user-facing error report boundary.

For example:

ERROR REPORT
  [MD001]: A required source or destination object was not found
  AFFECTED OBJECT: schema=public, table=orders
  CAUSED BY:
    0: postgres/42P01: relation does not exist

Each code is a stable condition identity, and one report contains one final code. Stage is independent metadata. Application logic must inspect the typed ErrorCode; it must not parse messages or combine a code and stage into another identifier.

Compatibility rules

  • Codes are five uppercase ASCII letters or digits.
  • Codes are append-only and must never be reused for a different condition.
  • A category code ending in 000 is reserved and must not be emitted.
  • The stage is not part of the stable error identity.
  • Stage and endpoint are explicitly attached by execution boundaries; reports never infer either field from a code, provider, or module name.
  • Retry, skip, fallback, and DLQ behavior are policy decisions outside the error-code contract.

The catalog is declared once in dt-common::error and generates the enum, wire-code mapping, default message, default hint, and complete code list. New codes must be added to that declaration rather than maintained in parallel matches.

Code catalog

Code Condition Default meaning
CF001 MissingConfig Required configuration is missing
CF002 InvalidConfig Configuration is invalid
CF003 MissingConfigItem A required configuration item is missing
CN001 ConnectionFailed An endpoint connection could not be established or was lost
CN002 ConnectionTimeout Connecting or acquiring a pooled connection timed out
CN003 TlsFailed A secure database connection could not be established
AU001 AuthenticationFailed Credentials were rejected
AU002 PermissionDenied The authenticated account lacks permission
PR001 PrerequisiteNotMet A required version, CDC setting, slot, or capability is unavailable
PR002 UnsupportedDatabaseVersion The database version is unsupported
PR003 CdcNotEnabled The source database is not configured for CDC
PR004 ReplicationCapacityExhausted No replication slot or sender capacity is available
PR005 UnsupportedTableStructure A table structure is unsupported
PR006 UnsupportedStatement A source statement is unsupported
RS001 ResourceExhausted A source or destination resource limit was reached
MD001 ObjectNotFound A required table, topic, or other endpoint object does not exist
MD002 DatabaseNotFound A database does not exist
DB001 DatabaseOperationFailed A database operation failed
DB002 DatabaseOperationTimeout A database operation timed out
DB003 DatabaseOperationConflict A database operation conflicts with concurrent or existing database state
DT001 DataDecodeFailed Migration data could not be decoded
IC001 IntegrityViolation A constraint was violated
ST001 CheckpointReadFailed Checkpoint state could not be read
IO001 IoFailed An I/O operation failed
RT001 WorkerFailed A task worker terminated unexpectedly
RT002 OperationInterrupted The requested operation was interrupted
IN001 InvariantViolated An internal invariant was violated
IN999 Unclassified No stable classification is available yet

Diagnostic stage catalog

Stage Meaning
bootstrap Loading task configuration
precheck Checking migration prerequisites
extractor Reading from the source
parallelizer Preparing migration work
pipeline Processing migration data
sinker Writing to the destination
checker Checking migrated data
resumer Restoring saved task progress
task Running the migration task
unknown No structured stage is available

User and diagnostic views

ErrorReport JSON is a versioned machine interface. The current schema_version is 1. Code, stage, task ID, and endpoint role are scalar. User messages, details, hints, and affected objects are arrays. Code, stage, and endpoint are resolved from innermost to outermost; task ID is resolved from outermost to innermost. A recognized concrete error owns the code, and an explicit context code is a fallback when the concrete error is unclassified. Arrays preserve outermost-to-innermost first-seen order and remove exact duplicates. Text rendering always uses zero-based detail indexes. CLI text layout is not a stable machine interface. Messages appear after the bracketed code and are separated by semicolons. Fields within one affected object are separated by commas, while multiple affected objects are separated by semicolons.

The serialized ErrorReport no longer stores error_chain or context_count. It includes its UTC creation timestamp and an optional captured backtrace in addition to the user-facing fields. details collects ordinary anyhow::Context values and concrete causes from outermost to innermost; the internal metadata marker is omitted. The report boundary uses rtb-redact to remove credentials, authenticated URL userinfo, authorization values, provider tokens, JWTs, long opaque tokens, and private keys before any of these values enter the user view.

Text output shows the code and messages, optional affected objects, details under CAUSED BY, and an optional backtrace:

ERROR REPORT
  [DB001]: A database operation failed
  AFFECTED OBJECT: schema=public, table=orders, constraint=orders_pkey
  CAUSED BY:
    0: starting task
    1: postgres/42P01: relation does not exist
  BACKTRACE:
    0: dt_task::task_runner::TaskRunner::start_task

Details can include SQL, row data, object names, provider messages, and anyhow::Context values. Credential-shaped values and authenticated URLs are redacted, but stderr and captured logs must still be treated as sensitive data. dt-main writes the text form to default.log and additionally appends the complete JSON form to error_report.log. Each error_report.log record starts with a UTC logger timestamp followed by | and the JSON string. The JSON also contains the report creation timestamp; JSON escaping keeps a multi-line backtrace within the same log record. This dedicated logger accepts every log level and is independent of the task's configured runtime log level.

Structured errors

anyhow::Error is the only error transport container. DtErrorContext is a typed metadata frame whose code, message, detail, hint, stage, task ID, endpoint role, and database object are all optional. It is a plain data object and implements neither Display nor std::error::Error. The endpoint role is source, destination, or metadata. Database objects may include schema, table, column, and constraint names.

The innermost cause is always a real error. Provider failures keep their original provider error type. Application-authored failures use the thiserror-based DtError enum.

Project-owned failures select a semantic DtError variant and add only the metadata known at that call site. For example, DtError::InvalidConfig(detail) creates the root cause; its classifier supplies ErrorCode::InvalidConfig, the detail, and Stage::Bootstrap. The ClassifyError implementation maps every DtError variant to a DtErrorContext containing its stable default code and any metadata intrinsic to that variant. Adding a new variant therefore requires an explicit classifier arm and cannot silently fall through. DtError::Unclassified is reserved for a project-owned failure for which no stable classification is available.

The error extension trait is implemented for every error type that can convert into anyhow::Error. DtResultExt provides the same code, message, hint, stage, task_id, endpoint, and object methods directly on Result. Its dt_context method accepts a closure, so metadata is not built on the successful path. Project-owned failures should normally use a semantic DtError variant. Unknown concrete errors retain their source type when converted to anyhow::Error. When a project-owned classification also has a lower-level source, put the semantic DtError in the anyhow context chain above that source. Both the DtError and the original source then remain downcastable, and the report can classify both without discarding the source diagnostics.

Metadata is added as the error crosses ownership boundaries. A leaf frame can contain the business code and affected object. Stage and endpoint are attached at the narrowest component boundary that owns the operation, while task ID is attached at the highest boundary that knows it: for example, the extractor worker attaches extractor/source, the common sinker adapter attaches sinker/destination, the recovery initialization boundary attaches resumer/metadata, and the precheck entry attaches precheck and the task ID while its builder attaches source or destination around each checker call. The normal task entry is the only production boundary that attaches its task ID. No report-time inference is used.

Each frame is flat and immutable. The context extension appends frames to an internal ordered list carried by anyhow::Error; DtErrorContext itself has no parent link. ErrorReport resolves code, stage, and endpoint from innermost to outermost, while task ID is resolved from outermost to innermost. Messages, details, hints, and affected objects are appended in outermost-to-innermost first-seen order with exact duplicates removed. Classifiers primarily populate DtErrorContext.detail; ordinary contexts and unclassified concrete causes also contribute redacted details. Project-owned DtError values retain their variant-specific full Display text and typed payload, just as provider errors retain their own Display, source chain, and concrete type.

ErrorReport is the user-facing boundary representation. Its text form uses the bracketed error code followed by semicolon-separated messages. When present, affected objects are rendered on one line: fields within an object use commas and multiple objects use semicolons. Details remain ordered and zero-based under CAUSED BY, and a captured backtrace is appended as a BACKTRACE block. The complete structured fields remain available in its JSON form.

Provider classifier implementations live under dt-common::error::provider. They implement ClassifyError using provider-native codes, typed error kinds, and Rust error variants; they must not parse provider messages. Each implementation returns a DtErrorContext containing an optional recognized code, affected object, and complete provider detail. A small chain registry in the parent-level dt-common::error::classifier applies the same trait to supported provider causes preserved in the source chain. The SQLx classifier infers MySQL or PostgreSQL from the concrete database error. Call sites do not pass a database-family hint or attach a separate provider frame. A recognized provider condition owns the identity. Every supported provider classifier maps an unrecognized driver error directly to DB001; project-owned semantics use a typed DtError. Provider name, original code, and concrete error text are stored in details. Stage, endpoint, and task ID are attached separately at the execution layer that knows them.

Provider classification belongs in dt-common::error::provider, not in per-crate wrappers. Business modules use DtError for project-owned failures, ordinary anyhow::Context for diagnostic prose, and the context extension methods only for explicit metadata. Do not introduce per-crate helper modules that merely forward arguments to these shared mechanisms.

dt-common::error::classifier first collects typed frames, recognizes a typed DtError through anyhow::Error::downcast_ref, and then traverses the source chain to classify concrete causes. A classifier supplies provider detail directly; an unclassified non-marker error uses its redacted Display as detail. ErrorReport only applies the returned DtErrorContext values in order. IN999 is used when no code is available. Failure-path tests and code review remain necessary for project-owned semantic errors and call-site metadata that no raw provider error can supply.

Failures while reading endpoint catalog or control metadata retain their typed provider error so that the provider classifier determines the code. Missing objects use MD001/MD002, unsupported structures use PR005, source statements unsupported by the Ape-DTS parser use PR006, unmet version or topology requirements use PR001/PR002, and rejected destination DDL uses DB001. Invalid source payloads and persisted migration records use DT001.

Classify errors by what the user can verify or change, not by the internal module or data structure that detected the failure. A missing schema, table, or column uses MD001 and identifies the affected object. A CDC row event without its matching table or relation definition uses DB001 and tells the user how to restart from an earlier log position. An incomplete Redis slot map uses PR001 and tells the user to stabilize the cluster topology. Use IN001 only when no endpoint, configuration, or migration action can reasonably resolve the failure. Internal terms such as cache entry, OID map, or Rust type must never be the only explanation. The user message and hint must state the affected endpoint behavior and the next action. DETAIL may include a provider identifier such as a table or relation ID when it helps correlate the failure with provider logs.

Provider error preservation

Provider codes are diagnostic data, not Ape-DTS error codes. The original typed provider error remains in the source chain, and its redacted display and source text contribute to DETAIL; there is no separate provider-origin field. The codes are also used internally for these initial SQLx mappings:

Provider error Ape-DTS code
PostgreSQL missing schema/table/column/object/function; MySQL missing table/column/object/routine MD001
PostgreSQL 3D000/57P04, MySQL 1049 MD002
PostgreSQL SQLSTATE class 28; MySQL access-denied, locked, or rejected account AU001
PostgreSQL 42501; MySQL command, object, or administrative permission errors AU002
PostgreSQL SQLSTATE class 08 and shutdown errors; MySQL client/server connection errors CN001
PostgreSQL SQLSTATE class 23/40002; SQLx integrity error kinds IC001
PostgreSQL serialization/deadlock/lock conflicts; MySQL lock timeout/deadlock/NOWAIT conflict DB003
PostgreSQL query canceled; MySQL query interrupted RT002
MySQL maximum statement execution time exceeded DB002
PostgreSQL resource/program limits; MySQL disk, memory, table, connection, or quota limits RS001
PostgreSQL unsupported feature class; MySQL unsupported or disabled feature PR001
SQLx connection configuration error CF002
SQLx I/O, protocol, or closed pool CN001
SQLx pool timeout CN002
SQLx TLS error CN003
SQLx missing database type MD001
SQLx row or value decode error DT001
SQLx worker crash RT001

Unrecognized SQLx database and client variants are DB001. The original provider code, constraint/table metadata, and source error information remain available through detail and object fields.

The tokio-postgres replication adapter uses the same PostgreSQL SQLSTATE rules. It also classifies closed connections, nested I/O timeouts, and nested OpenSSL failures without parsing error messages.

Other initial provider mappings are:

Provider error Ape-DTS code
Redis authentication / NOAUTH / WRONGPASS AU001
Redis NOPERM AU002
Redis READONLY PR001
Redis invalid client configuration CF002
Redis timeout CN002
Redis I/O, cluster down, master down, or missing cluster connection CN001
Redis missing script or Sentinel master MD001
Redis unsupported RESP3 capability PR001
MongoDB authentication or reauthentication AU001
MongoDB command code 13 AU002
MongoDB missing namespace/index or GridFS file/chunk/revision MD001
MongoDB duplicate-key or document-validation error IC001
MongoDB max-time error / write conflict DB002 / DB003
MongoDB missing resume token / unsupported session or server capability ST001 / PR001
MongoDB invalid client options / invalid TLS configuration CF002 / CN003
MongoDB DNS, I/O, invalid response, pool-cleared, server-selection, or shutdown CN001 or CN002 for I/O timeout
MongoDB SystemOverloadedError label RS001
Kafka authentication / authorization AU001 / AU002
Kafka unknown topic, partition, group, resource, or log directory MD001
Kafka connection timeout / operation timeout / broker transport failure CN002 / DB002 / CN001
Kafka offset unavailable / operation conflict / resource limit ST001 / DB003 / RS001
Kafka filesystem or broker log-storage error IO001
Kafka unsupported capability / interrupted operation PR001 / RT002
SQL Server authentication / permission / missing object or database AU001 / AU002 / MD001 / MD002
SQL Server constraint / concurrency conflict / resource limit IC001 / DB003 / RS001
SQL Server value encoding, conversion, or parsing failure DT001
SQL Server transport, pool timeout, or TLS failure CN001 / CN002 / CN003
HTTP connection timeout / established-request timeout / connection failure CN002 / DB002 / CN001
HTTP 401/403/404/409/429 AU001/AU002/MD001/DB003/RS001
HTTP response decode failure DT001
Other HTTP status, redirect, or body failure IN999
MySQL binlog I/O timeout / other transport failure CN002 / CN001
MySQL binlog invalid URL or GTID / unsupported column type CF002 / PR005
MySQL binlog error 1236 (requested binlog unavailable) ST001
Other MySQL binlog decoding failures DT001

Worker join failures are RT001. Provider URL parsing variants are CF002. Logger YAML errors occur during startup and are surfaced directly by the dt-main expect boundary. DtError::Unclassified is IN999; an unsupported or unrecognized raw error adds IN999 only when the report contains no other code. A local filesystem std::io::Error is IO001; network I/O should retain its provider error type so that it becomes CN001 or CN002 instead. User-interrupted CLI operations are RT002.

mysql-binlog-connector-rust v0.3.4 discards the numeric code from MySQL error packets and exposes error 1236 as ConnectError(String). Until the connector preserves that typed code, the MySQL binlog classifier recognizes only the known "requested binlog unavailable" messages and classifies them as ST001. The raw ConnectError text remains in DETAIL. This narrow message-based exception compensates for type information lost by the provider library; other provider classification remains type- and code-based.

A malformed database connection URL is configuration failure CF002. CN001 is used only after a syntactically valid endpoint cannot be reached or an established connection is lost. This distinction keeps the user action clear: correct the config for CF002; investigate service and network reachability for CN001.