Jackie-Jiang commented on code in PR #19180:
URL: https://github.com/apache/pinot/pull/19180#discussion_r4251542468
##########
pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSegmentRestletResource.java:
##########
@@ -317,6 +334,134 @@ public Map<String, Object> getSegmentMetadata(
}
}
+ @PUT
+ @Path("segments/{tableNameWithType}/{segmentName}/metadata")
+ @Authorize(targetType = TargetType.TABLE, paramName = "tableNameWithType",
action = Actions.Table.UPLOAD_SEGMENT)
+ @Authenticate(AccessType.UPDATE)
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(value = "Update the custom map in the ZK metadata for a
segment",
+ notes = "Updates only the segment ZK metadata custom map without
uploading or refreshing the segment")
+ @ApiResponses(value = {
+ @ApiResponse(code = 200, message = "Success"),
+ @ApiResponse(code = 400, message = "Invalid table name, CRC, or custom
map modifier"),
+ @ApiResponse(code = 404, message = "Table or segment not found"),
+ @ApiResponse(code = 409, message = "Segment metadata changed
concurrently"),
+ @ApiResponse(code = 412, message = "Segment CRC does not match"),
+ @ApiResponse(code = 413, message = "Custom map modifier or resulting ZK
metadata is too large")
+ })
+ public SuccessResponse updateSegmentZKMetadataCustomMap(
+ @ApiParam(value = "Table name with type", required = true, example =
"myTable_OFFLINE")
+ @PathParam("tableNameWithType") String tableNameWithType,
+ @ApiParam(value = "Name of the segment", required = true)
@PathParam("segmentName") @Encoded String segmentName,
+ @ApiParam(value = "Expected segment CRC", required = true)
@HeaderParam(HttpHeaders.IF_MATCH)
+ String expectedCrcString,
+ @ApiParam(value = "Custom map modifier", required = true) InputStream
customMapModifierInputStream,
+ @Context HttpHeaders headers) {
+ tableNameWithType = DatabaseUtils.translateTableName(tableNameWithType,
headers);
+ segmentName = decodePathSegment(segmentName);
+ if (tableNameWithType.contains("/") || tableNameWithType.contains("\\") ||
segmentName.isEmpty()
+ || segmentName.contains("/") || segmentName.contains("\\")) {
+ throw new ControllerApplicationException(LOGGER, "Invalid table or
segment path", Status.BAD_REQUEST);
+ }
+ if (TableNameBuilder.getTableTypeFromTableName(tableNameWithType) == null)
{
+ throw new ControllerApplicationException(LOGGER,
+ String.format("Table type not provided with table name: %s",
tableNameWithType), Status.BAD_REQUEST);
+ }
+ String customMapModifierJson =
readCustomMapModifier(customMapModifierInputStream);
+
+ long expectedCrc;
+ try {
+ expectedCrc = Long.parseLong(expectedCrcString);
+ } catch (Exception e) {
+ throw new ControllerApplicationException(LOGGER, "Missing or invalid
If-Match segment CRC", Status.BAD_REQUEST,
+ e);
+ }
+
+ SegmentZKMetadataCustomMapModifier customMapModifier;
+ try {
+ customMapModifier = new
SegmentZKMetadataCustomMapModifier(customMapModifierJson);
+ } catch (Exception e) {
+ throw new ControllerApplicationException(LOGGER, "Invalid segment ZK
metadata custom map modifier",
+ Status.BAD_REQUEST, e);
+ }
+
+ ZNRecord segmentMetadataRecord =
+
_pinotHelixResourceManager.getSegmentMetadataZnRecord(tableNameWithType,
segmentName);
+ if (segmentMetadataRecord == null) {
+ throw new ControllerApplicationException(LOGGER,
+ String.format("Failed to find segment: %s in table: %s",
segmentName, tableNameWithType), Status.NOT_FOUND);
+ }
+ SegmentZKMetadata segmentZKMetadata = new
SegmentZKMetadata(segmentMetadataRecord);
+ if (segmentZKMetadata.getSegmentUploadStartTime() > 0) {
+ throw new ControllerApplicationException(LOGGER,
+ String.format("Segment upload is in progress for segment: %s in
table: %s", segmentName,
+ tableNameWithType), Status.CONFLICT);
+ }
+ if (segmentZKMetadata.getCrc() != expectedCrc) {
+ throw new ControllerApplicationException(LOGGER,
+ String.format("Segment CRC does not match for segment: %s in table:
%s", segmentName, tableNameWithType),
+ Status.PRECONDITION_FAILED);
+ }
+
segmentZKMetadata.setCustomMap(customMapModifier.modifyMap(segmentZKMetadata.getCustomMap()));
+ validateSegmentZKMetadataSize(segmentZKMetadata);
+ if
(!_pinotHelixResourceManager.updateZkMetadataWithoutDataChange(tableNameWithType,
segmentZKMetadata,
+ segmentMetadataRecord.getVersion())) {
+ throw new ControllerApplicationException(LOGGER,
+ String.format("Segment metadata changed concurrently for segment: %s
in table: %s", segmentName,
+ tableNameWithType), Status.CONFLICT);
+ }
+ return new SuccessResponse(
+ String.format("Successfully updated ZK metadata for segment: %s in
table: %s", segmentName,
+ tableNameWithType));
+ }
+
+ private static String readCustomMapModifier(@Nullable InputStream
inputStream) {
+ if (inputStream == null) {
+ throw new ControllerApplicationException(LOGGER, "Missing segment ZK
metadata custom map modifier",
+ Status.BAD_REQUEST);
+ }
+ try {
+ byte[] bytes = ByteStreams.toByteArray(ByteStreams.limit(inputStream,
MAX_CUSTOM_MAP_MODIFIER_SIZE_BYTES + 1L));
Review Comment:
Non-blocking question: what is the basis for the new 64 KiB request limit? A
bounded read is reasonable, but this is a new API-specific limit, separate from
the size limit on the resulting ZK record, and existing upload callers do not
use this helper. Could we document the supported request limit and explain why
64 KiB is appropriate, or reuse a controller request-size policy if one
applies? JSON shape/type validation should remain regardless.
##########
pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSegmentRestletResource.java:
##########
@@ -317,6 +334,134 @@ public Map<String, Object> getSegmentMetadata(
}
}
+ @PUT
+ @Path("segments/{tableNameWithType}/{segmentName}/metadata")
+ @Authorize(targetType = TargetType.TABLE, paramName = "tableNameWithType",
action = Actions.Table.UPLOAD_SEGMENT)
+ @Authenticate(AccessType.UPDATE)
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(value = "Update the custom map in the ZK metadata for a
segment",
+ notes = "Updates only the segment ZK metadata custom map without
uploading or refreshing the segment")
+ @ApiResponses(value = {
+ @ApiResponse(code = 200, message = "Success"),
+ @ApiResponse(code = 400, message = "Invalid table name, CRC, or custom
map modifier"),
+ @ApiResponse(code = 404, message = "Table or segment not found"),
+ @ApiResponse(code = 409, message = "Segment metadata changed
concurrently"),
+ @ApiResponse(code = 412, message = "Segment CRC does not match"),
+ @ApiResponse(code = 413, message = "Custom map modifier or resulting ZK
metadata is too large")
+ })
+ public SuccessResponse updateSegmentZKMetadataCustomMap(
+ @ApiParam(value = "Table name with type", required = true, example =
"myTable_OFFLINE")
+ @PathParam("tableNameWithType") String tableNameWithType,
+ @ApiParam(value = "Name of the segment", required = true)
@PathParam("segmentName") @Encoded String segmentName,
+ @ApiParam(value = "Expected segment CRC", required = true)
@HeaderParam(HttpHeaders.IF_MATCH)
+ String expectedCrcString,
+ @ApiParam(value = "Custom map modifier", required = true) InputStream
customMapModifierInputStream,
+ @Context HttpHeaders headers) {
+ tableNameWithType = DatabaseUtils.translateTableName(tableNameWithType,
headers);
+ segmentName = decodePathSegment(segmentName);
+ if (tableNameWithType.contains("/") || tableNameWithType.contains("\\") ||
segmentName.isEmpty()
Review Comment:
Non-blocking question: can the table/segment path checks reuse shared
validation, or can we document why a local check is needed? Rejecting '/'
before constructing the ZK path has a concrete safety purpose and should be
retained. The empty-name and backslash rules are additional name policy
introduced only for this API; existing metadata GET/reset paths do not have
this same guard. Please clarify the intended contract and whether this belongs
in a shared path-component validator, taking care to preserve valid
existing/realtime segment names. A broad cleanup of existing endpoints need not
block this API.
##########
pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSegmentRestletResource.java:
##########
@@ -317,6 +334,134 @@ public Map<String, Object> getSegmentMetadata(
}
}
+ @PUT
+ @Path("segments/{tableNameWithType}/{segmentName}/metadata")
+ @Authorize(targetType = TargetType.TABLE, paramName = "tableNameWithType",
action = Actions.Table.UPLOAD_SEGMENT)
+ @Authenticate(AccessType.UPDATE)
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(value = "Update the custom map in the ZK metadata for a
segment",
+ notes = "Updates only the segment ZK metadata custom map without
uploading or refreshing the segment")
+ @ApiResponses(value = {
+ @ApiResponse(code = 200, message = "Success"),
+ @ApiResponse(code = 400, message = "Invalid table name, CRC, or custom
map modifier"),
+ @ApiResponse(code = 404, message = "Table or segment not found"),
+ @ApiResponse(code = 409, message = "Segment metadata changed
concurrently"),
+ @ApiResponse(code = 412, message = "Segment CRC does not match"),
+ @ApiResponse(code = 413, message = "Custom map modifier or resulting ZK
metadata is too large")
+ })
+ public SuccessResponse updateSegmentZKMetadataCustomMap(
+ @ApiParam(value = "Table name with type", required = true, example =
"myTable_OFFLINE")
+ @PathParam("tableNameWithType") String tableNameWithType,
+ @ApiParam(value = "Name of the segment", required = true)
@PathParam("segmentName") @Encoded String segmentName,
+ @ApiParam(value = "Expected segment CRC", required = true)
@HeaderParam(HttpHeaders.IF_MATCH)
+ String expectedCrcString,
+ @ApiParam(value = "Custom map modifier", required = true) InputStream
customMapModifierInputStream,
+ @Context HttpHeaders headers) {
+ tableNameWithType = DatabaseUtils.translateTableName(tableNameWithType,
headers);
+ segmentName = decodePathSegment(segmentName);
+ if (tableNameWithType.contains("/") || tableNameWithType.contains("\\") ||
segmentName.isEmpty()
+ || segmentName.contains("/") || segmentName.contains("\\")) {
+ throw new ControllerApplicationException(LOGGER, "Invalid table or
segment path", Status.BAD_REQUEST);
+ }
+ if (TableNameBuilder.getTableTypeFromTableName(tableNameWithType) == null)
{
+ throw new ControllerApplicationException(LOGGER,
+ String.format("Table type not provided with table name: %s",
tableNameWithType), Status.BAD_REQUEST);
+ }
+ String customMapModifierJson =
readCustomMapModifier(customMapModifierInputStream);
+
+ long expectedCrc;
+ try {
+ expectedCrc = Long.parseLong(expectedCrcString);
+ } catch (Exception e) {
+ throw new ControllerApplicationException(LOGGER, "Missing or invalid
If-Match segment CRC", Status.BAD_REQUEST,
+ e);
+ }
+
+ SegmentZKMetadataCustomMapModifier customMapModifier;
+ try {
+ customMapModifier = new
SegmentZKMetadataCustomMapModifier(customMapModifierJson);
+ } catch (Exception e) {
+ throw new ControllerApplicationException(LOGGER, "Invalid segment ZK
metadata custom map modifier",
+ Status.BAD_REQUEST, e);
+ }
+
+ ZNRecord segmentMetadataRecord =
+
_pinotHelixResourceManager.getSegmentMetadataZnRecord(tableNameWithType,
segmentName);
+ if (segmentMetadataRecord == null) {
+ throw new ControllerApplicationException(LOGGER,
+ String.format("Failed to find segment: %s in table: %s",
segmentName, tableNameWithType), Status.NOT_FOUND);
+ }
+ SegmentZKMetadata segmentZKMetadata = new
SegmentZKMetadata(segmentMetadataRecord);
+ if (segmentZKMetadata.getSegmentUploadStartTime() > 0) {
+ throw new ControllerApplicationException(LOGGER,
+ String.format("Segment upload is in progress for segment: %s in
table: %s", segmentName,
+ tableNameWithType), Status.CONFLICT);
+ }
+ if (segmentZKMetadata.getCrc() != expectedCrc) {
+ throw new ControllerApplicationException(LOGGER,
+ String.format("Segment CRC does not match for segment: %s in table:
%s", segmentName, tableNameWithType),
+ Status.PRECONDITION_FAILED);
+ }
+
segmentZKMetadata.setCustomMap(customMapModifier.modifyMap(segmentZKMetadata.getCustomMap()));
+ validateSegmentZKMetadataSize(segmentZKMetadata);
+ if
(!_pinotHelixResourceManager.updateZkMetadataWithoutDataChange(tableNameWithType,
segmentZKMetadata,
+ segmentMetadataRecord.getVersion())) {
+ throw new ControllerApplicationException(LOGGER,
+ String.format("Segment metadata changed concurrently for segment: %s
in table: %s", segmentName,
+ tableNameWithType), Status.CONFLICT);
+ }
+ return new SuccessResponse(
+ String.format("Successfully updated ZK metadata for segment: %s in
table: %s", segmentName,
+ tableNameWithType));
+ }
+
+ private static String readCustomMapModifier(@Nullable InputStream
inputStream) {
+ if (inputStream == null) {
+ throw new ControllerApplicationException(LOGGER, "Missing segment ZK
metadata custom map modifier",
+ Status.BAD_REQUEST);
+ }
+ try {
+ byte[] bytes = ByteStreams.toByteArray(ByteStreams.limit(inputStream,
MAX_CUSTOM_MAP_MODIFIER_SIZE_BYTES + 1L));
+ if (bytes.length > MAX_CUSTOM_MAP_MODIFIER_SIZE_BYTES) {
+ throw new ControllerApplicationException(LOGGER, "Segment ZK metadata
custom map modifier is too large",
+ Status.REQUEST_ENTITY_TOO_LARGE);
+ }
+ return new String(bytes, StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new ControllerApplicationException(LOGGER, "Failed to read segment
ZK metadata custom map modifier",
+ Status.BAD_REQUEST, e);
+ }
+ }
+
+ private static String decodePathSegment(String pathSegment) {
+ // URIUtils uses form decoding, where '+' represents a space. In a path,
'+' is a literal character. Jersey can
+ // supply either the raw or decoded path parameter depending on the
request URI, so protect literal plus signs
+ // before decoding percent escapes.
+ return URIUtils.decode(pathSegment.replace("+", "%2B"));
Review Comment:
Non-blocking question: why does this endpoint need different segment-name
decoding from the existing metadata GET/reset endpoints? Those use @Encoded
with URIUtils.decode; this helper additionally preserves a raw '+'. A correctly
encoded literal plus (%2B) already decodes correctly with URIUtils.decode, and
the new client helper sends %2B. Please explain the request/Jersey case that
requires the extra replacement, particularly the claim that an @Encoded
parameter can already be decoded. If path decoding needs a general fix, could
we use a shared path-specific utility with focused tests for raw '+', %2B, %20,
and %25, while checking compatibility of existing endpoints? The client's fix
to encode each component only once should be preserved.
##########
pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSegmentRestletResource.java:
##########
@@ -317,6 +334,134 @@ public Map<String, Object> getSegmentMetadata(
}
}
+ @PUT
+ @Path("segments/{tableNameWithType}/{segmentName}/metadata")
+ @Authorize(targetType = TargetType.TABLE, paramName = "tableNameWithType",
action = Actions.Table.UPLOAD_SEGMENT)
+ @Authenticate(AccessType.UPDATE)
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(value = "Update the custom map in the ZK metadata for a
segment",
+ notes = "Updates only the segment ZK metadata custom map without
uploading or refreshing the segment")
+ @ApiResponses(value = {
+ @ApiResponse(code = 200, message = "Success"),
+ @ApiResponse(code = 400, message = "Invalid table name, CRC, or custom
map modifier"),
+ @ApiResponse(code = 404, message = "Table or segment not found"),
+ @ApiResponse(code = 409, message = "Segment metadata changed
concurrently"),
+ @ApiResponse(code = 412, message = "Segment CRC does not match"),
+ @ApiResponse(code = 413, message = "Custom map modifier or resulting ZK
metadata is too large")
+ })
+ public SuccessResponse updateSegmentZKMetadataCustomMap(
+ @ApiParam(value = "Table name with type", required = true, example =
"myTable_OFFLINE")
+ @PathParam("tableNameWithType") String tableNameWithType,
+ @ApiParam(value = "Name of the segment", required = true)
@PathParam("segmentName") @Encoded String segmentName,
+ @ApiParam(value = "Expected segment CRC", required = true)
@HeaderParam(HttpHeaders.IF_MATCH)
+ String expectedCrcString,
+ @ApiParam(value = "Custom map modifier", required = true) InputStream
customMapModifierInputStream,
+ @Context HttpHeaders headers) {
+ tableNameWithType = DatabaseUtils.translateTableName(tableNameWithType,
headers);
+ segmentName = decodePathSegment(segmentName);
+ if (tableNameWithType.contains("/") || tableNameWithType.contains("\\") ||
segmentName.isEmpty()
+ || segmentName.contains("/") || segmentName.contains("\\")) {
+ throw new ControllerApplicationException(LOGGER, "Invalid table or
segment path", Status.BAD_REQUEST);
+ }
+ if (TableNameBuilder.getTableTypeFromTableName(tableNameWithType) == null)
{
+ throw new ControllerApplicationException(LOGGER,
+ String.format("Table type not provided with table name: %s",
tableNameWithType), Status.BAD_REQUEST);
+ }
+ String customMapModifierJson =
readCustomMapModifier(customMapModifierInputStream);
+
+ long expectedCrc;
+ try {
+ expectedCrc = Long.parseLong(expectedCrcString);
+ } catch (Exception e) {
+ throw new ControllerApplicationException(LOGGER, "Missing or invalid
If-Match segment CRC", Status.BAD_REQUEST,
+ e);
+ }
+
+ SegmentZKMetadataCustomMapModifier customMapModifier;
+ try {
+ customMapModifier = new
SegmentZKMetadataCustomMapModifier(customMapModifierJson);
+ } catch (Exception e) {
+ throw new ControllerApplicationException(LOGGER, "Invalid segment ZK
metadata custom map modifier",
+ Status.BAD_REQUEST, e);
+ }
+
+ ZNRecord segmentMetadataRecord =
+
_pinotHelixResourceManager.getSegmentMetadataZnRecord(tableNameWithType,
segmentName);
+ if (segmentMetadataRecord == null) {
+ throw new ControllerApplicationException(LOGGER,
+ String.format("Failed to find segment: %s in table: %s",
segmentName, tableNameWithType), Status.NOT_FOUND);
+ }
+ SegmentZKMetadata segmentZKMetadata = new
SegmentZKMetadata(segmentMetadataRecord);
+ if (segmentZKMetadata.getSegmentUploadStartTime() > 0) {
+ throw new ControllerApplicationException(LOGGER,
+ String.format("Segment upload is in progress for segment: %s in
table: %s", segmentName,
+ tableNameWithType), Status.CONFLICT);
+ }
+ if (segmentZKMetadata.getCrc() != expectedCrc) {
+ throw new ControllerApplicationException(LOGGER,
+ String.format("Segment CRC does not match for segment: %s in table:
%s", segmentName, tableNameWithType),
+ Status.PRECONDITION_FAILED);
+ }
+
segmentZKMetadata.setCustomMap(customMapModifier.modifyMap(segmentZKMetadata.getCustomMap()));
+ validateSegmentZKMetadataSize(segmentZKMetadata);
+ if
(!_pinotHelixResourceManager.updateZkMetadataWithoutDataChange(tableNameWithType,
segmentZKMetadata,
+ segmentMetadataRecord.getVersion())) {
+ throw new ControllerApplicationException(LOGGER,
+ String.format("Segment metadata changed concurrently for segment: %s
in table: %s", segmentName,
+ tableNameWithType), Status.CONFLICT);
+ }
+ return new SuccessResponse(
+ String.format("Successfully updated ZK metadata for segment: %s in
table: %s", segmentName,
+ tableNameWithType));
+ }
+
+ private static String readCustomMapModifier(@Nullable InputStream
inputStream) {
+ if (inputStream == null) {
+ throw new ControllerApplicationException(LOGGER, "Missing segment ZK
metadata custom map modifier",
+ Status.BAD_REQUEST);
+ }
+ try {
+ byte[] bytes = ByteStreams.toByteArray(ByteStreams.limit(inputStream,
MAX_CUSTOM_MAP_MODIFIER_SIZE_BYTES + 1L));
+ if (bytes.length > MAX_CUSTOM_MAP_MODIFIER_SIZE_BYTES) {
+ throw new ControllerApplicationException(LOGGER, "Segment ZK metadata
custom map modifier is too large",
+ Status.REQUEST_ENTITY_TOO_LARGE);
+ }
+ return new String(bytes, StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new ControllerApplicationException(LOGGER, "Failed to read segment
ZK metadata custom map modifier",
+ Status.BAD_REQUEST, e);
+ }
+ }
+
+ private static String decodePathSegment(String pathSegment) {
+ // URIUtils uses form decoding, where '+' represents a space. In a path,
'+' is a literal character. Jersey can
+ // supply either the raw or decoded path parameter depending on the
request URI, so protect literal plus signs
+ // before decoding percent escapes.
+ return URIUtils.decode(pathSegment.replace("+", "%2B"));
+ }
+
+ private static void validateSegmentZKMetadataSize(SegmentZKMetadata
segmentZKMetadata) {
+ int zkJuteMaxBufferSizeBytes =
+ Integer.getInteger(ZkSystemPropertyKeys.JUTE_MAXBUFFER,
DEFAULT_ZK_JUTE_MAX_BUFFER_SIZE_BYTES);
+ if (zkJuteMaxBufferSizeBytes <= 0) {
+ zkJuteMaxBufferSizeBytes = DEFAULT_ZK_JUTE_MAX_BUFFER_SIZE_BYTES;
+ }
+ int safeJuteRecordSizeBytes = (int) ((long) zkJuteMaxBufferSizeBytes *
ZK_RECORD_SIZE_LIMIT_PERCENT / 100);
Review Comment:
Non-blocking question: do we need a separate full-record size policy here?
The normal Helix write already serializes the ZNRecord and enforces its
serializer write limit, accounting for compression. This precheck serializes it
again before the actual write and additionally caps it at 90% of
jute.maxbuffer, making this endpoint stricter than existing metadata writes.
What failure case requires the 10% reserve and early serialization? If the goal
is only to return a clear 413, could we rely on the existing limit and
translate the size failure appropriately? If the extra policy is necessary,
please document the rationale and consider shared enforcement. Removing the
precheck should not turn an oversized request into a misleading 409 concurrency
error.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]