fix(artifacts): do not derive a save's version number from a failed listing - #1419
fix(artifacts): do not derive a save's version number from a failed listing#1419svetanis wants to merge 1 commit into
Conversation
|
Hi @svetanis, thank you for your contribution! We appreciate you taking the time to submit this pull request. I noticed that the branch is currently out of sync with the base branch. could you please resolve the merge conflicts so we can proceed with the review? |
c11a83c to
920f6f0
Compare
|
Hi @hemasekhar-p, thanks for taking a look. I've rebased the branch onto the latest main (1929be7) — it's now up to date with the base branch and still a single commit. There were no merge conflicts to resolve: the branch was simply behind main. It touches only GcsArtifactService.java and GcsArtifactServiceTest.java, and neither file was modified on main since the branch point — the two intervening commits touched RemoteA2AAgent and pom.xml. The diff is unchanged from the original submission. Ready for review whenever it's convenient. |
920f6f0 to
3aaaeb1
Compare
|
@svetanis, Thank you for the update. Currently this PR is under review by our team, we will keep you posted if any additional information is required. thank you. |
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
1. Link to an existing issue (if applicable):
2. Or, if no issue exists, describe the change:
Problem:
GcsArtifactService.listVersionsreturns an empty list when the listing raisesStorageException.saveArtifactAndReturnBlobderives the next version number from that result(
versions.isEmpty() ? 0 : max(versions) + 1) and writes with no precondition. At the point wherethe version number is chosen, an empty list can mean either that the artifact has no versions or
that the listing did not complete — so a 503, a 429, a timeout or IAM propagation makes the save
compute version 0 and write over the object already stored at version 0, while returning success and
version
0to the caller.listArtifactKeysissues the samestorageClient.list(bucketName, BlobListOption.prefix(...))calland surfaces a failure as
VerifyException.listVersionsis the one whose result selects the blobname a save writes to.
Solution:
Separate the listing from the decision about what a failed listing means. The query moves into a
private
readVersionsthat letsStorageExceptionpropagate, and each caller applies its ownpolicy:
listVersionscatches it and returns an empty list as before, the save path catches it andthrows. Four pieces:
readVersions— new private methodlistVersions, moved verbatim; the stream pipeline is unchangedlistVersions— public, behavior unchangedreadVersions, catchStorageException, returnImmutableList.of()versionsBeforeSaving— new private methodreadVersions, catchStorageException, throwVerifyException("Failed to list artifact versions from GCS", e)— the same formlistArtifactKeysalready uses for this operation, naming what failedsaveArtifactAndReturnBlob— one linelistVersions(...), nowSingle.fromCallable(() -> versionsBeforeSaving(...)); everything after it is untouchedlistVersionshas three callers besides the save path —loadArtifact,deleteArtifact, andArtifactControllerin thedevmodule — so removing the catch outright would changeall of them as well. Leaving it in place keeps the diff to the one caller that overwrites stored
data: those three behave exactly as before, and
listVersions_storageException_returnsEmptyListpasses untouched. That is a scoping choice, not a claim that the empty list is right for them; the
issue lists them as related and uncovered. If you would rather the catch went away and those callers
moved with it, say so and I will make that change instead.
core/src/main/java/…/artifacts/GcsArtifactService.java+81/-18core/src/test/java/…/artifacts/GcsArtifactServiceTest.javaTesting Plan
Unit Tests:
GcsArtifactServiceTest: Tests run: 26, Failures: 0, Errors: 0, Skipped: 0. Both new tests wereconfirmed failing on unmodified
mainby revertingGcsArtifactService.javaalone and re-running —Tests run: 26, Failures: 2, exactly these two:mainsave_listStorageException_propagatesVerifyException, withhasCauseThat().isInstanceOf(StorageException.class)so the underlying failure stays visiblesave_listStorageException_doesNotWriteverify(mockStorage, never()).create(...)Both also
verify(mockStorage).list(...), so neither can pass by failing earlier for an unrelatedreason.
Manual End-to-End (E2E) Tests:
PAYLOAD-Athrough a fully-permissionedclient, then save
PAYLOAD-Bto the same filename — arm 1 through an identity deniedstorage.objects.list, arm 2 through a client that can list. The bucket is inspected afterwardswith a privileged credential, since the restricted identity cannot list.
PAYLOAD-APAYLOAD-Astorage.objects.listPAYLOAD-BPAYLOAD-AloadArtifact(0)PAYLOAD-BPAYLOAD-AThe control arm is unchanged by the fix in both runs — versions
[0, 1], both objects kept — so thetwo arms differ only in which client performs the second save.
Checklist