diff --git a/src/main/java/io/airlift/units/MaxThreadCount.java b/src/main/java/io/airlift/units/MaxThreadCount.java new file mode 100644 index 0000000..6d17393 --- /dev/null +++ b/src/main/java/io/airlift/units/MaxThreadCount.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = MaxThreadCountValidator.class) +public @interface MaxThreadCount +{ + String value(); + + String message() default "{io.airlift.units.MaxThreadCount.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/io/airlift/units/MaxThreadCountValidator.java b/src/main/java/io/airlift/units/MaxThreadCountValidator.java new file mode 100644 index 0000000..e620315 --- /dev/null +++ b/src/main/java/io/airlift/units/MaxThreadCountValidator.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class MaxThreadCountValidator + implements ConstraintValidator +{ + private ThreadCount max; + + @Override + public void initialize(MaxThreadCount annotation) + { + this.max = ThreadCount.valueOf(annotation.value()); + } + + @Override + public boolean isValid(ThreadCount threadCount, ConstraintValidatorContext constraintValidatorContext) + { + return threadCount == null || threadCount.compareTo(max) <= 0; + } + + @Override + public String toString() + { + return "max: " + max; + } +} diff --git a/src/main/java/io/airlift/units/MinThreadCount.java b/src/main/java/io/airlift/units/MinThreadCount.java new file mode 100644 index 0000000..c6090dc --- /dev/null +++ b/src/main/java/io/airlift/units/MinThreadCount.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = MinThreadCountValidator.class) +public @interface MinThreadCount +{ + String value(); + + String message() default "{io.airlift.units.MinThreadCount.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/io/airlift/units/MinThreadCountValidator.java b/src/main/java/io/airlift/units/MinThreadCountValidator.java new file mode 100644 index 0000000..84662f1 --- /dev/null +++ b/src/main/java/io/airlift/units/MinThreadCountValidator.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class MinThreadCountValidator + implements ConstraintValidator +{ + private ThreadCount min; + + @Override + public void initialize(MinThreadCount annotation) + { + this.min = ThreadCount.valueOf(annotation.value()); + } + + @Override + public boolean isValid(ThreadCount threadCount, ConstraintValidatorContext constraintValidatorContext) + { + return threadCount == null || threadCount.compareTo(min) >= 0; + } + + @Override + public String toString() + { + return "min: " + min; + } +} diff --git a/src/main/java/io/airlift/units/ThreadCount.java b/src/main/java/io/airlift/units/ThreadCount.java new file mode 100644 index 0000000..61bb05a --- /dev/null +++ b/src/main/java/io/airlift/units/ThreadCount.java @@ -0,0 +1,183 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.OptionalInt; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static io.airlift.units.Preconditions.checkArgument; +import static java.lang.Math.min; +import static java.lang.Math.round; +import static java.lang.Math.toIntExact; +import static java.lang.String.format; +import static java.nio.file.Files.exists; +import static java.nio.file.Files.lines; + +public class ThreadCount + implements Comparable +{ + private static final String PER_CORE_SUFFIX = "C"; + private static final Supplier AVAILABLE_PROCESSORS = MachineInfo::getAvailablePhysicalProcessorCount; + private final int threadCount; + + ThreadCount(int threadCount) + { + checkArgument(threadCount >= 0, "Thread count cannot be negative"); + this.threadCount = threadCount; + } + + public int getThreadCount() + { + return threadCount; + } + + public static ThreadCount exactValueOf(int value) + { + return new ThreadCount(value); + } + + public static ThreadCount valueOf(String value) + { + if (value.endsWith(PER_CORE_SUFFIX)) { + float parsedMultiplier = parseFloat(value.substring(0, value.lastIndexOf(PER_CORE_SUFFIX)).trim()); + checkArgument(parsedMultiplier > 0, "Thread multiplier cannot be negative"); + float threadCount = parsedMultiplier * AVAILABLE_PROCESSORS.get(); + checkArgument(threadCount <= Integer.MAX_VALUE, "Thread count is greater than 2^32 - 1"); + return new ThreadCount(round(threadCount)); + } + + return new ThreadCount(parseInteger(value)); + } + + public static ThreadCount boundedValueOf(String value, String minValue, String maxValue) + { + ThreadCount parsed = ThreadCount.valueOf(value); + ThreadCount min = ThreadCount.valueOf(minValue); + ThreadCount max = ThreadCount.valueOf(maxValue); + + if (parsed.compareTo(min) < 0) { + return min; + } + + if (parsed.compareTo(max) > 0) { + return max; + } + return parsed; + } + + private static float parseFloat(String value) + { + try { + return Float.parseFloat(value); + } + catch (NumberFormatException e) { + throw new IllegalArgumentException(format("Cannot parse value '%s' as float", value), e); + } + } + + private static int parseInteger(String value) + { + try { + long parsed = Long.parseLong(value); + checkArgument(parsed <= Integer.MAX_VALUE, "Thread count is greater than 2^32 - 1"); + return toIntExact(parsed); + } + catch (NumberFormatException e) { + throw new IllegalArgumentException(format("Cannot parse value '%s' as integer", value), e); + } + } + + @Override + public int compareTo(ThreadCount o) + { + return Integer.compare(threadCount, o.threadCount); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ThreadCount that = (ThreadCount) o; + return threadCount == that.threadCount; + } + + @Override + public int hashCode() + { + return threadCount; + } + + @Override + public String toString() + { + return (threadCount == 1) ? "1 thread" : (threadCount + " threads"); + } + + static final class MachineInfo + { + private static final Path CPU_INFO_PATH = Paths.get("/proc/cpuinfo"); + + // cache physical processor count, so that it's not queried multiple times during tests + private static volatile int physicalProcessorCount = -1; + + private MachineInfo() {} + + public static int getAvailablePhysicalProcessorCount() + { + if (physicalProcessorCount != -1) { + return physicalProcessorCount; + } + + String osArch = System.getProperty("os.arch"); + // logical core count (including container cpu quota if there is any) + int availableProcessorCount = Runtime.getRuntime().availableProcessors(); + int totalPhysicalProcessorCount = availableProcessorCount; + if ("amd64".equals(osArch) || "x86_64".equals(osArch)) { + OptionalInt procInfo = tryReadFromProcCpuinfo(); + if (procInfo.isPresent()) { + totalPhysicalProcessorCount = procInfo.getAsInt(); + } + } + + // cap available processor count to container cpu quota (if there is any). + physicalProcessorCount = min(totalPhysicalProcessorCount, availableProcessorCount); + return physicalProcessorCount; + } + + private static OptionalInt tryReadFromProcCpuinfo() + { + if (!exists(CPU_INFO_PATH)) { + return OptionalInt.empty(); + } + + try (Stream lines = lines(CPU_INFO_PATH)) { + return OptionalInt.of(toIntExact(lines.filter(line -> + line.matches("^processor\\s+: \\d")).count())); + } + catch (IOException e) { + return OptionalInt.empty(); + } + } + } +} diff --git a/src/main/resources/ValidationMessages.properties b/src/main/resources/ValidationMessages.properties index 00195c6..e3943b8 100644 --- a/src/main/resources/ValidationMessages.properties +++ b/src/main/resources/ValidationMessages.properties @@ -2,3 +2,5 @@ io.airlift.units.MinDuration.message=must be greater than or equal to {value} io.airlift.units.MaxDuration.message=must be less than or equal to {value} io.airlift.units.MinDataSize.message=must be greater than or equal to {value} io.airlift.units.MaxDataSize.message=must be less than or equal to {value} +io.airlift.units.MinThreadCount.message=must be greater than or equal to {value} +io.airlift.units.MaxThreadCount.message=must be less than or equal to {value} diff --git a/src/test/java/io/airlift/units/MockMaxThreadCount.java b/src/test/java/io/airlift/units/MockMaxThreadCount.java new file mode 100644 index 0000000..a96becd --- /dev/null +++ b/src/test/java/io/airlift/units/MockMaxThreadCount.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import jakarta.validation.Payload; + +import java.lang.annotation.Annotation; + +import static java.util.Objects.requireNonNull; + +@SuppressWarnings("ClassExplicitlyAnnotation") +class MockMaxThreadCount + implements MaxThreadCount +{ + private final ThreadCount threadCount; + + public MockMaxThreadCount(ThreadCount threadCount) + { + this.threadCount = requireNonNull(threadCount, "threadCount is null"); + } + + @Override + public String value() + { + return String.valueOf(threadCount.getThreadCount()); + } + + @Override + public String message() + { + throw new UnsupportedOperationException(); + } + + @Override + public Class[] groups() + { + throw new UnsupportedOperationException(); + } + + @Override + public Class[] payload() + { + throw new UnsupportedOperationException(); + } + + @Override + public Class annotationType() + { + return MaxThreadCount.class; + } +} diff --git a/src/test/java/io/airlift/units/MockMinThreadCount.java b/src/test/java/io/airlift/units/MockMinThreadCount.java new file mode 100644 index 0000000..039c6e3 --- /dev/null +++ b/src/test/java/io/airlift/units/MockMinThreadCount.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import jakarta.validation.Payload; + +import java.lang.annotation.Annotation; + +import static java.util.Objects.requireNonNull; + +@SuppressWarnings("ClassExplicitlyAnnotation") +class MockMinThreadCount + implements MinThreadCount +{ + private final ThreadCount threadCount; + + public MockMinThreadCount(ThreadCount threadCount) + { + this.threadCount = requireNonNull(threadCount, "threadCount is null"); + } + + @Override + public String value() + { + return String.valueOf(threadCount.getThreadCount()); + } + + @Override + public String message() + { + throw new UnsupportedOperationException(); + } + + @Override + public Class[] groups() + { + throw new UnsupportedOperationException(); + } + + @Override + public Class[] payload() + { + throw new UnsupportedOperationException(); + } + + @Override + public Class annotationType() + { + return MinThreadCount.class; + } +} diff --git a/src/test/java/io/airlift/units/TestThreadCountValidator.java b/src/test/java/io/airlift/units/TestThreadCountValidator.java new file mode 100644 index 0000000..344414c --- /dev/null +++ b/src/test/java/io/airlift/units/TestThreadCountValidator.java @@ -0,0 +1,263 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import jakarta.validation.ValidationException; +import org.testng.annotations.Test; + +import java.util.Optional; + +import static io.airlift.testing.ValidationAssertions.assertFailsValidation; +import static io.airlift.testing.ValidationAssertions.assertValidates; +import static io.airlift.units.ConstraintValidatorAssert.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TestThreadCountValidator +{ + @Test + public void testMaxThreadCountValidator() + { + MaxThreadCountValidator maxValidator = new MaxThreadCountValidator(); + maxValidator.initialize(new MockMaxThreadCount(new ThreadCount(8))); + + assertThat(maxValidator).isValidFor(new ThreadCount(0)); + assertThat(maxValidator).isValidFor(new ThreadCount(5)); + assertThat(maxValidator).isValidFor(new ThreadCount(8)); + } + + @Test + public void testMinThreadCountValidator() + { + MinThreadCountValidator minValidator = new MinThreadCountValidator(); + minValidator.initialize(new MockMinThreadCount(new ThreadCount(4))); + + assertThat(minValidator).isValidFor(new ThreadCount(4)); + assertThat(minValidator).isValidFor(new ThreadCount(5)); + } + + @Test + public void testAllowsNullMinAnnotation() + { + assertValidates(new TestThreadCountValidator.NullMinAnnotation()); + } + + @Test + public void testAllowsNullMaxAnnotation() + { + assertValidates(new TestThreadCountValidator.NullMaxAnnotation()); + } + + @Test + public void testDetectsBrokenMinAnnotation() + { + assertThatThrownBy(() -> assertValidates(new TestThreadCountValidator.BrokenMinAnnotation())) + .isInstanceOf(ValidationException.class) + .hasMessage("HV000032: Unable to initialize io.airlift.units.MinThreadCountValidator.") + .hasRootCauseInstanceOf(NumberFormatException.class) + .hasRootCauseMessage("For input string: \"broken\""); + + assertThatThrownBy(() -> assertValidates(new TestThreadCountValidator.MinAnnotationOnOptional())) + .isInstanceOf(ValidationException.class) + .hasMessageContaining("No validator could be found for constraint 'io.airlift.units.MinThreadCount' validating type 'java.util.Optional'. Check configuration for 'constrainedByMin'"); + + assertThatThrownBy(() -> assertValidates(new TestThreadCountValidator.BrokenOptionalMinAnnotation())) + .isInstanceOf(ValidationException.class) + .hasMessage("HV000032: Unable to initialize io.airlift.units.MinThreadCountValidator.") + .hasRootCauseInstanceOf(NumberFormatException.class) + .hasRootCauseMessage("For input string: \"broken\""); + } + + @Test + public void testDetectsBrokenMaxAnnotation() + { + assertThatThrownBy(() -> assertValidates(new TestThreadCountValidator.BrokenMaxAnnotation())) + .isInstanceOf(ValidationException.class) + .hasMessage("HV000032: Unable to initialize io.airlift.units.MaxThreadCountValidator.") + .hasRootCauseInstanceOf(NumberFormatException.class) + .hasRootCauseMessage("For input string: \"broken\""); + + assertThatThrownBy(() -> assertValidates(new TestThreadCountValidator.MaxAnnotationOnOptional())) + .isInstanceOf(ValidationException.class) + .hasMessageContaining("No validator could be found for constraint 'io.airlift.units.MaxThreadCount' validating type 'java.util.Optional'. Check configuration for 'constrainedByMin'"); + + assertThatThrownBy(() -> assertValidates(new TestThreadCountValidator.BrokenOptionalMaxAnnotation())) + .isInstanceOf(ValidationException.class) + .hasMessage("HV000032: Unable to initialize io.airlift.units.MaxThreadCountValidator.") + .hasRootCauseInstanceOf(NumberFormatException.class) + .hasRootCauseMessage("For input string: \"broken\""); + } + + @Test + public void testPassesValidation() + { + assertValidates(new TestThreadCountValidator.ConstrainedThreadCount(new ThreadCount(7))); + assertValidates(new TestThreadCountValidator.ConstrainedOptionalThreadCount(Optional.of(new ThreadCount(7)))); + assertValidates(new TestThreadCountValidator.ConstrainedOptionalThreadCount(Optional.empty())); + assertValidates(new TestThreadCountValidator.ConstrainedOptionalThreadCount(null)); + } + + @Test + public void testFailsMaxThreadCountConstraint() + { + assertFailsValidation(new TestThreadCountValidator.ConstrainedThreadCount(new ThreadCount(11)), "constrainedByMinAndMax", "must be less than or equal to 10", MaxThreadCount.class); + assertFailsValidation(new TestThreadCountValidator.ConstrainedThreadCount(new ThreadCount(11)), "constrainedByMax", "must be less than or equal to 10", MaxThreadCount.class); + + assertFailsValidation(new TestThreadCountValidator.ConstrainedOptionalThreadCount(Optional.of(new ThreadCount(11))), "constrainedByMinAndMax", "must be less than or equal to 10", MaxThreadCount.class); + assertFailsValidation(new TestThreadCountValidator.ConstrainedOptionalThreadCount(Optional.of(new ThreadCount(11))), "constrainedByMax", "must be less than or equal to 10", MaxThreadCount.class); + } + + @Test + public void testFailsMinThreadCountConstraint() + { + assertFailsValidation(new TestThreadCountValidator.ConstrainedThreadCount(new ThreadCount(1)), "constrainedByMin", "must be greater than or equal to 5", MinThreadCount.class); + assertFailsValidation(new TestThreadCountValidator.ConstrainedThreadCount(new ThreadCount(1)), "constrainedByMinAndMax", "must be greater than or equal to 5", MinThreadCount.class); + + assertFailsValidation(new TestThreadCountValidator.ConstrainedOptionalThreadCount(Optional.of(new ThreadCount(1))), "constrainedByMin", "must be greater than or equal to 5", MinThreadCount.class); + assertFailsValidation(new TestThreadCountValidator.ConstrainedOptionalThreadCount(Optional.of(new ThreadCount(1))), "constrainedByMinAndMax", "must be greater than or equal to 5", MinThreadCount.class); + } + + @SuppressWarnings("UnusedDeclaration") + public static class ConstrainedThreadCount + { + private final ThreadCount threadCount; + + public ConstrainedThreadCount(ThreadCount threadCount) + { + this.threadCount = threadCount; + } + + @MinThreadCount("5") + public ThreadCount getConstrainedByMin() + { + return threadCount; + } + + @MaxThreadCount("10") + public ThreadCount getConstrainedByMax() + { + return threadCount; + } + + @MinThreadCount("5") + @MaxThreadCount("10") + public ThreadCount getConstrainedByMinAndMax() + { + return threadCount; + } + } + + @SuppressWarnings("UnusedDeclaration") + public static class ConstrainedOptionalThreadCount + { + private final Optional threadCount; + + public ConstrainedOptionalThreadCount(Optional threadCount) + { + this.threadCount = threadCount; + } + + public Optional<@MinThreadCount("5") ThreadCount> getConstrainedByMin() + { + return threadCount; + } + + public Optional<@MaxThreadCount("10") ThreadCount> getConstrainedByMax() + { + return threadCount; + } + + public Optional<@MinThreadCount("5") @MaxThreadCount("10") ThreadCount> getConstrainedByMinAndMax() + { + return threadCount; + } + } + + public static class NullMinAnnotation + { + @SuppressWarnings("UnusedDeclaration") + @MinThreadCount("16") + public ThreadCount getConstrainedByMin() + { + return null; + } + } + + public static class NullMaxAnnotation + { + @SuppressWarnings("UnusedDeclaration") + @MaxThreadCount("16") + public ThreadCount getConstrainedByMin() + { + return null; + } + } + + public static class BrokenMinAnnotation + { + @SuppressWarnings("UnusedDeclaration") + @MinThreadCount("broken") + public ThreadCount getConstrainedByMin() + { + return new ThreadCount(32); + } + } + + public static class BrokenMaxAnnotation + { + @SuppressWarnings("UnusedDeclaration") + @MaxThreadCount("broken") + public ThreadCount getConstrainedByMin() + { + return new ThreadCount(32); + } + } + + public static class MinAnnotationOnOptional + { + @SuppressWarnings("UnusedDeclaration") + @MinThreadCount("16") + public Optional getConstrainedByMin() + { + return Optional.empty(); + } + } + + public static class MaxAnnotationOnOptional + { + @SuppressWarnings("UnusedDeclaration") + @MaxThreadCount("16") + public Optional getConstrainedByMin() + { + return Optional.empty(); + } + } + + public static class BrokenOptionalMinAnnotation + { + @SuppressWarnings("UnusedDeclaration") + public Optional<@MinThreadCount("broken") ThreadCount> getConstrainedByMin() + { + return Optional.empty(); + } + } + + public static class BrokenOptionalMaxAnnotation + { + @SuppressWarnings("UnusedDeclaration") + public Optional<@MaxThreadCount("broken") ThreadCount> getConstrainedByMax() + { + return Optional.empty(); + } + } +} diff --git a/src/test/java/io/airlift/units/TestThreadsCount.java b/src/test/java/io/airlift/units/TestThreadsCount.java new file mode 100644 index 0000000..6f8d510 --- /dev/null +++ b/src/test/java/io/airlift/units/TestThreadsCount.java @@ -0,0 +1,98 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import org.testng.annotations.Test; + +import static io.airlift.units.ThreadCount.MachineInfo.getAvailablePhysicalProcessorCount; +import static java.lang.Math.round; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +public class TestThreadsCount +{ + public static final int AVAILABLE_PROCESSORS = getAvailablePhysicalProcessorCount(); + + @Test + public void ensureRequiredNumberOfProcessors() + { + // For tests to work properly we need at least two threads + assertThat(AVAILABLE_PROCESSORS).isGreaterThan(1); + } + + @Test + public void testParsingIntegerValues() + { + assertThreadsCount("1", 1); + assertThreadsCount("2", 2); + assertThreadsCount("67", 67); + assertThreadsCount("0", 0); + assertThreadsCount(Integer.valueOf(Integer.MAX_VALUE).toString(), Integer.MAX_VALUE); + assertInvalidValue("-1", "Thread count cannot be negative"); + assertInvalidValue("67.0", "Cannot parse value '67.0' as integer"); + assertInvalidValue(Long.valueOf(((long) Integer.MAX_VALUE) + 1).toString(), "Thread count is greater than 2^32 - 1"); + } + + @Test + public void testParsingMultiplierPerCore() + { + assertThreadsCount("1C", AVAILABLE_PROCESSORS); + assertThreadsCount("0.5 C", AVAILABLE_PROCESSORS / 2); + assertThreadsCount("0.2 C", round(AVAILABLE_PROCESSORS / 5.0f)); + assertThreadsCount("1.5C", round(AVAILABLE_PROCESSORS * 1.5f)); + assertThreadsCount("2 C", AVAILABLE_PROCESSORS * 2); + assertThreadsCount("0.0001 C", 0); + assertInvalidValue("-0.0001 C", "Thread multiplier cannot be negative"); + assertInvalidValue(-1, "Thread count cannot be negative"); + assertInvalidValue("-1C", "Thread multiplier cannot be negative"); + assertInvalidValue("-1SC", "Cannot parse value '-1S' as float"); + assertInvalidValue("2147483647C", "Thread count is greater than 2^32 - 1"); + assertInvalidValue("3147483648C", "Thread count is greater than 2^32 - 1"); + } + + @Test + public void testParsingBoundedValue() + { + assertBoundedThreadsCount("3", "1", "1", 1); + assertBoundedThreadsCount("256C", "4", "16", 16); + assertBoundedThreadsCount("3", "4", "16", 4); + } + + private void assertThreadsCount(String value, int expected) + { + ThreadCount threadCount = ThreadCount.valueOf(value); + assertThat(threadCount).isEqualTo(ThreadCount.exactValueOf(expected)); + assertThat(threadCount.getThreadCount()).isEqualTo(expected); + } + + private void assertBoundedThreadsCount(String value, String min, String max, int expected) + { + ThreadCount threadCount = ThreadCount.boundedValueOf(value, min, max); + assertThat(threadCount).isEqualTo(new ThreadCount(expected)); + } + + private void assertInvalidValue(String value, String expectedMessage) + { + assertThatThrownBy(() -> ThreadCount.valueOf(value)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(expectedMessage); + } + + private void assertInvalidValue(int value, String expectedMessage) + { + assertThatThrownBy(() -> new ThreadCount(value)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(expectedMessage); + } +}