Skip to content

Initial-copy replication silently drops records with scalar @computed attributes #2359

Description

@kylebernhardy

+## Summary

On Harper 5.2.6, initial-copy replication can silently drop every record from a table that has a surfaced scalar @computed attribute while advancing the copy cursor and reporting the link healthy.

The receiver logs:

TypeError: attribute.set is not a function

This is a data-convergence failure, not only a read error. The receive loop classifies the exception as decode-drop, skips the row, and continues to COPY_COMPLETE. Reconnecting does not redeliver rows after the cursor has advanced.

A contaminated record already at rest can also prevent the sender's copy walk from progressing, producing a close/retry loop at the same row.

Independent reproduction

This reproduction uses only Harper/Harper Pro and a minimal component; it does not depend on another application or production data.

  1. Start two Harper 5.2.6 nodes, A and B, with replication enabled and the same component deployed.
  2. Use this schema:
type ComputedReplicationRecord @table @export {
  id: ID @primaryKey
  source: String
  derived: String @computed(from: "source") @indexed
}
  1. Before B subscribes, write multiple records on A:
PUT /ComputedReplicationRecord/one
Content-Type: application/json

{"id":"one","source":"value"}
  1. Connect B to A with add_node and allow the initial copy/catch-up to finish.
  2. Compare source and receiver row counts, read /ComputedReplicationRecord/one on B, and inspect receiver logs and cluster status.
  3. The same core failure can be exercised without networking by decoding/materializing an affected-release payload containing {"id":"one","source":"value","derived":"value"} against the table's struct prototype.

Actual

  • The receiver throws TypeError: attribute.set is not a function while decoding the copy row.
  • The row is skipped under the decode-drop path.
  • The copy cursor advances and the node can report connected/current despite missing rows.
  • A table-sized copy can produce one large error log per dropped row.

Expected

  • A stored or transported value colliding with a read-only computed attribute cannot prevent materialization.
  • The computed resolver remains authoritative.
  • Durable and transport encodings contain stored fields, not response-only computed projections.
  • The affected payload decodes and applies successfully, so it never reaches Pro's established permanent-decode skip path.

Root cause

TableResource.updatedAttributes installs an accessor setter for every resolved attribute, but scalar computed attributes have no attribute.set. Harper's decoded record prototype also has a REST-oriented toJSON that adds surfaced scalar computed values. Any internal re-encode of a decoded record invokes that response projection before struct encoding.

Harper Pro initial copy performs such a re-encode on the sender. Core replication apply then re-encodes the decoded RecordObject into receiver storage, so a Pro-only sender fix would contaminate the receiver and fail on a later hop. Live audit-tail forwarding normally sends the original durable bytes and does not take the initial-copy re-encode path.

Acceptance criteria

  • Tolerate affected-release payloads on the immutable decoded-record surface; signal the first collision per table/worker without per-row analytics or log flood.
  • Keep mutable computed attributes explicitly read-only and preserve writable relationship setters.
  • At the shared core durable-encoding boundary, suppress an affected table's response toJSON so the real stored struct is encoded without a projection allocation; use a nesting-safe global symbol shared by duplicate module instances.
  • In Pro's indexed residency/invalidation projection, substitute a simple computed field with its source attribute so receiver-side recomputation preserves search behavior.
  • Preserve direct-write validation: clients still cannot assign @computed attributes.
  • Add a focused core regression that fails on the current base with the exact TypeError and passes with the fix.
  • Prove durable encoding bypasses response projection while ordinary JSON output and no-computed table descriptor/bytes are unchanged; cover nested records, buffer grow/retry, variable/overflow structures, and typed values.
  • Assert a forged transported computed value wins neither point reads nor computed-index lookups.
  • Cover authentic old put and patch/merge payloads plus a local read-modify-write; assert primary and audit durable bytes omit the computed key.
  • Add a Pro unit regression through initial-copy encode/decode, source-substituting indexed-partial encoding, and receiver durable re-encode, and mechanically demonstrate it fails on the Pro base revision.
  • Add or extend a two-node integration test covering an authentic affected old payload and initial copy of multiple records with an indexed scalar @computed attribute.
  • The integration test verifies row-count parity, resolver-wins behavior, no decode-drop/error log, connected/current status, and a post-copy live update.
  • Keep decodeDropRecovery.test.mjs green unchanged; generic permanent-decode quarantine/retry policy is separate protocol work.
  • Run the full required core and Pro unit/integration gates plus before/after cluster-ingest performance coverage.

Rollout and recovery

Upgrade source/sender nodes first so they can materialize contaminated stored rows and emit clean payloads, then upgrade receivers. A new receiver can tolerate an affected old sender when the sender can still materialize its source row; already-contaminated sender storage requires the sender upgrade first.

Because affected receivers may already have advanced past dropped rows or retained stale computed-index entries, force a full base copy and rebuild affected computed indices after both sides are fixed, then verify row-count and computed-index parity; reconnect alone is insufficient.

Version

Confirmed on 5.2.6. Target the v5.2 patch line as well as main.

Comment generated by kAIle (Codex GPT-5.6)

Metadata

Metadata

Assignees

Labels

area:replicationReplication, clusteringbugSomething isn't working

Type

Fields

Priority

P1

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions