-
Notifications
You must be signed in to change notification settings - Fork 0
Add a register endpoint to upload zk metadata directly #145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -71,6 +71,9 @@ | |||||||||||||||||||
| import org.apache.commons.lang3.StringUtils; | ||||||||||||||||||||
| import org.apache.commons.lang3.tuple.ImmutablePair; | ||||||||||||||||||||
| import org.apache.commons.lang3.tuple.Pair; | ||||||||||||||||||||
| import org.apache.helix.model.IdealState; | ||||||||||||||||||||
| import org.apache.helix.zookeeper.datamodel.ZNRecord; | ||||||||||||||||||||
| import org.apache.pinot.common.metadata.segment.SegmentZKMetadata; | ||||||||||||||||||||
| import org.apache.pinot.common.metrics.ControllerGauge; | ||||||||||||||||||||
| import org.apache.pinot.common.metrics.ControllerMeter; | ||||||||||||||||||||
| import org.apache.pinot.common.metrics.ControllerMetrics; | ||||||||||||||||||||
|
|
@@ -986,6 +989,135 @@ public void uploadSegmentAsMultiPartV2(FormDataMultiPart multiPart, | |||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @POST | ||||||||||||||||||||
| @ManagedAsync | ||||||||||||||||||||
| @Produces(MediaType.APPLICATION_JSON) | ||||||||||||||||||||
| @Consumes(MediaType.APPLICATION_JSON) | ||||||||||||||||||||
| @Path("/segments/register") | ||||||||||||||||||||
| @Authorize(targetType = TargetType.TABLE, paramName = "tableName", action = Actions.Table.UPLOAD_SEGMENT) | ||||||||||||||||||||
| @Authenticate(AccessType.CREATE) | ||||||||||||||||||||
| @ApiOperation(value = "Register a segment from pre-built ZK metadata", | ||||||||||||||||||||
| notes = "Registers a segment directly from its ZK metadata JSON without file upload, unpacking, or copying. " | ||||||||||||||||||||
| + "The segment files must already exist at the downloadUrl in the ZK metadata. " | ||||||||||||||||||||
| + "Request body is SegmentZKMetadata JSON: {\"segmentName\": \"...\", \"simpleFields\": {...}}. " | ||||||||||||||||||||
| + "Required simpleFields: segment.download.url, segment.crc. " | ||||||||||||||||||||
| + "Optional but recommended: segment.total.docs, segment.start.time, segment.end.time, " | ||||||||||||||||||||
| + "segment.time.unit, segment.index.version, segment.creation.time.") | ||||||||||||||||||||
| @ApiResponses(value = { | ||||||||||||||||||||
| @ApiResponse(code = 200, message = "Successfully registered segment"), | ||||||||||||||||||||
| @ApiResponse(code = 400, message = "Bad Request"), | ||||||||||||||||||||
| @ApiResponse(code = 409, message = "Segment already exists and refresh not permitted"), | ||||||||||||||||||||
| @ApiResponse(code = 500, message = "Internal error") | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| @TrackInflightRequestMetrics | ||||||||||||||||||||
| @TrackedByGauge(gauge = ControllerGauge.SEGMENT_UPLOADS_IN_PROGRESS) | ||||||||||||||||||||
| public void registerSegment( | ||||||||||||||||||||
| String segmentZKMetadataJson, | ||||||||||||||||||||
| @ApiParam(value = "Name of the table", required = true) | ||||||||||||||||||||
| @QueryParam(FileUploadDownloadClient.QueryParameters.TABLE_NAME) String tableName, | ||||||||||||||||||||
| @ApiParam(value = "Type of the table") | ||||||||||||||||||||
| @QueryParam(FileUploadDownloadClient.QueryParameters.TABLE_TYPE) @DefaultValue("OFFLINE") String tableType, | ||||||||||||||||||||
| @ApiParam(value = "Whether to refresh if the segment already exists") | ||||||||||||||||||||
| @QueryParam(FileUploadDownloadClient.QueryParameters.ALLOW_REFRESH) @DefaultValue("true") boolean allowRefresh, | ||||||||||||||||||||
| @Suspended final AsyncResponse asyncResponse) { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| asyncResponse.resume(registerSegmentInternal(segmentZKMetadataJson, tableName, tableType, allowRefresh)); | ||||||||||||||||||||
| } catch (Throwable t) { | ||||||||||||||||||||
| asyncResponse.resume(t); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private SuccessResponse registerSegmentInternal(String segmentZKMetadataJson, String tableName, String tableType, | ||||||||||||||||||||
| boolean allowRefresh) { | ||||||||||||||||||||
| if (StringUtils.isEmpty(tableName)) { | ||||||||||||||||||||
| throw new ControllerApplicationException(LOGGER, "tableName is required", Response.Status.BAD_REQUEST); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| SegmentZKMetadata segmentZKMetadata; | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| segmentZKMetadata = SegmentZKMetadata.fromJsonString(segmentZKMetadataJson); | ||||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||||
| throw new ControllerApplicationException(LOGGER, | ||||||||||||||||||||
| "Failed to parse segment ZK metadata: " + e.getMessage(), Response.Status.BAD_REQUEST); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| String segmentName = segmentZKMetadata.getSegmentName(); | ||||||||||||||||||||
| if (StringUtils.isEmpty(segmentName)) { | ||||||||||||||||||||
| throw new ControllerApplicationException(LOGGER, "segmentName is required in ZK metadata", | ||||||||||||||||||||
| Response.Status.BAD_REQUEST); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if (StringUtils.isEmpty(segmentZKMetadata.getDownloadUrl())) { | ||||||||||||||||||||
| throw new ControllerApplicationException(LOGGER, | ||||||||||||||||||||
| "segment.download.url is required in simpleFields", Response.Status.BAD_REQUEST); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| String rawTableName = TableNameBuilder.extractRawTableName(tableName); | ||||||||||||||||||||
| TableType type = TableType.valueOf(tableType.toUpperCase()); | ||||||||||||||||||||
| String tableNameWithType = TableNameBuilder.forType(type).tableNameWithType(rawTableName); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| TableConfig tableConfig = _pinotHelixResourceManager.getTableConfig(tableNameWithType); | ||||||||||||||||||||
| if (tableConfig == null) { | ||||||||||||||||||||
| throw new ControllerApplicationException(LOGGER, "Failed to find table: " + tableNameWithType, | ||||||||||||||||||||
| Response.Status.BAD_REQUEST); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| LOGGER.info("Registering segment: {} for table: {}", segmentName, tableNameWithType); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ZNRecord existingZNRecord = _pinotHelixResourceManager.getSegmentMetadataZnRecord(tableNameWithType, segmentName); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // If ZK metadata exists but the segment is absent from the ideal state, a previous registration failed | ||||||||||||||||||||
| // partway through. Remove the stale ZK entry and treat this as a new segment. | ||||||||||||||||||||
| if (existingZNRecord != null) { | ||||||||||||||||||||
| IdealState idealState = _pinotHelixResourceManager.getTableIdealState(tableNameWithType); | ||||||||||||||||||||
| Preconditions.checkState(idealState != null, "Failed to find ideal state for table: %s", tableNameWithType); | ||||||||||||||||||||
| if (idealState.getInstanceStateMap(segmentName) == null) { | ||||||||||||||||||||
| LOGGER.warn("Removing stale segment ZK metadata (recovering from previous registration failure) " | ||||||||||||||||||||
| + "for table: {}, segment: {}", tableNameWithType, segmentName); | ||||||||||||||||||||
| Preconditions.checkState(_pinotHelixResourceManager.removeSegmentZKMetadata(tableNameWithType, segmentName), | ||||||||||||||||||||
| "Failed to remove stale segment ZK metadata for table: %s, segment: %s", tableNameWithType, segmentName); | ||||||||||||||||||||
| existingZNRecord = null; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (existingZNRecord != null) { | ||||||||||||||||||||
| if (!allowRefresh) { | ||||||||||||||||||||
| throw new ControllerApplicationException(LOGGER, | ||||||||||||||||||||
| String.format("Segment: %s already exists in table: %s. Refresh not permitted.", segmentName, | ||||||||||||||||||||
| tableNameWithType), Response.Status.CONFLICT); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| LOGGER.info("Segment: {} already exists in table: {}, refreshing", segmentName, tableNameWithType); | ||||||||||||||||||||
| segmentZKMetadata.setRefreshTime(System.currentTimeMillis()); | ||||||||||||||||||||
| if (!_pinotHelixResourceManager.updateZkMetadata(tableNameWithType, segmentZKMetadata, | ||||||||||||||||||||
|
Comment on lines
+1089
to
+1090
|
||||||||||||||||||||
| segmentZKMetadata.setRefreshTime(System.currentTimeMillis()); | |
| if (!_pinotHelixResourceManager.updateZkMetadata(tableNameWithType, segmentZKMetadata, | |
| // Merge request-provided fields into existing ZK metadata instead of overwriting the whole record. | |
| SegmentZKMetadata mergedSegmentZKMetadata = new SegmentZKMetadata(existingZNRecord); | |
| // Whitelist fields that are allowed to change on refresh. | |
| mergedSegmentZKMetadata.setDownloadUrl(segmentZKMetadata.getDownloadUrl()); | |
| mergedSegmentZKMetadata.setCrc(segmentZKMetadata.getCrc()); | |
| mergedSegmentZKMetadata.setRefreshTime(System.currentTimeMillis()); | |
| if (!_pinotHelixResourceManager.updateZkMetadata(tableNameWithType, mergedSegmentZKMetadata, |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new endpoint introduces non-trivial registration/refresh behavior (stale-ZK cleanup, create vs update, Helix assignment). The existing test suite for this class only covers helper methods; please add unit tests for registerSegmentInternal covering: new registration, refresh without wiping existing metadata, allowRefresh=false (409), and stale-ZK cleanup when IdealState is missing the segment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The swagger notes state that
segment.crcis required, but the implementation only validatessegment.download.url. Ifcrcis missing, SegmentZKMetadata#getCrc() will be -1 and downstream refresh/download logic can behave incorrectly. Please validate thatsegmentZKMetadata.getCrc()is present/valid (>= 0) and return 400 if not.