Skip to content

feat: send Glue VersionId on Hive table updates (GLUE_SEND_VERSION_ID) - #150

Merged
jamespfaulkner merged 7 commits into
mainfrom
feat/gluesync-send-versionid-single-attempt
Oct 8, 2026
Merged

jamespfaulkner merged 7 commits into
mainfrom
feat/gluesync-send-versionid-single-attempt

Conversation

@jamespfaulkner

Copy link
Copy Markdown
Contributor

What

Adds an opt-in GLUE_SEND_VERSION_ID flag (default off) to the GlueSync listener. When enabled, the listener reads the current Glue VersionId (via GetTable) and passes it on UpdateTable requests for non-Iceberg (Hive) tables, so AWS records requestParameters.versionId on the emitted CloudTrail UpdateTable event.

Why

The emitted versionId is the version the table was at immediately before the update. Consumers of the Glue/CloudTrail event stream can use it to deterministically identify and fetch the pre-update table version (via GetTableVersion) when handling ALTER_TABLE events, instead of guessing. Today no versionId is emitted, so that pre-update state can't be resolved reliably.

How

  • GlueTableService.update() — for Hive tables, reads the current VersionId and sets it on the UpdateTable. Iceberg tables are skipped (identified by metadata_location), avoiding the extra GetTable and optimistic-locking overhead.
  • The versionId is best-effort and never regresses the sync. Glue enforces VersionId as a compare-and-swap, so a concurrent writer fails the write with ConcurrentModificationException; in that case — or if the current VersionId can't be read (any Glue failure other than the table not existing) — the listener does a single unconditional update instead, matching the listener's existing last-write-wins behaviour when the flag is off. Only that one event loses its versionId. There is no retry loop: a conflict carries no correctness signal for a mirror, and the unconditional write reaches the same end state a manual resync would.
  • Observability — the two fallbacks surface on the glue_listener_event counter's outcome tag as version_conflict_fallback (CAS conflict) and version_unavailable_fallback (version unreadable).
  • Archive retention — for the carried versionId to resolve to fetchable content, the prior version must be archived, so when the flag is on, Hive tables are forced to skipArchive=false. An explicit per-table apiary.gluesync.skipArchive property still wins; Iceberg tables keep skipArchive=true (avoids table-version explosion).
  • Missing table — an EntityNotFoundException from the version read propagates so updateOrCreateTable creates the table, exactly as before the flag existed.

Behaviour is unchanged when the flag is unset (default), and the flag is honoured identically by the listener and the resync CLI.

Testing

  • GlueTableServiceTest — versionId set/skipped (Hive vs Iceberg vs disabled); conflict → single unconditional fallback; unreadable version → unconditional fallback; EntityNotFoundException propagates for create-on-missing.
  • GluePartitionServiceTest — skipArchive precedence (per-table override > Hive-retain-when-enabled > env default > built-in) and env-var parsing.
  • ApiaryGlueSyncTest — end-to-end: with the flag on, an ALTER_TABLE for a table missing in Glue still drives a create.
  • mvn verify green (142 tests, 0 failures), checkstyle/license clean.

Docs

README (GLUE_SEND_VERSION_ID env var + "Table update VersionId" section + updated SkipArchive precedence) and CHANGELOG updated.

jamespfaulkner and others added 5 commits October 7, 2026 12:50
Add an opt-in GLUE_SEND_VERSION_ID flag (default off) that makes the
GlueSync listener read the current Glue VersionId and pass it on
UpdateTable requests for non-Iceberg (Hive) tables, so AWS records
requestParameters.versionId on the emitted CloudTrail event. Consumers of
that event stream can then identify and fetch the pre-update table version
for ALTER_TABLE handling.

Glue enforces VersionId as a compare-and-swap, so a concurrent writer fails
with ConcurrentModificationException. The retry/fallback control flow is
extracted into OptimisticUpdateRetry (pure, independently tested): refresh
the version and retry up to a bound, then fall back to an unconditional
update so the sync never regresses below today's last-write-wins behaviour
(surfaced via the version_conflict_fallback outcome tag).

