This is an automated email from the ASF dual-hosted git repository. sammichen pushed a commit to branch HDDS-8342 in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/HDDS-8342 by this push: new 1fe66180880 HDDS-13548. Prevent OM startup failure from past lifecycle expiration dates (#8953) 1fe66180880 is described below commit 1fe661808805f2acf33cda6025148b3281cf3fe3 Author: XiChen <32928346+xiche...@users.noreply.github.com> AuthorDate: Wed Sep 3 12:55:05 2025 +0800 HDDS-13548. Prevent OM startup failure from past lifecycle expiration dates (#8953) --- .../apache/hadoop/ozone/om/helpers/OmLCAction.java | 3 +- .../hadoop/ozone/om/helpers/OmLCExpiration.java | 21 ++++---- .../apache/hadoop/ozone/om/helpers/OmLCFilter.java | 10 +--- .../apache/hadoop/ozone/om/helpers/OmLCRule.java | 18 +++---- .../ozone/om/helpers/OmLifecycleConfiguration.java | 16 +++--- .../om/helpers/OmLifecycleRuleAndOperator.java | 9 +--- .../ozone/om/helpers/TestOmLCExpiration.java | 63 +++++++++++----------- .../hadoop/ozone/om/helpers/TestOmLCFilter.java | 15 +++--- .../hadoop/ozone/om/helpers/TestOmLCRule.java | 22 +++++--- .../om/helpers/TestOmLifeCycleConfiguration.java | 48 +++++++++++++++-- .../om/helpers/TestOmLifecycleRuleAndOperator.java | 8 +-- .../src/main/proto/OmClientProtocol.proto | 2 +- .../apache/hadoop/ozone/om/OMMetadataManager.java | 3 +- .../hadoop/ozone/om/OmMetadataManagerImpl.java | 13 ++--- .../OMLifecycleConfigurationSetRequest.java | 3 +- .../ozone/om/service/KeyLifecycleService.java | 17 +++++- .../TestOMLifecycleConfigurationRequest.java | 1 + .../ozone/om/service/TestKeyLifecycleService.java | 2 +- .../hadoop/ozone/s3/endpoint/BucketEndpoint.java | 4 +- .../s3/endpoint/S3LifecycleConfiguration.java | 2 +- 20 files changed, 168 insertions(+), 112 deletions(-) diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCAction.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCAction.java index 427bf87908b..4db13e20743 100644 --- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCAction.java +++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCAction.java @@ -36,9 +36,10 @@ public interface OmLCAction { * Validates the action configuration. * Each concrete action implementation must define its own validation logic. * + * @param creationTime The creation time of the lifecycle configuration in milliseconds since epoch * @throws OMException if the validation fails */ - void valid() throws OMException; + void valid(long creationTime) throws OMException; /** * Returns the action type. diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCExpiration.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCExpiration.java index bb15b447691..b5e78eb6241 100644 --- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCExpiration.java +++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCExpiration.java @@ -87,10 +87,11 @@ public ActionType getActionType() { * - The date value must be in the future * - The date value must be at midnight UTC (00:00:00Z) * + * @param creationTime The creation time of the lifecycle configuration in milliseconds since epoch * @throws OMException if the validation fails */ @Override - public void valid() throws OMException { + public void valid(long creationTime) throws OMException { boolean hasDays = days != null; boolean hasDate = !StringUtils.isBlank(date); @@ -106,7 +107,7 @@ public void valid() throws OMException { daysInMilli = TimeUnit.DAYS.toMillis(days); } if (hasDate) { - validateExpirationDate(date); + validateExpirationDate(date, creationTime); } } @@ -118,19 +119,21 @@ public void valid() throws OMException { * - Represents midnight UTC (00:00:00Z) when converted to UTC. * * @param expirationDate The date string to validate + * @param creationTime The creation time to compare against in milliseconds since epoch * @throws OMException if the date is invalid */ - private void validateExpirationDate(String expirationDate) throws OMException { + private void validateExpirationDate(String expirationDate, long creationTime) throws OMException { try { ZonedDateTime parsedDate = ZonedDateTime.parse(expirationDate, DateTimeFormatter.ISO_DATE_TIME); - ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); // Convert to UTC for validation ZonedDateTime dateInUTC = parsedDate.withZoneSameInstant(ZoneOffset.UTC); // The date value must conform to the ISO 8601 format, be in the future. - if (dateInUTC.isBefore(now)) { - throw new OMException("Invalid lifecycle configuration: 'Date' must be in the future " + now + "," + dateInUTC, - OMException.ResultCodes.INVALID_REQUEST); + ZonedDateTime createDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(creationTime), ZoneOffset.UTC); + if (dateInUTC.isBefore(createDate)) { + throw new OMException("Invalid lifecycle configuration: 'Date' must be in the future " + createDate + "," + + dateInUTC, OMException.ResultCodes.INVALID_REQUEST); } + // Verify that the time is midnight UTC (00:00:00Z) if (!test && (dateInUTC.getHour() != 0 || dateInUTC.getMinute() != 0 || @@ -202,9 +205,7 @@ public Builder setDate(String lcDate) { } public OmLCExpiration build() throws OMException { - OmLCExpiration omLCExpiration = new OmLCExpiration(this); - omLCExpiration.valid(); - return omLCExpiration; + return new OmLCExpiration(this); } } diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCFilter.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCFilter.java index d6da6f45cd9..ff6f36b2cc5 100644 --- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCFilter.java +++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCFilter.java @@ -205,7 +205,6 @@ public static class Builder { private String tagKey = null; private String tagValue = null; private OmLifecycleRuleAndOperator andOperator = null; - private BucketLayout bucketLayout; public Builder setPrefix(String lcPrefix) { this.prefix = lcPrefix; @@ -223,15 +222,8 @@ public Builder setAndOperator(OmLifecycleRuleAndOperator andOp) { return this; } - public Builder setBucketLayout(BucketLayout layout) { - this.bucketLayout = layout; - return this; - } - public OmLCFilter build() throws OMException { - OmLCFilter omLCFilter = new OmLCFilter(this); - omLCFilter.valid(bucketLayout); - return omLCFilter; + return new OmLCFilter(this); } } } diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCRule.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCRule.java index b8a63865513..f22eec1beee 100644 --- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCRule.java +++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCRule.java @@ -152,9 +152,11 @@ public boolean isTagEnable() { * - Filter must be valid * - There must be at most one Expiration action per rule * + * @param bucketLayout The bucket layout for validation + * @param creationTime The creation time of the lifecycle configuration in milliseconds since epoch * @throws OMException if the validation fails */ - public void valid(BucketLayout bucketLayout) throws OMException { + public void valid(BucketLayout bucketLayout, Long creationTime) throws OMException { if (id.length() > LC_ID_MAX_LENGTH) { throw new OMException("ID length should not exceed allowed limit of " + LC_ID_MAX_LENGTH, OMException.ResultCodes.INVALID_REQUEST); @@ -175,7 +177,7 @@ public void valid(BucketLayout bucketLayout) throws OMException { throw new OMException("A rule can have at most one Expiration action.", OMException.ResultCodes.INVALID_REQUEST); } - action.valid(); + action.valid(creationTime); } if (prefix != null && filter != null) { @@ -304,7 +306,7 @@ public static OmLCRule getFromProtobuf(LifecycleRule lifecycleRule, BucketLayout builder.setFilter(OmLCFilter.getFromProtobuf(lifecycleRule.getFilter(), layout)); } - return builder.setBucketLayout(layout).build(); + return builder.build(); } @Override @@ -329,7 +331,6 @@ public static class Builder { private boolean enabled; private List<OmLCAction> actions = new ArrayList<>(); private OmLCFilter filter; - private BucketLayout bucketLayout; public Builder setId(String lcId) { this.id = lcId; @@ -378,15 +379,8 @@ public OmLCFilter getFilter() { return filter; } - public Builder setBucketLayout(BucketLayout layout) { - this.bucketLayout = layout; - return this; - } - public OmLCRule build() throws OMException { - OmLCRule omLCRule = new OmLCRule(this); - omLCRule.valid(bucketLayout); - return omLCRule; + return new OmLCRule(this); } } } diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLifecycleConfiguration.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLifecycleConfiguration.java index d711e3cb937..ad6d31d7cf1 100644 --- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLifecycleConfiguration.java +++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLifecycleConfiguration.java @@ -130,7 +130,7 @@ public void valid() throws OMException { } for (OmLCRule rule : rules) { - rule.valid(bucketLayout); + rule.valid(bucketLayout, creationTime); } } @@ -213,9 +213,7 @@ public static OmLifecycleConfiguration getFromProtobuf( .setBucketLayout(layout) .setRules(rulesList); - if (lifecycleConfiguration.hasCreationTime()) { - builder.setCreationTime(lifecycleConfiguration.getCreationTime()); - } + builder.setCreationTime(lifecycleConfiguration.getCreationTime()); if (lifecycleConfiguration.hasObjectID()) { builder.setObjectID(lifecycleConfiguration.getObjectID()); } @@ -258,8 +256,8 @@ public Builder setBucketLayout(BucketLayout layout) { return this; } - public Builder setCreationTime(long ctime) { - this.creationTime = ctime; + public Builder setCreationTime(long creationTime) { + this.creationTime = creationTime; return this; } @@ -286,7 +284,11 @@ public Builder setUpdateID(long uID) { } public OmLifecycleConfiguration build() throws OMException { - OmLifecycleConfiguration omLifecycleConfiguration = new OmLifecycleConfiguration(this); + return new OmLifecycleConfiguration(this); + } + + public OmLifecycleConfiguration buildAndValid() throws OMException { + OmLifecycleConfiguration omLifecycleConfiguration = build(); omLifecycleConfiguration.valid(); return omLifecycleConfiguration; } diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLifecycleRuleAndOperator.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLifecycleRuleAndOperator.java index d28eaea1418..522fc6ca930 100644 --- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLifecycleRuleAndOperator.java +++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLifecycleRuleAndOperator.java @@ -138,7 +138,6 @@ public boolean match(OmKeyInfo omKeyInfo, String keyPath) { public static class Builder { private Map<String, String> tags = new HashMap<>(); private String prefix; - private BucketLayout bucketLayout; public Builder setPrefix(String lcPrefix) { this.prefix = lcPrefix; @@ -156,15 +155,9 @@ public Builder setTags(Map<String, String> lcTags) { } return this; } - public Builder setBucketLayout(BucketLayout layout) { - this.bucketLayout = layout; - return this; - } public OmLifecycleRuleAndOperator build() throws OMException { - OmLifecycleRuleAndOperator omLifecycleRuleAndOperator = new OmLifecycleRuleAndOperator(this); - omLifecycleRuleAndOperator.valid(bucketLayout); - return omLifecycleRuleAndOperator; + return new OmLifecycleRuleAndOperator(this); } } diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCExpiration.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCExpiration.java index a2fe3951cb0..1aa3e2b5011 100644 --- a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCExpiration.java +++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCExpiration.java @@ -37,51 +37,52 @@ class TestOmLCExpiration { public void testCreateValidOmLCExpiration() { OmLCExpiration.Builder exp1 = new OmLCExpiration.Builder() .setDays(30); - assertDoesNotThrow(exp1::build); + long currentTime = System.currentTimeMillis(); + assertDoesNotThrow(() -> exp1.build().valid(currentTime)); OmLCExpiration.Builder exp2 = new OmLCExpiration.Builder() .setDate("2099-10-10T00:00:00Z"); - assertDoesNotThrow(exp2::build); + assertDoesNotThrow(() -> exp2.build().valid(currentTime)); OmLCExpiration.Builder exp3 = new OmLCExpiration.Builder() .setDays(1); - assertDoesNotThrow(exp3::build); + assertDoesNotThrow(() -> exp3.build().valid(currentTime)); OmLCExpiration.Builder exp4 = new OmLCExpiration.Builder() .setDate("2099-12-31T00:00:00Z"); - assertDoesNotThrow(exp4::build); + assertDoesNotThrow(() -> exp4.build().valid(currentTime)); OmLCExpiration.Builder exp5 = new OmLCExpiration.Builder() .setDate("2099-02-15T00:00:00.000Z"); - assertDoesNotThrow(exp5::build); + assertDoesNotThrow(() -> exp5.build().valid(currentTime)); OmLCExpiration.Builder exp6 = new OmLCExpiration.Builder() .setDate("2042-04-02T00:00:00Z"); - assertDoesNotThrow(exp6::build); + assertDoesNotThrow(() -> exp6.build().valid(currentTime)); OmLCExpiration.Builder exp7 = new OmLCExpiration.Builder() .setDate("2042-04-02T00:00:00+00:00"); - assertDoesNotThrow(exp7::build); + assertDoesNotThrow(() -> exp7.build().valid(currentTime)); OmLCExpiration.Builder exp8 = new OmLCExpiration.Builder() .setDate("2099-12-31T00:00:00+00:00"); - assertDoesNotThrow(exp8::build); + assertDoesNotThrow(() -> exp8.build().valid(currentTime)); OmLCExpiration.Builder exp9 = new OmLCExpiration.Builder() .setDate("2099-12-31T23:00:00-01:00"); - assertDoesNotThrow(exp9::build); + assertDoesNotThrow(() -> exp9.build().valid(currentTime)); OmLCExpiration.Builder exp10 = new OmLCExpiration.Builder() .setDate("2100-01-01T01:00:00+01:00"); - assertDoesNotThrow(exp10::build); + assertDoesNotThrow(() -> exp10.build().valid(currentTime)); OmLCExpiration.Builder exp11 = new OmLCExpiration.Builder() .setDate("2099-12-31T12:00:00-12:00"); - assertDoesNotThrow(exp11::build); + assertDoesNotThrow(() -> exp11.build().valid(currentTime)); OmLCExpiration.Builder exp12 = new OmLCExpiration.Builder() .setDate("2100-01-01T12:00:00+12:00"); - assertDoesNotThrow(exp12::build); + assertDoesNotThrow(() -> exp12.build().valid(currentTime)); } @Test @@ -89,84 +90,86 @@ public void testCreateInValidOmLCExpiration() { OmLCExpiration.Builder exp1 = new OmLCExpiration.Builder() .setDays(30) .setDate(getFutureDateString(100)); - assertOMException(exp1::build, INVALID_REQUEST, + long currentTime = System.currentTimeMillis(); + assertOMException(() -> exp1.build().valid(currentTime), INVALID_REQUEST, "Either 'days' or 'date' should be specified, but not both or neither."); OmLCExpiration.Builder exp2 = new OmLCExpiration.Builder() .setDays(-1); - assertOMException(exp2::build, INVALID_REQUEST, + assertOMException(() -> exp2.build().valid(currentTime), INVALID_REQUEST, "'Days' for Expiration action must be a positive integer"); OmLCExpiration.Builder exp3 = new OmLCExpiration.Builder() .setDate(null); - assertOMException(exp3::build, INVALID_REQUEST, + assertOMException(() -> exp3.build().valid(currentTime), INVALID_REQUEST, "Either 'days' or 'date' should be specified, but not both or neither."); OmLCExpiration.Builder exp4 = new OmLCExpiration.Builder() .setDate(""); - assertOMException(exp4::build, INVALID_REQUEST, + assertOMException(() -> exp4.build().valid(currentTime), INVALID_REQUEST, "Either 'days' or 'date' should be specified, but not both or neither."); OmLCExpiration.Builder exp5 = new OmLCExpiration.Builder(); - assertOMException(exp5::build, INVALID_REQUEST, + assertOMException(() -> exp5.build().valid(currentTime), INVALID_REQUEST, "Either 'days' or 'date' should be specified, but not both or neither."); OmLCExpiration.Builder exp6 = new OmLCExpiration.Builder() .setDate("10-10-2099"); - assertOMException(exp6::build, INVALID_REQUEST, + assertOMException(() -> exp6.build().valid(currentTime), INVALID_REQUEST, "'Date' must be in ISO 8601 format"); OmLCExpiration.Builder exp7 = new OmLCExpiration.Builder() .setDate("2099-12-31T00:00:00"); - assertOMException(exp7::build, INVALID_REQUEST, + assertOMException(() -> exp7.build().valid(currentTime), INVALID_REQUEST, "'Date' must be in ISO 8601 format"); - // Testing for date in the past + // Testing for date in the past with creation time OmLCExpiration.Builder exp8 = new OmLCExpiration.Builder() .setDate(getFutureDateString(-1)); - assertOMException(exp8::build, INVALID_REQUEST, + assertOMException(() -> exp8.build().valid(currentTime), INVALID_REQUEST, "'Date' must be in the future"); OmLCExpiration.Builder exp9 = new OmLCExpiration.Builder() .setDays(0); - assertOMException(exp9::build, INVALID_REQUEST, + assertOMException(() -> exp9.build().valid(currentTime), INVALID_REQUEST, "'Days' for Expiration action must be a positive integer"); - // 1 minute ago + // 1 minute ago with creation time OmLCExpiration.Builder exp10 = new OmLCExpiration.Builder() .setDate(getFutureDateString(0, 0, -1)); - assertOMException(exp10::build, INVALID_REQUEST, + assertOMException(() -> exp10.build().valid(currentTime), INVALID_REQUEST, "'Date' must be in the future"); } @Test public void testDateMustBeAtMidnightUTC() { // Acceptable date - midnight UTC + long currentTime = System.currentTimeMillis(); OmLCExpiration.Builder validExp = new OmLCExpiration.Builder() .setDate("2099-10-10T00:00:00Z"); - assertDoesNotThrow(validExp::build); + assertDoesNotThrow(() -> validExp.build().valid(currentTime)); // Non-midnight UTC dates should be rejected OmLCExpiration.Builder exp1 = new OmLCExpiration.Builder() .setDate("2099-10-10T10:00:00Z"); - assertOMException(exp1::build, INVALID_REQUEST, "'Date' must represent midnight UTC"); + assertOMException(() -> exp1.build().valid(currentTime), INVALID_REQUEST, "'Date' must represent midnight UTC"); OmLCExpiration.Builder exp2 = new OmLCExpiration.Builder() .setDate("2099-10-10T00:30:00Z"); - assertOMException(exp2::build, INVALID_REQUEST, "'Date' must represent midnight UTC"); + assertOMException(() -> exp2.build().valid(currentTime), INVALID_REQUEST, "'Date' must represent midnight UTC"); OmLCExpiration.Builder exp3 = new OmLCExpiration.Builder() .setDate("2099-10-10T00:00:30Z"); - assertOMException(exp3::build, INVALID_REQUEST, "'Date' must represent midnight UTC"); + assertOMException(() -> exp3.build().valid(currentTime), INVALID_REQUEST, "'Date' must represent midnight UTC"); OmLCExpiration.Builder exp4 = new OmLCExpiration.Builder() .setDate("2099-10-10T00:00:00.123Z"); - assertOMException(exp4::build, INVALID_REQUEST, "'Date' must represent midnight UTC"); + assertOMException(() -> exp4.build().valid(currentTime), INVALID_REQUEST, "'Date' must represent midnight UTC"); // Non-UTC timezone should be rejected OmLCExpiration.Builder exp5 = new OmLCExpiration.Builder() .setDate("2099-10-10T00:00:00+01:00"); - assertOMException(exp5::build, INVALID_REQUEST, "'Date' must represent midnight UTC"); + assertOMException(() -> exp5.build().valid(currentTime), INVALID_REQUEST, "'Date' must represent midnight UTC"); } @Test diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCFilter.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCFilter.java index d073dad71d8..bd3809ba10a 100644 --- a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCFilter.java +++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCFilter.java @@ -42,11 +42,14 @@ class TestOmLCFilter { @Test public void testInValidOmLCRulePrefixFilterCoExist() throws OMException { + long currentTime = System.currentTimeMillis(); OmLCRule.Builder rule1 = getOmLCRuleBuilder("id", "prefix", true, 1, VALID_OM_LC_FILTER); - assertOMException(rule1::build, INVALID_REQUEST, "Filter and Prefix cannot be used together"); + assertOMException(() -> rule1.build().valid(BucketLayout.DEFAULT, currentTime), INVALID_REQUEST, + "Filter and Prefix cannot be used together"); OmLCRule.Builder rule2 = getOmLCRuleBuilder("id", "", true, 1, VALID_OM_LC_FILTER); - assertOMException(rule2::build, INVALID_REQUEST, "Filter and Prefix cannot be used together"); + assertOMException(() -> rule2.build().valid(BucketLayout.DEFAULT, currentTime), INVALID_REQUEST, + "Filter and Prefix cannot be used together"); } @Test @@ -70,19 +73,19 @@ public void testValidFilter() throws OMException { @Test public void testInValidFilter() { OmLCFilter.Builder lcFilter1 = getOmLCFilterBuilder("prefix", Pair.of("key", "value"), VALID_OM_LC_AND_OPERATOR); - assertOMException(lcFilter1::build, INVALID_REQUEST, + assertOMException(() -> lcFilter1.build().valid(BucketLayout.DEFAULT), INVALID_REQUEST, "Only one of 'Prefix', 'Tag', or 'AndOperator' should be specified"); OmLCFilter.Builder lcFilter2 = getOmLCFilterBuilder("prefix", Pair.of("key", "value"), null); - assertOMException(lcFilter2::build, INVALID_REQUEST, + assertOMException(() -> lcFilter2.build().valid(BucketLayout.DEFAULT), INVALID_REQUEST, "Only one of 'Prefix', 'Tag', or 'AndOperator' should be specified"); OmLCFilter.Builder lcFilter3 = getOmLCFilterBuilder("prefix", null, VALID_OM_LC_AND_OPERATOR); - assertOMException(lcFilter3::build, INVALID_REQUEST, + assertOMException(() -> lcFilter3.build().valid(BucketLayout.DEFAULT), INVALID_REQUEST, "Only one of 'Prefix', 'Tag', or 'AndOperator' should be specified"); OmLCFilter.Builder lcFilter4 = getOmLCFilterBuilder(null, Pair.of("key", "value"), VALID_OM_LC_AND_OPERATOR); - assertOMException(lcFilter4::build, INVALID_REQUEST, + assertOMException(() -> lcFilter4.build().valid(BucketLayout.DEFAULT), INVALID_REQUEST, "Only one of 'Prefix', 'Tag', or 'AndOperator' should be specified"); } diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCRule.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCRule.java index 4e4736c257e..0a17af7b3e7 100644 --- a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCRule.java +++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCRule.java @@ -47,6 +47,7 @@ class TestOmLCRule { @Test public void testCreateValidOmLCRule() throws OMException { + long currentTime = System.currentTimeMillis(); OmLCExpiration exp = new OmLCExpiration.Builder() .setDays(30) .build(); @@ -56,13 +57,14 @@ public void testCreateValidOmLCRule() throws OMException { .setEnabled(true) .setPrefix("/spark/logs") .setAction(exp); - assertDoesNotThrow(r1::build); + assertDoesNotThrow(() -> r1.build().valid(BucketLayout.DEFAULT, currentTime)); OmLCRule.Builder r2 = new OmLCRule.Builder() .setEnabled(true) .setPrefix("") .setAction(exp); OmLCRule omLCRule = assertDoesNotThrow(r2::build); + assertDoesNotThrow(() -> omLCRule.valid(BucketLayout.DEFAULT, currentTime)); // Empty id should generate a 48 (default) bit one. assertEquals(OmLCRule.LC_ID_LENGTH, omLCRule.getId().length(), @@ -71,6 +73,7 @@ public void testCreateValidOmLCRule() throws OMException { @Test public void testCreateInValidOmLCRule() throws OMException { + long currentTime = System.currentTimeMillis(); OmLCExpiration exp = new OmLCExpiration.Builder() .setDays(30) .build(); @@ -81,26 +84,28 @@ public void testCreateInValidOmLCRule() throws OMException { OmLCRule.Builder r1 = new OmLCRule.Builder() .setId(new String(id)) .setAction(exp); - assertOMException(r1::build, INVALID_REQUEST, "ID length should not exceed allowed limit of 255"); + assertOMException(() -> r1.build().valid(BucketLayout.DEFAULT, currentTime), INVALID_REQUEST, + "ID length should not exceed allowed limit of 255"); OmLCRule.Builder r2 = new OmLCRule.Builder() .setId("remove Spark logs after 30 days") .setEnabled(true) .setPrefix("/spark/logs") .setAction(null); - assertOMException(r2::build, INVALID_REQUEST, + assertOMException(() -> r2.build().valid(BucketLayout.DEFAULT, currentTime), INVALID_REQUEST, "At least one action needs to be specified in a rule"); OmLCRule.Builder r3 = new OmLCRule.Builder() .setEnabled(true) .setAction(exp); - assertOMException(r3::build, INVALID_REQUEST, + assertOMException(() -> r3.build().valid(BucketLayout.DEFAULT, currentTime), INVALID_REQUEST, "Filter and Prefix cannot both be null."); } @Test public void testMultipleActionsInRule() throws OMException { + long currentTime = System.currentTimeMillis(); OmLCExpiration expiration1 = new OmLCExpiration.Builder() .setDays(30) .build(); @@ -118,11 +123,13 @@ public void testMultipleActionsInRule() throws OMException { OmLCRule.Builder rule = builder.setActions(actions); - assertOMException(rule::build, INVALID_REQUEST, "A rule can have at most one Expiration action"); + assertOMException(() -> rule.build().valid(BucketLayout.DEFAULT, currentTime), INVALID_REQUEST, + "A rule can have at most one Expiration action"); } @Test public void testRuleWithAndOperatorFilter() throws OMException { + long currentTime = System.currentTimeMillis(); Map<String, String> tags = ImmutableMap.of("app", "hadoop", "env", "test"); OmLifecycleRuleAndOperator andOperator = getOmLCAndOperatorBuilder("/logs/", tags).build(); OmLCFilter filter = getOmLCFilterBuilder(null, null, andOperator).build(); @@ -134,12 +141,14 @@ public void testRuleWithAndOperatorFilter() throws OMException { .setAction(new OmLCExpiration.Builder().setDays(30).build()); OmLCRule rule = assertDoesNotThrow(builder::build); + assertDoesNotThrow(() -> rule.valid(BucketLayout.DEFAULT, currentTime)); assertTrue(rule.isPrefixEnable()); assertTrue(rule.isTagEnable()); } @Test public void testRuleWithTagFilter() throws OMException { + long currentTime = System.currentTimeMillis(); OmLCFilter filter = getOmLCFilterBuilder(null, Pair.of("app", "hadoop"), null).build(); OmLCRule.Builder builder = new OmLCRule.Builder() @@ -149,6 +158,7 @@ public void testRuleWithTagFilter() throws OMException { .setAction(new OmLCExpiration.Builder().setDays(30).build()); OmLCRule rule = assertDoesNotThrow(builder::build); + assertDoesNotThrow(() -> rule.valid(BucketLayout.DEFAULT, currentTime)); assertFalse(rule.isPrefixEnable()); assertTrue(rule.isTagEnable()); } @@ -174,7 +184,7 @@ public void testDuplicateRuleIDs() throws OMException { .setBucket("bucket") .setRules(rules); - assertOMException(config::build, INVALID_REQUEST, "Duplicate rule IDs found"); + assertOMException(() -> config.build().valid(), INVALID_REQUEST, "Duplicate rule IDs found"); } @Test diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLifeCycleConfiguration.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLifeCycleConfiguration.java index c05d4743223..3afb45e84db 100644 --- a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLifeCycleConfiguration.java +++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLifeCycleConfiguration.java @@ -30,6 +30,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import com.google.common.collect.ImmutableMap; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -71,14 +73,14 @@ public void testCreateInValidLCConfiguration() throws OMException { List<OmLCRule> rules = Collections.singletonList(rule); OmLifecycleConfiguration.Builder lcc0 = getOmLifecycleConfiguration(null, "bucket", rules); - assertOMException(lcc0::build, INVALID_REQUEST, "Volume cannot be blank"); + assertOMException(() -> lcc0.buildAndValid(), INVALID_REQUEST, "Volume cannot be blank"); OmLifecycleConfiguration.Builder lcc1 = getOmLifecycleConfiguration("volume", null, rules); - assertOMException(lcc1::build, INVALID_REQUEST, "Bucket cannot be blank"); + assertOMException(() -> lcc1.buildAndValid(), INVALID_REQUEST, "Bucket cannot be blank"); OmLifecycleConfiguration.Builder lcc3 = getOmLifecycleConfiguration( "volume", "bucket", Collections.emptyList()); - assertOMException(lcc3::build, INVALID_REQUEST, + assertOMException(() -> lcc3.buildAndValid(), INVALID_REQUEST, "At least one rules needs to be specified in a lifecycle configuration"); List<OmLCRule> rules4 = new ArrayList<>( @@ -92,7 +94,7 @@ public void testCreateInValidLCConfiguration() throws OMException { rules4.add(r); } OmLifecycleConfiguration.Builder lcc4 = getOmLifecycleConfiguration("volume", "bucket", rules4); - assertOMException(lcc4::build, INVALID_REQUEST, + assertOMException(() -> lcc4.buildAndValid(), INVALID_REQUEST, "The number of lifecycle rules must not exceed the allowed limit of"); } @@ -193,7 +195,7 @@ public void testDisabledRule() throws OMException { .build(); assertFalse(rule.isEnabled()); - assertDoesNotThrow(() -> rule.valid(BucketLayout.DEFAULT)); + assertDoesNotThrow(() -> rule.valid(BucketLayout.DEFAULT, System.currentTimeMillis())); } @Test @@ -225,4 +227,40 @@ public void testProtobufConversion() throws OMException { assertEquals(30, ruleFromProto.getExpiration().getDays()); } + @Test + public void testOMStartupWithPastExpirationDate() throws OMException { + // Simulate a lifecycle configuration with expiration date in the past + // This scenario can happen when OM restarts after some time has passed + // since the lifecycle configuration was created. + + // Create a rule with expiration date that is in the past (simulating old config) + String pastDate = getFutureDateString(-1); // A date clearly in the past (1 day ago) + OmLCExpiration pastExpiration = new OmLCExpiration.Builder() + .setDate(pastDate) + .build(); + + OmLCRule ruleWithPastDate = new OmLCRule.Builder() + .setId("test-rule-past-date") + .setPrefix("/old-logs/") + .setEnabled(true) + .addAction(pastExpiration) + .build(); + + OmLifecycleConfiguration config = new OmLifecycleConfiguration.Builder() + .setVolume("test-volume") + .setBucket("test-bucket") + .setBucketLayout(BucketLayout.DEFAULT) + // An Expiration was created two days ago and expired 1 day ago, should be valid + .setCreationTime(ZonedDateTime.now(ZoneOffset.UTC).minusDays(2).toInstant().toEpochMilli()) + .addRule(ruleWithPastDate) + .setObjectID(123456L) + .setUpdateID(78910L) + .build(); + + LifecycleConfiguration proto = config.getProtobuf(); + OmLifecycleConfiguration configFromProto = assertDoesNotThrow(() -> + OmLifecycleConfiguration.getFromProtobuf(proto)); + assertDoesNotThrow(configFromProto::valid); + } + } diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLifecycleRuleAndOperator.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLifecycleRuleAndOperator.java index 5b77a625980..43c7ffbc41b 100644 --- a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLifecycleRuleAndOperator.java +++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLifecycleRuleAndOperator.java @@ -59,15 +59,17 @@ public void testValidAndOperator() throws OMException { @Test public void testInValidAndOperator() { OmLifecycleRuleAndOperator.Builder andOperator1 = getOmLCAndOperatorBuilder("prefix", null); - assertOMException(andOperator1::build, INVALID_REQUEST, "'Prefix' alone is not allowed"); + assertOMException(() -> andOperator1.build().valid(BucketLayout.DEFAULT), INVALID_REQUEST, + "'Prefix' alone is not allowed"); OmLifecycleRuleAndOperator.Builder andOperator2 = getOmLCAndOperatorBuilder(null, Collections.singletonMap("tag1", "value1")); - assertOMException(andOperator2::build, INVALID_REQUEST, + assertOMException(() -> andOperator2.build().valid(BucketLayout.DEFAULT), INVALID_REQUEST, "If 'Tags' are specified without 'Prefix', there should be more than one tag"); OmLifecycleRuleAndOperator.Builder andOperator3 = getOmLCAndOperatorBuilder(null, null); - assertOMException(andOperator3::build, INVALID_REQUEST, "Either 'Tags' or 'Prefix' must be specified."); + assertOMException(() -> andOperator3.build().valid(BucketLayout.DEFAULT), INVALID_REQUEST, + "Either 'Tags' or 'Prefix' must be specified."); } @Test diff --git a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto index 4823e81eaa0..1a83bf4ff58 100644 --- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto +++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto @@ -2390,7 +2390,7 @@ message LifecycleConfiguration { required string volume = 1; required string bucket = 2; required BucketLayoutProto bucketLayout = 3; - optional uint64 creationTime = 4; + required uint64 creationTime = 4; repeated LifecycleRule rules = 5; optional uint64 objectID = 6; optional uint64 updateID = 7; diff --git a/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/OMMetadataManager.java b/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/OMMetadataManager.java index e790d370f83..98299fb2ba4 100644 --- a/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/OMMetadataManager.java +++ b/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/OMMetadataManager.java @@ -35,6 +35,7 @@ import org.apache.hadoop.hdds.utils.db.cache.CacheKey; import org.apache.hadoop.hdds.utils.db.cache.CacheValue; import org.apache.hadoop.ozone.common.BlockGroup; +import org.apache.hadoop.ozone.om.exceptions.OMException; import org.apache.hadoop.ozone.om.helpers.BucketLayout; import org.apache.hadoop.ozone.om.helpers.ListKeysResult; import org.apache.hadoop.ozone.om.helpers.ListOpenFilesResult; @@ -474,7 +475,7 @@ String getMultipartKeyFSO(String volume, String bucket, String key, String /** * @return list all LifecycleConfigurations. */ - List<OmLifecycleConfiguration> listLifecycleConfigurations(); + List<OmLifecycleConfiguration> listLifecycleConfigurations() throws OMException; /** * Fetches the lifecycle configuration by bucketName. diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java index e9b7dc84559..1dd6833df1f 100644 --- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java +++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java @@ -2126,22 +2126,23 @@ public List<OmLifecycleConfiguration> listLifecycleConfigurations() { public OmLifecycleConfiguration getLifecycleConfiguration(String volumeName, String bucketName) throws IOException { Preconditions.checkNotNull(bucketName); + OmLifecycleConfiguration value = null; try { String bucketKey = getBucketKey(volumeName, bucketName); - OmLifecycleConfiguration value = getLifecycleConfigurationTable().get(bucketKey); - + value = getLifecycleConfigurationTable().get(bucketKey); if (value == null) { LOG.debug("lifecycle configuration of bucket /{}/{} not found.", volumeName, bucketName); throw new OMException("Lifecycle configuration not found", LIFECYCLE_CONFIGURATION_NOT_FOUND); } + value.valid(); return value; } catch (IOException ex) { - if (!(ex instanceof OMException)) { - LOG.error("Exception while getting lifecycle configuration for " + - "bucket: /{}/{}", volumeName, bucketName, ex); - } + LOG.error("Exception while getting lifecycle configuration for " + + "bucket: /{}/{}, LifecycleConfiguration {}", volumeName, bucketName, + value != null ? value.getProtobuf() : "", ex); + throw ex; } } diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/lifecycle/OMLifecycleConfigurationSetRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/lifecycle/OMLifecycleConfigurationSetRequest.java index 91f4567dcce..da395fa30c1 100644 --- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/lifecycle/OMLifecycleConfigurationSetRequest.java +++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/lifecycle/OMLifecycleConfigurationSetRequest.java @@ -47,7 +47,6 @@ import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.UserInfo; import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer; import org.apache.hadoop.ozone.security.acl.OzoneObj; -import org.apache.hadoop.util.Time; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -100,7 +99,7 @@ public OMRequest preExecute(OzoneManager ozoneManager) throws IOException { .setVolume(resolvedBucket.realVolume()) .setBucket(resolvedBucket.realBucket()); - newLifecycleConfiguration.setCreationTime(Time.now()); + newLifecycleConfiguration.setCreationTime(System.currentTimeMillis()); newCreateRequest.setLifecycleConfiguration(newLifecycleConfiguration); return omRequest.toBuilder().setUserInfo(getUserInfo()) diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/KeyLifecycleService.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/KeyLifecycleService.java index f707979746b..80d267cc84f 100644 --- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/KeyLifecycleService.java +++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/KeyLifecycleService.java @@ -128,9 +128,22 @@ public BackgroundTaskQueue getTasks() { return queue; } - List<OmLifecycleConfiguration> lifecycleConfigurationList = - omMetadataManager.listLifecycleConfigurations(); + List<OmLifecycleConfiguration> lifecycleConfigurationList = null; + try { + lifecycleConfigurationList = omMetadataManager.listLifecycleConfigurations(); + } catch (OMException e) { + LOG.error("Failed to list lifecycle configurations", e); + return queue; + } for (OmLifecycleConfiguration lifecycleConfiguration : lifecycleConfigurationList) { + try { + lifecycleConfiguration.valid(); + } catch (OMException e) { + LOG.error("Skip invalid lifecycle configuration for {}/{}: LifecycleConfiguration:\n {}", + lifecycleConfiguration.getVolume(), lifecycleConfiguration.getBucket(), + lifecycleConfiguration.getProtobuf(), e); + continue; + } String bucketKey = omMetadataManager.getBucketKey(lifecycleConfiguration.getVolume(), lifecycleConfiguration.getBucket()); if (lifecycleConfiguration.getRules().stream().anyMatch(r -> r.isEnabled())) { diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/lifecycle/TestOMLifecycleConfigurationRequest.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/lifecycle/TestOMLifecycleConfigurationRequest.java index 03075091b94..5f54c24a064 100644 --- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/lifecycle/TestOMLifecycleConfigurationRequest.java +++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/lifecycle/TestOMLifecycleConfigurationRequest.java @@ -123,6 +123,7 @@ public OMRequest setLifecycleConfigurationRequest(String volumeName, String prefix = "prefix/"; LifecycleConfiguration.Builder builder = LifecycleConfiguration.newBuilder() .setBucketLayout(BucketLayoutProto.OBJECT_STORE) + .setCreationTime(System.currentTimeMillis()) .setVolume(volumeName) .setBucket(bucketName); diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyLifecycleService.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyLifecycleService.java index 26b82c0c5fa..7df7e244b76 100644 --- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyLifecycleService.java +++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyLifecycleService.java @@ -1504,7 +1504,6 @@ private void createLifecyclePolicy(String volume, String bucket, BucketLayout la .setEnabled(enabled) .setPrefix(prefix) .setFilter(filter) - .setBucketLayout(layout) .setAction(new OmLCExpiration.Builder() .setDate(date) .build()) @@ -1513,6 +1512,7 @@ private void createLifecyclePolicy(String volume, String bucket, BucketLayout la String key = "/" + volume + "/" + bucket; LifecycleConfiguration lcProto = lcc.getProtobuf(); OmLifecycleConfiguration canonicalLcc = OmLifecycleConfiguration.getFromProtobuf(lcProto); + canonicalLcc.valid(); metadataManager.getLifecycleConfigurationTable().put(key, lcc); metadataManager.getLifecycleConfigurationTable().addCacheEntry( new CacheKey<>(key), CacheValue.get(1L, canonicalLcc)); diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java index cb9b79c858f..b2b90cc3a2b 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java @@ -813,7 +813,9 @@ public Response putBucketLifecycleConfiguration( if (ex.getResult() == ResultCodes.ACCESS_DENIED) { throw S3ErrorTable.newError(S3ErrorTable.ACCESS_DENIED, bucketName); } else if (ex.getResult() == ResultCodes.INVALID_REQUEST) { - throw S3ErrorTable.newError(S3ErrorTable.INVALID_REQUEST, bucketName); + OS3Exception invalidException = S3ErrorTable.INVALID_REQUEST; + invalidException.setErrorMessage(ex.getMessage()); + throw S3ErrorTable.newError(invalidException, bucketName); } } return Response.ok().build(); diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3LifecycleConfiguration.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3LifecycleConfiguration.java index 54009f28491..8aa3e52f433 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3LifecycleConfiguration.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3LifecycleConfiguration.java @@ -263,7 +263,7 @@ public OmLifecycleConfiguration toOmLifecycleConfiguration(OzoneBucket ozoneBuck builder.addRule(convertToOmRule(rule)); } - return builder.build(); + return builder.buildAndValid(); } catch (Exception ex) { if (ex instanceof IllegalStateException) { throw S3ErrorTable.newError(S3ErrorTable.INVALID_REQUEST, ozoneBucket.getName(), ex); --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@ozone.apache.org For additional commands, e-mail: commits-h...@ozone.apache.org