Repository navigation
SOLR-18212: Migrate PackageAPI endpoints from @EndPoint/@Command to JAX-RS - #4178
Conversation
- Replace PackagePayload.AddVersion with AddPackageVersionRequestBody
- Move package name from body to URL: POST /cluster/package/{name}/versions
- Replace delete command pattern with DELETE /cluster/package/{name}/versions/{version}
- Replace refresh command with POST /cluster/package/{name}/refresh
- Update errPath in testAPI from /details[0]/errorMessages[0] to /msg
- Remove all add.pkg assignments (pkg now lives in URL path)
- Replace add.pkg references in verifyComponent() with literal package name strings
- Update import: remove PackagePayload, add AddPackageVersionRequestBody (sorted)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: epugh <22395+epugh@users.noreply.github.com>
…sponse.error field Co-authored-by: epugh <22395+epugh@users.noreply.github.com>
Co-authored-by: epugh <22395+epugh@users.noreply.github.com>
Co-authored-by: epugh <22395+epugh@users.noreply.github.com>
|
Is there a JIRA ticket associated with this change @epugh? |
No, because I viewed it as "just a refactoring and no one will notice....", and that I was hoping there were no debates that would require a JIRA to hash out! |
| authors: | ||
| - name: Eric Pugh | ||
| links: | ||
| - name: PR#4178 |
There was a problem hiding this comment.
[0] Probably worth having a JIRA reference for this guy, even if it's just a tie-back to one of the v2 umbrella tickets? It's changing APIs and stuff, so worth tracking in more than just the PR description I'd imagine.
| public int hashCode() { | ||
| return Objects.hash(version); | ||
| } | ||
| final var request = |
There was a problem hiding this comment.
[Q] Is there a reason you're not using the auto-generated SolrJ type here? We should have one now that the API is JAX-RS!
If this was an intentional decision made to prevent scope-creep, I'm fine with that. Just making sure it's not oversight.
There was a problem hiding this comment.
so many places to update... I suspect in the future we need to do a "Check each GSR for fitness" check ;-)
There was a problem hiding this comment.
Like I said, I think not doing these bits in the same PR is a totally valid choice. I don't want to add extra steps that'd apply to all these PRs. But since the lines were changed in this PR already I wanted to check that it was an intentional choice is all 🤷
…kageapi-to-jax-rs
Rename PackageAPI to ClusterPackage to follow naming pattern of other V2 apis.
Add test, revamp description.
|
@gerlowskija I think I am ready for another review. I renamed |
|
@epugh I am planning to review this PR, can you make this one ready for review by rebasing with latest main? |
|
@jaykay12 I added you as a collaborate to my |
…n the Package v2 API - Move the inter-node refresh/sync fan-out off GET /cluster/packages (which was mutating state via refreshPackage/expectedVersion query params) onto POST endpoints: a localOnly flag on the existing /refresh action (mirroring ClusterFileStoreApis.deleteFile's localDelete), and a new /sync action. - getPackage() now returns 404 for an unknown package, matching deletePackageVersion/refreshPackage's existing NOT_FOUND behavior. - Split the single-package GET response into its own flat shape (GetPackageResponse) instead of reusing the list endpoint's map-shaped response filtered to one entry, and renamed both response classes to match the List<Resource>Response / Get<Resource>Response convention used elsewhere (aliases, cluster properties, schema fields). - Extracted the duplicated node-broadcast loop in ClusterPackage into a shared helper. - Documented the List/Get response-naming convention in dev-docs/apis-v2-conventions.adoc. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019kzScQszwRcG4S7kA5Pa8b
PackageManager's deploy --update and undeploy flows were still POSTing a
v1-style JSON command body ({"refresh": "name"}) to /api/cluster/packages,
left over from before the JAX-RS migration. The new endpoint doesn't accept
a body at all - it's POST /cluster/packages/{name}/refresh - so these calls
were silently broken against the migrated server. Switch both call sites to
the generated PackageApi.RefreshPackage client, matching how
DeletePackageVersion/AddPackageVersion were already migrated elsewhere in
this file. Removes the now-unused PackageUtils.PACKAGE_PATH constant.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kzScQszwRcG4S7kA5Pa8b
…kage API shape testPayload()'s @EndPoint/@command fixture (path /cluster/package, an AddVersion bean with pkg/version/files) mirrored the now-deleted v1 Package API almost exactly, which reads as leftover Package API code rather than a generic framework test. Rename to /foos/FooVersion/myfoo so it's obviously just a test fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019kzScQszwRcG4S7kA5Pa8b
ReferenceEquality flagged schemas[0] != schemas[1] as a likely mistake, but the test intentionally checks whether the core picked up a new IndexSchema instance after the package version bump - identity, not value equality, is what's being verified. Suppress with the same pattern used elsewhere in the codebase for intentional identity checks (SolrConfig.java, CachingDirectoryFactory.java). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019kzScQszwRcG4S7kA5Pa8b
…sterPackage addPackageVersion and deletePackageVersion each inlined the same "deserialize packages.json bytes, fall back to a fresh Packages on missing/invalid data, then copy()" block, and the same isEnabled()/BAD_REQUEST guard. Extracted both into PackageStore (parseForUpdate, requireEnabled) since they operate on PackageStore's own data/mapper, and call them from both atomicUpdate lambdas. Also noted, via a one-line comment, why PackageManager's undeploy refresh call site keeps a broader catch(Exception) than its sibling deploy call site (it still wraps a postJsonToSolr call that declares throws Exception). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019kzScQszwRcG4S7kA5Pa8b
|
@jaykay12 would love your review! I think this is ready for merging. |
| /** | ||
| * Holds package data loaded from ZooKeeper ({@code /solr/packages.json}) and manages ZK watchers. | ||
| */ | ||
| public class PackageStore { |
There was a problem hiding this comment.
this class is majorly (PackageAPI - {code no longer needed}), please correct me if i am wrong.
[suggestion] should we be splitting up PRs in those case, so that review stays easier & any unintentional change gets caught.
This PR could have been just pure migration & removal of deprecated stuff from PackageAPI class. In subsequent follow-up this renaming could have been attempted?
This is my pure thought, with this minor change, the PR size would have reduced by atleast 8-9 files.
| @Override | ||
| @PermissionName(PACKAGE_EDIT_PERM) | ||
| public SolrJerseyResponse addPackageVersion( | ||
| String packageName, AddPackageVersionRequestBody requestBody) { |
There was a problem hiding this comment.
should the core logic related to addVersion & deleteVersion be kept in PackageStore or PackageAPI for now (if earlier comment is chosen to be addressed)
that way, this class remains as thin adapter only.
There was a problem hiding this comment.
Agreed, done in 7503001 -- moved the ZK atomicUpdate orchestration for both addVersion and deleteVersion into PackageStore. ClusterPackage now just validates/delegates/broadcasts:
Integer newVersion = packageStore.addVersion(packageName, requestBody);
if (newVersion != null) {
notifyAllNodesToSync(newVersion);
}addVersion() returns the new znode version (or null if the ZK update failed), preserving the original behavior of only notifying other nodes/refreshing local config when the update actually succeeded.
| return response; | ||
| } | ||
|
|
||
| private void syncToVersion(PackageStore packageStore, int expectedVersion) { |
There was a problem hiding this comment.
should this be kept as package private function inside PackageStore?
[reason]
this function mutates - packageStore.pkgs, ideally we can keep that ownership from within packageStore? if this function moves inside packageStore, we can esure package.pkgs is private to that class & no one changes its state from outside other than its own functions.
There was a problem hiding this comment.
Agreed, moved in 7503001. syncToVersion is now a package-private instance method on PackageStore operating on its own pkgs field; ClusterPackage no longer assigns to packageStore.pkgs anywhere (confirmed -- grepped for it). This also makes it consistent with PackageStore's existing refreshPackages() watch handler, which already pairs a pkgs mutation with an immediate refreshPackageConf() call the same way.
| solrClient, PackageUtils.PACKAGE_PATH, "{\"refresh\": \"" + packageName + "\"}"); | ||
| } catch (Exception ex) { | ||
| new PackageApi.RefreshPackage(packageName).process(solrClient); | ||
| } catch (Exception ex) { // postJsonToSolr above declares a plain throws Exception |
There was a problem hiding this comment.
this code comment still valid?
| /** Response for the get-a-specific-package API. */ | ||
| public class GetPackageResponse extends SolrJerseyResponse { | ||
|
|
||
| @JsonProperty("result") |
There was a problem hiding this comment.
[info] why the key name as "result", why not just "packageInfo" here?
| /** Response for the package listing API. */ | ||
| public class ListPackagesResponse extends SolrJerseyResponse { | ||
|
|
||
| @JsonProperty("result") |
There was a problem hiding this comment.
[question] why not "packages" as the key?
| @JsonProperty(required = true) | ||
| public List<String> files; | ||
| @JsonProperty("version") | ||
| @Schema(description = "The version string for this package version.", required = true) |
There was a problem hiding this comment.
thanks, going to look....
There was a problem hiding this comment.
Confirmed via the compiler (-Xlint:deprecation):
AddPackageVersionRequestBody.java:27: warning: [deprecation] required() in Schema has been deprecated
AddPackageVersionRequestBody.java:33: warning: [deprecation] required() in Schema has been deprecated
@Schema(required = true) is deprecated in favor of @Schema(requiredMode = Schema.RequiredMode.REQUIRED). That said, this isn't something to fix just here -- 19 other files under solr/api/src/java/org/apache/solr/client/api/model/ use the exact same required = true attribute, so it's a pre-existing, codebase-wide pattern rather than something this PR introduced. Rather than fix it inconsistently in this one new file, I'll leave it as a follow-up to do as its own small cleanup PR across the whole module.
| } | ||
| if (packageStore.pkgs.znodeVersion >= expectedVersion) { | ||
| if (origVersion < packageStore.pkgs.znodeVersion) { | ||
| coreContainer.getPackageLoader().refreshPackageConf(); |
There was a problem hiding this comment.
Per dev-docs/apis-v2-conventions.adoc's HTTP Methods section: "GET - used for non-mutating (i.e. 'read only') requests... Do not use for requests like queries that change state."
listPackages()/getPackage() are GET endpoints, but when called with expectedVersion they call syncToVersion(), which can reach this refreshPackageConf() call -- reloading plugin instances on this node. That's more than a pure read: it can swap out live plugin instances for concurrent requests being served on this node.
This is a defensible gray area (it's catching the node up to state already committed elsewhere in ZK, not originating a new mutation), but it's worth either a comment explaining the tradeoff, or considering whether the GET path should only re-read from ZK (without the full refreshPackageConf() reload) and leave the reload to the write paths / the explicit sync command.
| Boolean localOnly); | ||
|
|
||
| @POST | ||
| @Path("/sync") |
There was a problem hiding this comment.
Per dev-docs/apis-v2-conventions.adoc's "Unproxied APIs" section: paths like /api/node/key exist specifically to mark operations that only affect the receiving node and must never be proxied. syncPackages() fits that exactly -- it only ever touches the local PackageStore (see ClusterPackage.syncToVersion), and is invoked exclusively via broadcastToLiveNodes()/processWithBaseUrl() targeting one specific node's base URL. The @Operation summary here says as much: "Used internally for inter-node fan-out after a package change; not normally invoked directly."
Recommend POST /api/node/packages/sync instead of /cluster/packages/sync, following the existing /node/{resource} pattern (NodeSystemInfoApi → /node/system, NodeHealthApi → /node/health, and especially GetNodeCommandStatusApi → /node/commands/, the closest precedent as another receiving-node-only, not-cluster-tracked concept) combined with the "command API" convention already used elsewhere in this PR (refreshPackage → .../refresh): packages as the resource, sync as the appended command verb. Only this one endpoint would need to move -- listPackages/getPackage are genuinely cluster-wide, proxy-friendly reads and should stay under /cluster/packages.
jaykay12 asked on PR apache#4178 whether this comment was still valid. Checked git blame: it predates this migration entirely (SOLR-13662, 2021) and was just carried over as incidental diff context. The underlying question was never acted on and isn't related to this PR's scope, so removing it rather than leaving a stale rhetorical TODO in migrated code. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RwsEje8u9apx3SRzissZm
Addresses jaykay12's review comments on PR apache#4178: - addPackageVersion()/deletePackageVersion() embedded ~40 lines of ZK atomicUpdate orchestration directly in ClusterPackage, rather than delegating to PackageStore, which already owns `pkgs` and the relevant ZK helpers (parseForUpdate, handleZkErr). Moved that logic into PackageStore.addVersion()/deleteVersion(), keeping ClusterPackage a thin adapter: validate inputs, delegate, broadcast to other nodes. - syncToVersion() mutated packageStore.pkgs from outside the class that owns it (pkgs has no visibility modifier, so same-package access let ClusterPackage write it directly). Moved it into PackageStore as a package-private instance method operating on its own `pkgs` field. ClusterPackage no longer assigns to packageStore.pkgs anywhere. addVersion() now returns the new znode version (or null if the ZK update failed), preserving the original behavior of only notifying other nodes and refreshing local package config when the update actually succeeded. This also makes PackageStore.addVersion()/syncToVersion() consistent with its own existing refreshPackages() watch handler, which already pairs a `pkgs` mutation with an immediate refreshPackageConf() call. Verified via 6 repeated runs of PackageToolTest/PackageToolPicocliTest (which exercise cross-node package deployment) plus ClusterPackageTest, TestPackages, PackageSchemaReloadTest, ConfigsetPinnedPackageVersionTest, PackageStoreSchemaPluginsTest, and TestContainerPlugin -- all pass. One transient failure during verification reproduced identically on the unmodified baseline, confirming it's pre-existing test flakiness (each node's syncToVersion polls ZK independently, so this reorders only when *this* node refreshes relative to notifying *other* nodes, which doesn't affect their own sync timing). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RwsEje8u9apx3SRzissZm
|
Hey @jaykay12 your feedback was super valuable!!!!! I rethought things based on your input! |
Flatten ListPackagesResponse and GetPackageResponse so "packages"/ "znodeVersion"/"name"/"versions" are top-level fields instead of being nested under a generic "result" wrapper, matching the existing ListCollectionsResponse convention. Updates ClusterPackageTest and the ref guide's example JSON accordingly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RwsEje8u9apx3SRzissZm
TestContainerPlugin, TestPackages, and PackageSchemaReloadTest asserted on the hardcoded JSON path ":result:packages:..." against GET /cluster/packages, which broke once ListPackagesResponse dropped its "result" wrapper. Update the expected paths to ":packages:...". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RwsEje8u9apx3SRzissZm

Migrates
org.apache.solr.pkg.PackageAPIfrom Solr's homegrown@EndPoint/@Commanddispatch to standard JAX-RS annotations, consistent with other V2 APIs (ClusterFileStore,ZookeeperRead,ClusterProperty).This has a pretty hefty update to the patterns, but I thought it was better to do it now while v2 api's are "experimental".
New REST endpoints
The command-based POST pattern is replaced with RESTful routes. Note the collection is now plural (
/cluster/packages), matching the v2 API path conventions used elsewhere:GET /cluster/packageGET /cluster/packagesGET /cluster/package/{name}GET /cluster/packages/{name}(now returns 404 if the package doesn't exist, instead of silently returning an empty result)POST /cluster/package {"add": {"package":"p","version":"v","files":[...]}}POST /cluster/packages/{name}/versionsPOST /cluster/package {"delete": {"pkg":"p","version":"v"}}DELETE /cluster/packages/{name}/versions/{version}POST /cluster/package {"refresh": "name"}POST /cluster/packages/{name}/refreshrefreshfor node-local reload)POST /cluster/packages/{name}/refresh?localOnly=truePOST /cluster/packages/sync?expectedVersion=NThe two internal-only rows replace an earlier version of this PR that smuggled the same inter-node signals onto
GET /cluster/packagesviarefreshPackage/expectedVersionquery params — a GET request shouldn't have side effects.localOnlymirrors the existinglocalDeleteparam onClusterFileStoreApis.deleteFile.Key structural changes
PackageAPI— new JAX-RS implementation (extends JerseyResource implements PackageApis); replaces the oldEdit/Readinner classes (~220 lines removed)PackageStore— renamed from the oldPackageAPI; now clearly a ZK data/state holder (Packages,PkgVersion, ZK watcher), not an endpoint class. THIS WAS A BIG REFACTORINGsolr/apimodule — newPackageApis.java(JAX-RS interface),AddPackageVersionRequestBody.java, and two response types instead of one:ListPackagesResponse(theGET /cluster/packagescollection response, map-keyed by package name) andGetPackageResponse(theGET /cluster/packages/{name}single-resource response, a flat object for just that package) — matching theList<Resource>Response/Get<Resource>Responsenaming and shape convention used byListAliasesResponse/GetAliasByNameResponseand similar pairs elsewhere in the v2 API. OAS generation producesPackageApiSolrJ request classes automatically.PackageAPITest— integration test usingSolrCloudTestCase(ZK required) with generatedPackageApiSolrJ request classesTestPackages— updated to use new endpoints; error path changed from/details[0]/errorMessages[0]to/msgpackage-manager-internals.adoc— updated endpoint reference and curl examplesdev-docs/apis-v2-conventions.adoc— added a new "Listing vs. Single-Resource Responses" section documenting theList<Resource>Response/Get<Resource>Responseconvention, since this PR is the first to need it spelled out explicitly