When the flag is on, Hive tables are forced to skipArchive=false so the
prior version is retained and the carried VersionId resolves via
GetTableVersion; an explicit per-table apiary.gluesync.skipArchive property
still wins. Iceberg tables are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously a non-EntityNotFound failure of the getTable read (throttle,
timeout, access denied, etc.) added by the versionId path propagated as a
sync failure for that event — a new failure surface vs the single-call
behaviour before the flag. The read is best-effort, so readVersionId now
returns null on any AmazonClientException (other than EntityNotFound, which
still propagates so the caller creates the missing table), and the update
proceeds unconditionally. Surfaced via a new version_unavailable_fallback
outcome tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…come

retryOnConflict now returns the result of whichever branch ran (Supplier<T>),
so each branch returns its own UpdateOutcome directly instead of signalling
"version unavailable" through a mutable boolean[] captured in the lambda.

Also add an end-to-end test proving that, with GLUE_SEND_VERSION_ID on, an
ALTER_TABLE for a table missing in Glue (EntityNotFoundException from the
getTable version read) still drives a create via updateOrCreateTable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the bounded retry loop and the OptimisticUpdateRetry helper. On a
ConcurrentModificationException (or when the versionId cannot be read), do a
single unconditional update instead of retrying. This keeps the key
guarantee — the sync never regresses below the listener's last-write-wins
behaviour — without the retry machinery, since a conflict carries no
correctness signal for a mirror and the unconditional write reaches the same
end state a manual resync would.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamespfaulkner
jamespfaulkner requested a review from a team as a code owner October 7, 2026 12:34
In prod the flag is set globally, so every table update takes the versionId
path. Add integration tests (real GluePartitionService, flag on) proving the
request shape for the flows the flag actually changes:
- Hive ALTER_TABLE on an existing table -> versionId set + skipArchive=false
- Iceberg ALTER_TABLE -> unaffected (no getTable, no versionId, skipArchive=true)
- CREATE_TABLE on an existing table (AlreadyExists -> update) -> versionId set

These exercise the real skipArchive wiring that the GlueTableService unit
tests mock away.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamespfaulkner
jamespfaulkner force-pushed the feat/gluesync-send-versionid-single-attempt branch from b129386 to 97b15dd Compare October 7, 2026 12:43
this(glueClient, gluePrefix, defaultSkipArchive, false);
}

public GluePartitionService(AWSGlue glueClient, String gluePrefix, Boolean defaultSkipArchive,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't exist versioning on glue partitions, so we don't need any change in this class?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. The GLUE_SKIP_ARCHIVE was in this class but I've moved the new prop to the table service.

* UpdateTable event does not carry a versionId.</li>
* </ul>
*/
public enum UpdateOutcome {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this in an external file class

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

…ched

Address PR #150 review:
- Send the Glue VersionId for all table types, not just Hive (Javier: versionId
  is preferable to scanning versions/metadata_location for the pre-update
  state). Removes the Iceberg filter.
- Leave GLUE_SKIP_ARCHIVE / shouldSkipArchive entirely in GluePartitionService,
  reverted to main (zero diff). The feature no longer overrides SkipArchive;
  archiving is governed by GLUE_SKIP_ARCHIVE as before (already false in all
  dronefly envs, so the emitted versionId resolves; versionId never gates or
  creates versions).
- Extract DeleteOutcome and UpdateOutcome into their own files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamespfaulkner jamespfaulkner changed the title feat: send Glue VersionId on Hive table updates (GLUE_SEND_VERSION_ID) feat: send Glue VersionId on Hive table updates (GLUE_SEND_VERSION_ID) Oct 7, 2026
public UpdateOutcome update(Table table) {
UpdateTableRequest updateTableRequest = new UpdateTableRequest()
.withSkipArchive(skipArchive)
.withSkipArchive(gluePartitionService.shouldSkipArchive(table))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why glue table calls glue partition serivce for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was already there. Just now rather than creating a boolean its inlined

@jamespfaulkner
jamespfaulkner merged commit 8e362a9 into main Oct 8, 2026
1 check passed
@jamespfaulkner
jamespfaulkner deleted the feat/gluesync-send-versionid-single-attempt branch October 8, 2026 09:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants