From 91a00618d8a751fc8e605b71b477ff4fea43d162 Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:23:49 +0200 Subject: [PATCH 01/34] Rebuild as independent Java concurrency library --- .github/workflows/ci.yml | 53 +++++ .gitignore | 8 + LICENSE | 222 ++---------------- README.md | 115 ++++++++- calculator/Accumulator.java | 145 ------------ calculator/ChunkSizeCalculator.java | 147 ------------ calculator/RatioCalculator.java | 69 ------ pom.xml | 176 ++++++++++++++ shared/SharedFiFoLockQueue.java | 102 -------- ...aredFiFoLockQueueInterruptedException.java | 11 - .../SharedFiFoLockQueueReleasedException.java | 11 - shared/SharedFiFoQueue.java | 210 ----------------- shared/SharedFiFoSleepingQueue.java | 50 ---- shared/SharedFifoQueueMode.java | 25 -- .../BoundedBlockingQueueBenchmark.java | 51 ++++ .../concurrent/BoundedBlockingQueue.java | 220 +++++++++++++++++ .../concurrent/QueueClosedException.java | 8 + .../concurrent/ResettableLongAccumulator.java | 47 ++++ .../concurrent/BoundedBlockingQueueTest.java | 210 +++++++++++++++++ .../ResettableLongAccumulatorTest.java | 64 +++++ 20 files changed, 969 insertions(+), 975 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore delete mode 100644 calculator/Accumulator.java delete mode 100644 calculator/ChunkSizeCalculator.java delete mode 100644 calculator/RatioCalculator.java create mode 100644 pom.xml delete mode 100644 shared/SharedFiFoLockQueue.java delete mode 100644 shared/SharedFiFoLockQueueInterruptedException.java delete mode 100644 shared/SharedFiFoLockQueueReleasedException.java delete mode 100644 shared/SharedFiFoQueue.java delete mode 100644 shared/SharedFiFoSleepingQueue.java delete mode 100644 shared/SharedFifoQueueMode.java create mode 100644 src/jmh/java/io/github/azamir911/concurrent/BoundedBlockingQueueBenchmark.java create mode 100644 src/main/java/io/github/azamir911/concurrent/BoundedBlockingQueue.java create mode 100644 src/main/java/io/github/azamir911/concurrent/QueueClosedException.java create mode 100644 src/main/java/io/github/azamir911/concurrent/ResettableLongAccumulator.java create mode 100644 src/test/java/io/github/azamir911/concurrent/BoundedBlockingQueueTest.java create mode 100644 src/test/java/io/github/azamir911/concurrent/ResettableLongAccumulatorTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..33bdd50 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + name: Java 21 validation + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + cache: maven + - name: Compile, test, and generate coverage report + run: mvn -B -ntp verify + + benchmark-smoke: + name: JMH smoke test + needs: validate + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + cache: maven + - name: Build benchmark executable + run: mvn -B -ntp -Pbenchmarks -DskipTests package + - name: Run short benchmark + run: >- + java -jar target/benchmarks.jar BoundedBlockingQueueBenchmark + -wi 1 -i 1 -f 1 -w 100ms -r 100ms diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94c5f2c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +target/ +.idea/ +*.iml +.classpath +.project +.settings/ +.DS_Store +hs_err_pid* diff --git a/LICENSE b/LICENSE index 261eeb9..0d89c4a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,21 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. +MIT License + +Copyright (c) 2026 Avishay Zamir + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 9bd3b23..60b4cd1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,112 @@ -# my-private +# Java Concurrency Primitives -Accumulator -SharedQueue -Queue +[![CI](https://github.com/azamir911/java-concurrency-primitives/actions/workflows/ci.yml/badge.svg)](https://github.com/azamir911/java-concurrency-primitives/actions/workflows/ci.yml) + +A small Java 21 library that explores explicit concurrency contracts instead of hiding lifecycle and backpressure behind a framework. + +The project currently contains: + +- `BoundedBlockingQueue`: a FIFO ring buffer backed by `ReentrantLock` and two `Condition` instances. +- `ResettableLongAccumulator`: a lock-free accumulator built on `AtomicLong` with atomic snapshot-and-reset semantics. +- deterministic unit tests, producer-consumer stress coverage, JaCoCo reports, and JMH benchmarks. + +## Why this project exists + +Concurrent data structures become difficult when shutdown behavior is left implicit. This implementation treats lifecycle as part of the API: + +- capacity provides backpressure rather than unbounded memory growth; +- producers block while the queue is full; +- consumers block while the queue is empty; +- `close()` rejects future writes and wakes all waiting threads; +- accepted elements remain available until the closed queue is drained; +- interruption is propagated through all blocking operations. + +## Requirements + +- Java 21 or later +- Maven 3.9 or later + +## Build and test + +```bash +mvn verify +``` + +The verification phase compiles the library, runs unit and concurrency stress tests, and creates a JaCoCo report under `target/site/jacoco`. + +## Queue example + +```java +BoundedBlockingQueue queue = new BoundedBlockingQueue<>(128); + +Thread producer = Thread.ofVirtual().start(() -> { + try { + queue.put("event-1"); + queue.put("event-2"); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + } finally { + queue.close(); + } +}); + +try { + while (true) { + System.out.println(queue.take()); + } +} catch (QueueClosedException drained) { + // The producer closed the queue and all accepted elements were consumed. +} + +producer.join(); +``` + +## Queue lifecycle + +| State | `put` / `offer` | `take` | `poll` | +|---|---|---|---| +| Open with capacity | Accepts an element | Returns or waits | Returns or times out | +| Open and full | Waits or times out | Returns an element | Returns an element | +| Closed with buffered data | Throws `QueueClosedException` | Drains buffered data | Drains buffered data | +| Closed and drained | Throws `QueueClosedException` | Throws `QueueClosedException` | Returns empty | + +Calling `close()` is idempotent. It does not discard elements that were already accepted. + +## Accumulator example + +```java +ResettableLongAccumulator bytes = ResettableLongAccumulator.sum(); + +bytes.accumulate(1_024); +bytes.accumulate(2_048); + +long intervalTotal = bytes.getAndReset(); // 3072 +``` + +Custom associative operations are supported: + +```java +ResettableLongAccumulator peak = + ResettableLongAccumulator.max(Long.MIN_VALUE); +``` + +## Benchmarks + +Build the JMH executable: + +```bash +mvn -Pbenchmarks -DskipTests package +java -jar target/benchmarks.jar +``` + +Benchmark results depend on the machine, JVM, queue capacity, and producer-consumer ratio. The repository intentionally includes the harness rather than publishing a hardware-independent performance claim. + +## Design notes + +`BoundedBlockingQueue` uses a fixed-size circular array. All structural state is protected by one lock, while separate `notEmpty` and `notFull` conditions avoid waking unrelated waiters. The implementation supports an optional fair lock when starvation resistance is more important than maximum throughput. + +`ResettableLongAccumulator` delegates updates to `AtomicLong.accumulateAndGet`. The function may be retried under contention, so it must be side-effect free and associative. + +## License + +MIT diff --git a/calculator/Accumulator.java b/calculator/Accumulator.java deleted file mode 100644 index c89c770..0000000 --- a/calculator/Accumulator.java +++ /dev/null @@ -1,145 +0,0 @@ -package com.calculator; - -import lombok.AccessLevel; -import lombok.experimental.FieldDefaults; - -/** - * Created by Avishay Zamir on 02/01/2020 - * - * A mutable data type that calculates: 1. mean 2. sample variance 3. sample standard deviation 4. maximum of a stream - * of real numbers a stable, one-pass algorithm. - * - * Help can be found here: https://www.calculator.net/standard-deviation-calculator.html - */ -@FieldDefaults(level = AccessLevel.PRIVATE) -public class Accumulator { - - // number of data values - int n; - - // sample variance * (n-1) - double squareSum; - - // sample mean (Average) - double mean; - - // max value - double max; - - // min value - double min; - - // sum of all values - double sum; - - public Accumulator() { - setValues(); - } - - public void reset() { - setValues(); - } - - private void setValues() { - this.n = 0; - this.squareSum = 0.0; - this.mean = 0.0; - this.max = Double.NEGATIVE_INFINITY; - this.min = Double.POSITIVE_INFINITY; - this.sum = 0.0; - } - - /** - * Adds the specified value to the accumulator. - * - * @param value the value - */ - public void add(double value) { - synchronized (this) { - this.n++; - double delta = value - this.mean; - this.mean += delta / this.n; - this.squareSum += (double) (this.n - 1) / this.n * delta * delta; - this.max = Math.max(max, value); - this.min = Math.min(min, value); - this.sum += value; - } - } - - /** - * Returns the mean of the data values (Average). - * - * @return the mean of the data values - */ - public double mean() { - return this.mean; - } - - /** - * Returns the sample variance of the data values. - * - * @return the sample variance of the data values - */ - public double variance() { - if (this.n <= 1) { - return Double.NaN; - } - return this.squareSum / (this.n - 1); - } - - /** - * Returns the sample standard deviation of the data values (StdDev). - * - * @return the sample standard deviation of the data values - */ - public double standardDeviation() { - return Math.sqrt(this.variance()); - } - - /** - * Returns the maximum value of the data values - * - * @return the maximum value of the data values - */ - public double max() { - return this.max; - } - - /** - * Returns the minimum value of the data values - * - * @return the minimum value of the data values - */ - public double min() { - return this.min; - } - - /** - * Returns the number of data values. - * - * @return the number of data values - */ - public int count() { - return this.n; - } - - /** - * Return the summarize of data values - * - * @return the summarize of data values - */ - public double sum() { - return this.sum; - } - - /** - * Returns a string representation of this accumulator. - * - * @return a string representation of this accumulator - */ - public String toString() { - return "Count = " + count() + ", Mean = " + mean() + ", Sample Standard Deviation = " + standardDeviation() - + ", Sample Variance = " + variance() + ", Max = " + max() + ", Min = " + min() + ", Sum = " + sum(); - } - -} diff --git a/calculator/ChunkSizeCalculator.java b/calculator/ChunkSizeCalculator.java deleted file mode 100644 index 9a92b33..0000000 --- a/calculator/ChunkSizeCalculator.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.sisense.build.connector.calculator; - -import com.sisense.build.connector.contracts.ColumnData; -import com.sisense.build.connector.monitor.MonitoringParameters; -import com.sisense.build.connector.pool.chunk.Chunk; -import com.sisense.build.connector.property.DoublePropertyKeys; -import com.sisense.build.connector.property.IntegerPropertyKeys; -import com.sisense.build.connector.bytes.serialization.Serializer; -import com.sisense.build.connector.bytes.serialization.SerializerException; -import com.sisense.common.propertymanager.PropertyManager; -import lombok.AccessLevel; -import lombok.experimental.FieldDefaults; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import javax.inject.Inject; -import java.math.RoundingMode; -import java.nio.ByteBuffer; -import java.util.List; - -/** - * The ChunkSizeCalculator class will calculate the chunk's size in bytes. It will now to calculate the header section - * and the rows section, based on that it will be able to assuming better how many rows should be in each chunk. The - * bufferSize is the maximum size for a chunk in bytes. The factor size is the parameter to evaluate how many records - * will be in the chunk based on the last calculation - */ -@Slf4j -@Component -@FieldDefaults(level = AccessLevel.PRIVATE) -public class ChunkSizeCalculator { - - // The declared buffer size - final int bufferSize; - // the declared factor - final double factor; - // the declared retries - final int sumRetries; - - final RatioCalculator ratioCalculator = new RatioCalculator(0); - - @Inject - private Serializer serializer; - - public ChunkSizeCalculator(PropertyManager propertyManager) { - // Getting the declared buffer size from the ZK - this.bufferSize = propertyManager.getIntPropertyValue(IntegerPropertyKeys.DEFAULT_CHUNK_SIZE.getKeys()); - // The value to multiple with the end result - this.factor = propertyManager.getDoublePropertyValue(DoublePropertyKeys.CHUNK_SIZE_FACTOR.getKeys()); - // The number of retries to do serialize for a Chunk - this.sumRetries = propertyManager.getIntPropertyValue(IntegerPropertyKeys.MAX_SERIALIZE_RETRIES.getKeys()); - } - - /** - * - * @param chunk the chunk on which the calculation will be based - * @param columns the columns in the header of the chunk - * @return the number of rows that should be in each chunk - */ - public int calculate(Chunk chunk, List columns, MonitoringParameters monitoringParameters) - throws Exception { - // If the chunk is empty so no rows to based on the calculation. Return 0. - if (chunk.size() == 0) { - log.debug("BE#293606 Tried to calculated empty Chunk, no data to read"); - return 0; - } - - // Size of buffer without rows, only headers and finish bytes - - // Calculate the columns size in bytes - int columnsSizeInBytes = sizeOf(null, columns, monitoringParameters); - log.debug("BE#135485 The columns' size is '{}' bytes", columnsSizeInBytes); - // If the columns size are bigger than the buffer size so returning the current chunk size. - // That means the size of each chunk will the the value of the NUMBER_OF_RECORDS_FOR_REVALUE_CHUNK_SIZE property - if (columnsSizeInBytes > bufferSize) { - log.warn("BE#733499 Headers size is bigger then default buffer size. Setting number of rows in Chunk " + - "as number of rows in sample Chunk"); - return chunk.size(); - } - - // Calculate the chunk size and columns size in bytes together - int totalSizeInBytes = sizeOf(chunk, columns, monitoringParameters); - log.debug("BE#974139 The total size is: '{}' bytes", totalSizeInBytes); - // Size of byte buffer with out headers and finish bytes - - // Calculate the chunk size in bytes - int rowsSizeInBytes = totalSizeInBytes - columnsSizeInBytes; - log.debug("BE#395536 The rows' size is: '{}' bytes", rowsSizeInBytes); - - // If the chunk is empty so no rows to based on the calculation. Return 0. - if (rowsSizeInBytes == 0) { - log.debug("BE#424455 The Chunk's size in bytes is 0. Setting the number of rows in Chunk as number of " + - "rows in sample Chunk"); - return chunk.size(); - } - - // Calculate the number of rows that fit in a buffer with ChunkSize - // Calculating the number of rows in a chunk - // 1. Reduce the columns size from the buffer size - // 2. Calculating the ratio between #1 and rowsSizeInBytes - // 3. Calculating how many rows on the chunk based on the ratio and the chunk's records size - // 4. Multiple the result with the factor for a final result - final int numerator = bufferSize - columnsSizeInBytes; - final double ratio = ratioCalculator.calculate(numerator, rowsSizeInBytes, 10, RoundingMode.UP); - final double evaluatedRows = ratio * chunk.size(); - final int calculatedRows = (int) (evaluatedRows * factor); - if (calculatedRows == 0) { - log.debug("BE#587383 Calculated rows in chunk is 0. Setting number of rows in chunk as number of " + - "rows in sample chunk"); - return chunk.size(); - } - - return calculatedRows; - } - - /** - * Calculating the chunk size and the columns size after a serialization - * - * @param chunk the chunk to calculate - * @param columns the columns to calculate - * @return the size in bytes - */ - private int sizeOf(Chunk chunk, List columns, MonitoringParameters monitoringParameters) - throws Exception { - int retries = 1; - - // Doing a serialization with only the columns - ByteBuffer serializedBuffer = null; - - while (serializedBuffer == null && retries <= sumRetries) { - try { - retries++; - serializedBuffer = serializer.serialize(chunk, columns, monitoringParameters); - } catch (SerializerException e) { - log.warn("BE#782473 Got exception from Serializer. Retrying {}/{}", retries, sumRetries); - } - } - - if (serializedBuffer == null) { - throw new Exception("BE#472360 Could not estimate chunk size"); - } - - // The position is the size of the serialized columns - int sizeOfChunkInBytes = serializedBuffer.position(); - serializer.put(serializedBuffer); - return sizeOfChunkInBytes; - } -} diff --git a/calculator/RatioCalculator.java b/calculator/RatioCalculator.java deleted file mode 100644 index 4bdd250..0000000 --- a/calculator/RatioCalculator.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.calculator; - -import lombok.AllArgsConstructor; -import lombok.Data; - -import java.math.BigDecimal; -import java.math.RoundingMode; - -/** - * The ratio calculator that gets 2 double numbers and return the ratio based on the scale and rounding mode - */ -@Data -@AllArgsConstructor -public class RatioCalculator { - - double factor; - - /** - * Return the ratio based on the numerator and denominator. Scale is 1 and rounding mode is up - * - * @param numerator the numerator - * @param denominator the denominator - * @return the ratio - */ - public double calculate(double numerator, double denominator) { - return calculate(numerator, denominator, 1, RoundingMode.UP); - } - - /** - * Return the ratio based on the numerator, denominator and scale. Rounding mode is up - * - * @param numerator the numerator - * @param denominator the denominator - * @param scale the scale - * @return the ratio - */ - public double calculate(double numerator, double denominator, int scale) { - return calculate(numerator, denominator, scale, RoundingMode.UP); - } - - /** - * Return the ratio based on the numerator, denominator and rounding mode. Scale is 1 - * - * @param numerator the numerator - * @param denominator the denominator - * @param roundingMode the rounding mode - * @return the ratio - */ - public double calculate(double numerator, double denominator, RoundingMode roundingMode) { - return calculate(numerator, denominator, 1, roundingMode); - } - - /** - * Return the ratio based on the numerator, denominator, rounding mode and scale. - * - * @param numerator the numerator - * @param denominator the denominator - * @param scale the scale - * @param roundingMode the rounding mode - * @return the ratio - */ - public double calculate(double numerator, double denominator, int scale, RoundingMode roundingMode) { - BigDecimal theNumerator = new BigDecimal(numerator); - BigDecimal theDenominator = new BigDecimal(denominator); - final BigDecimal divide = theNumerator.divide(theDenominator, scale, roundingMode); - return divide.add(new BigDecimal(factor)).doubleValue(); - } - -} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a43bf71 --- /dev/null +++ b/pom.xml @@ -0,0 +1,176 @@ + + + 4.0.0 + + io.github.azamir911 + java-concurrency-primitives + 1.0.0-SNAPSHOT + Java Concurrency Primitives + Dependency-free Java concurrency primitives with explicit lifecycle semantics. + https://github.com/azamir911/java-concurrency-primitives + + + + MIT License + https://opensource.org/license/mit + repo + + + + + 21 + UTF-8 + 5.11.4 + 1.37 + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + false + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.5.0 + + + enforce-build-environment + + enforce + + + + + [21,) + + + [3.9,) + + + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + + prepare-agent + + + + report + verify + + report + + + + + + + + + + benchmarks + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + provided + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.0 + + + add-jmh-sources + generate-sources + + add-source + + + + src/jmh/java + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + benchmarks + false + + + + org.openjdk.jmh.Main + + + + + + + + + + + diff --git a/shared/SharedFiFoLockQueue.java b/shared/SharedFiFoLockQueue.java deleted file mode 100644 index 0b855a0..0000000 --- a/shared/SharedFiFoLockQueue.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.collection.shared; - -import lombok.AccessLevel; -import lombok.NonNull; -import lombok.experimental.FieldDefaults; -import lombok.extern.slf4j.Slf4j; - -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.ReentrantLock; - -@Slf4j -@FieldDefaults(level = AccessLevel.PRIVATE) -public class SharedFiFoLockQueue extends SharedFiFoQueue { - - /** Main lock guarding all access */ - final ReentrantLock lock = new ReentrantLock(); - - /** Condition for waiting takes */ - final Condition notEmpty = lock.newCondition(); - - /** Condition for waiting puts */ - final Condition notFull = lock.newCondition(); - - public SharedFiFoLockQueue(int capacity) { - super(capacity); - } - - @Override - public boolean add(@NonNull T elem) { - final ReentrantLock lock = this.lock; - try { - lock.lockInterruptibly(); - - doAdd(elem); - - notEmpty.signal(); - } catch (InterruptedException e) { - throw new SharedFiFoLockQueueInterruptedException("BE#812296 " + e.getMessage()); - } - finally { - lock.unlock(); - } - - return true; - } - - @Override - protected void doAddWait() { - log.debug("BE#040523 Trying to add Element to the Queue while it full. Waiting for signal"); - try { - notFull.await(); - } catch (InterruptedException e) { - throw new SharedFiFoLockQueueInterruptedException("BE#010208 " + e.getMessage()); - } - } - - public T remove() { - final ReentrantLock lock = this.lock; - - try { - lock.lockInterruptibly(); - - final T element = doRemove(); - - notFull.signal(); - - return element; - } catch (InterruptedException e) { - throw new SharedFiFoLockQueueInterruptedException("BE#555292 " + e.getMessage()); - } - finally { - lock.unlock(); - } - } - - @Override - protected void doRemoveWait() { - log.debug("BE#298013 Trying to get Element from the Queue while it empty. Waiting for signal"); - try { - notEmpty.await(); - } catch (InterruptedException e) { - throw new SharedFiFoLockQueueInterruptedException("BE#562572 " + e.getMessage()); - } - } - - @Override - protected void doClear() { - fullSignal(); - } - - private void fullSignal() { - lock.lock(); - try { - notEmpty.signal(); - notFull.signal(); - } - finally { - lock.unlock(); - } - } - -} diff --git a/shared/SharedFiFoLockQueueInterruptedException.java b/shared/SharedFiFoLockQueueInterruptedException.java deleted file mode 100644 index 7d7859e..0000000 --- a/shared/SharedFiFoLockQueueInterruptedException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.collection.shared; - -public class SharedFiFoLockQueueInterruptedException extends RuntimeException { - - private static final long serialVersionUID = 4552016483312675750L; - - public SharedFiFoLockQueueInterruptedException(String message) { - super(message); - } - -} diff --git a/shared/SharedFiFoLockQueueReleasedException.java b/shared/SharedFiFoLockQueueReleasedException.java deleted file mode 100644 index 62029d5..0000000 --- a/shared/SharedFiFoLockQueueReleasedException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.collection.shared; - -public class SharedFiFoLockQueueReleasedException extends RuntimeException { - - private static final long serialVersionUID = -2628636830370021723L; - - public SharedFiFoLockQueueReleasedException(String message) { - super(message); - } - -} diff --git a/shared/SharedFiFoQueue.java b/shared/SharedFiFoQueue.java deleted file mode 100644 index ba94268..0000000 --- a/shared/SharedFiFoQueue.java +++ /dev/null @@ -1,210 +0,0 @@ -package com.collection.shared; - -import lombok.AccessLevel; -import lombok.experimental.FieldDefaults; -import lombok.extern.slf4j.Slf4j; - -import java.util.AbstractQueue; -import java.util.ConcurrentModificationException; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.stream.IntStream; - -@Slf4j -@FieldDefaults(level = AccessLevel.PRIVATE) -public abstract class SharedFiFoQueue extends AbstractQueue { - - final Object[] elements; - - int size = 0; - int placeIndex = 0; - int removeIndex = 0; - - private boolean isReleased = false; - private boolean releaseLocks = false; - - public SharedFiFoQueue(int capacity) { - this.elements = new Object[capacity]; - } - - @Override - public abstract boolean add(T elem); - - @Override - public boolean offer(T t) { - return add(t); - } - - protected final void doAdd(T elem) { - while (size >= elements.length && !isReleased) { - doAddWait(); - } - - inc(); - - if (isReleased) { - throw new SharedFiFoLockQueueReleasedException("BE#700157 Released by the user"); - } - - elements[placeIndex] = elem; - - // We need the modulo, in order to avoid going out of bounds. - placeIndex = (placeIndex + 1) % elements.length; - } - - protected abstract void doAddWait(); - - @Override - public abstract T remove(); - - @Override - public T poll() { - T element = peek(); - - if (element != null) { - dec(); - - // We need the modulo, in order to avoid going out of bounds. - removeIndex = (removeIndex + 1) % elements.length; - } - - return element; - } - - @SuppressWarnings("unchecked") - @Override - public T peek() { - if (releaseLocks && isEmpty()) { - return null; - } - - if (isReleased) { - throw new SharedFiFoLockQueueReleasedException("BE#547121 Released by the user"); - } - - return (T) elements[removeIndex]; - } - - protected final T doRemove() { - while (size <= 0 && !isReleased && !releaseLocks) { - doRemoveWait(); - } - - return poll(); - } - - protected abstract void doRemoveWait(); - - private void inc() { - addToSize(1); - } - - private void dec() { - addToSize(-1); - } - - private synchronized void addToSize(int value) { - size = size + value; - } - - @Override - public Iterator iterator() { - return new Itr(); - } - - @Override - public int size() { - return this.size; - } - - @Override - public boolean isEmpty() { - return this.size == 0; - } - - @Override - public void clear() { - this.isReleased = true; - doClear(); - IntStream.range(0, elements.length).forEach(value -> elements[value] = null); - } - - protected abstract void doClear(); - - public void releaseLocks() { - this.releaseLocks = true; - doClear(); - } - - /** - * Returns {@code true} if this list contains the specified element. More formally, returns {@code true} if and only - * if this list contains at least one element {@code e} such that {@code Objects.equals(o, e)}. - * - * @param o element whose presence in this list is to be tested - * @return {@code true} if this list contains the specified element - */ - @Override - public boolean contains(Object o) { - return indexOf(o) >= 0; - } - - /** - * Returns the index of the first occurrence of the specified element in this list, or -1 if this list does not - * contain the element. More formally, returns the lowest index {@code i} such that - * {@code Objects.equals(o, get(i))}, or -1 if there is no such index. - */ - public int indexOf(Object o) { - return indexOfRange(o, 0, elements.length); - } - - protected int indexOfRange(Object o, int start, int end) { - Object[] es = elements; - if (o == null) { - for (int i = start; i < end; i++) { - if (es[i] == null) { - return i; - } - } - } - else { - for (int i = start; i < end; i++) { - if (o.equals(es[i])) { - return i; - } - } - } - return -1; - } - - /** - * An optimized version of AbstractList.Itr - */ - private class Itr implements Iterator { - - int counter = 0; - int cursor = removeIndex; // index of next element to return - int lastRet = -1; // index of last element returned; -1 if no such - - public boolean hasNext() { - return counter != size(); - } - - @SuppressWarnings("unchecked") - public T next() { - int i = cursor; - if (counter >= size()) { - throw new NoSuchElementException("BE#162250 Element " + i + " does not exists"); - } - if (i >= elements.length) { - throw new ConcurrentModificationException("BE#059680 Trying to get element greater than the size"); - } - - // We need the modulo, in order to avoid going out of bounds. - cursor = (cursor + 1) % elements.length; - counter++; - - return (T) elements[lastRet = i]; - } - } - -} diff --git a/shared/SharedFiFoSleepingQueue.java b/shared/SharedFiFoSleepingQueue.java deleted file mode 100644 index adb0cf8..0000000 --- a/shared/SharedFiFoSleepingQueue.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.collection.shared; - -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class SharedFiFoSleepingQueue extends SharedFiFoQueue { - - public SharedFiFoSleepingQueue(int capacity) { - super(capacity); - } - - @Override - public boolean add(@NonNull T elem) { - doAdd(elem); - - return true; - } - - @Override - protected void doAddWait() { - log.debug("BE#040524 Trying to add Element to the Queue while it full. Sleeping for 100 milliseconds"); - try { - Thread.sleep(100); - } catch (InterruptedException e) { - throw new SharedFiFoLockQueueInterruptedException("BE#417414 " + e.getMessage()); - } - } - - public T remove() { - return doRemove(); - } - - @Override - protected void doRemoveWait() { - log.debug("BE#298014 Trying to get Element from the Queue while it empty. Sleeping for 100 milliseconds"); - try { - Thread.sleep(100); - } catch (InterruptedException e) { - throw new SharedFiFoLockQueueInterruptedException("BE#709989 " + e.getMessage()); - } - - } - - @Override - protected void doClear() { - - } - -} diff --git a/shared/SharedFifoQueueMode.java b/shared/SharedFifoQueueMode.java deleted file mode 100644 index 1284eb7..0000000 --- a/shared/SharedFifoQueueMode.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.collection.shared; - -import lombok.AccessLevel; -import lombok.experimental.FieldDefaults; - -@FieldDefaults(level = AccessLevel.PRIVATE) -public enum SharedFifoQueueMode { - LOCK, SLEEPING; - - final String value; - - SharedFifoQueueMode() { - this.value = this.name().toLowerCase(); - } - - public static SharedFifoQueueMode fromValue(String text, SharedFifoQueueMode defaultValue) { - final String s = text.toLowerCase(); - for (SharedFifoQueueMode b : SharedFifoQueueMode.values()) { - if (b.value.equals(s)) { - return b; - } - } - return defaultValue; - } -} diff --git a/src/jmh/java/io/github/azamir911/concurrent/BoundedBlockingQueueBenchmark.java b/src/jmh/java/io/github/azamir911/concurrent/BoundedBlockingQueueBenchmark.java new file mode 100644 index 0000000..30cfc3b --- /dev/null +++ b/src/jmh/java/io/github/azamir911/concurrent/BoundedBlockingQueueBenchmark.java @@ -0,0 +1,51 @@ +package io.github.azamir911.concurrent; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Group; +import org.openjdk.jmh.annotations.GroupThreads; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Warmup(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +public class BoundedBlockingQueueBenchmark { + @State(Scope.Group) + public static class QueueState { + @Param({"1024"}) + int capacity; + + BoundedBlockingQueue queue; + + @Setup(Level.Iteration) + public void setUp() { + queue = new BoundedBlockingQueue<>(capacity); + } + } + + @Benchmark + @Group("producerConsumer") + @GroupThreads(1) + public void put(QueueState state) throws InterruptedException { + state.queue.put(1); + } + + @Benchmark + @Group("producerConsumer") + @GroupThreads(1) + public int take(QueueState state) throws InterruptedException { + return state.queue.take(); + } +} diff --git a/src/main/java/io/github/azamir911/concurrent/BoundedBlockingQueue.java b/src/main/java/io/github/azamir911/concurrent/BoundedBlockingQueue.java new file mode 100644 index 0000000..2327b56 --- /dev/null +++ b/src/main/java/io/github/azamir911/concurrent/BoundedBlockingQueue.java @@ -0,0 +1,220 @@ +package io.github.azamir911.concurrent; + +import java.time.Duration; +import java.util.Collection; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * A bounded FIFO queue with blocking operations and an explicit close lifecycle. + * + *

Closing the queue rejects new elements, wakes blocked producers and consumers, and still allows + * consumers to drain elements that were already accepted. Once closed and drained, {@link #take()} + * throws {@link QueueClosedException} while polling methods return an empty result. + * + * @param element type + */ +public final class BoundedBlockingQueue { + private final Object[] elements; + private final ReentrantLock lock; + private final Condition notEmpty; + private final Condition notFull; + + private int head; + private int tail; + private int size; + private boolean closed; + + public BoundedBlockingQueue(int capacity) { + this(capacity, false); + } + + public BoundedBlockingQueue(int capacity, boolean fair) { + if (capacity <= 0) { + throw new IllegalArgumentException("capacity must be greater than zero"); + } + elements = new Object[capacity]; + lock = new ReentrantLock(fair); + notEmpty = lock.newCondition(); + notFull = lock.newCondition(); + } + + public void put(E element) throws InterruptedException { + Objects.requireNonNull(element, "element"); + lock.lockInterruptibly(); + try { + while (size == elements.length && !closed) { + notFull.await(); + } + ensureOpenForWrite(); + enqueue(element); + } finally { + lock.unlock(); + } + } + + public boolean offer(E element, Duration timeout) throws InterruptedException { + Objects.requireNonNull(element, "element"); + Objects.requireNonNull(timeout, "timeout"); + if (timeout.isNegative()) { + throw new IllegalArgumentException("timeout must not be negative"); + } + + long remaining = toNanosSaturated(timeout); + lock.lockInterruptibly(); + try { + while (size == elements.length && !closed) { + if (remaining <= 0) { + return false; + } + remaining = notFull.awaitNanos(remaining); + } + ensureOpenForWrite(); + enqueue(element); + return true; + } finally { + lock.unlock(); + } + } + + public E take() throws InterruptedException { + lock.lockInterruptibly(); + try { + while (size == 0 && !closed) { + notEmpty.await(); + } + if (size == 0) { + throw new QueueClosedException("queue is closed and drained"); + } + return dequeue(); + } finally { + lock.unlock(); + } + } + + public Optional poll() { + lock.lock(); + try { + return size == 0 ? Optional.empty() : Optional.of(dequeue()); + } finally { + lock.unlock(); + } + } + + public Optional poll(Duration timeout) throws InterruptedException { + Objects.requireNonNull(timeout, "timeout"); + if (timeout.isNegative()) { + throw new IllegalArgumentException("timeout must not be negative"); + } + + long remaining = toNanosSaturated(timeout); + lock.lockInterruptibly(); + try { + while (size == 0 && !closed) { + if (remaining <= 0) { + return Optional.empty(); + } + remaining = notEmpty.awaitNanos(remaining); + } + return size == 0 ? Optional.empty() : Optional.of(dequeue()); + } finally { + lock.unlock(); + } + } + + public int drainTo(Collection target, int maxElements) { + Objects.requireNonNull(target, "target"); + if (maxElements < 0) { + throw new IllegalArgumentException("maxElements must not be negative"); + } + + lock.lock(); + try { + int drained = Math.min(size, maxElements); + for (int i = 0; i < drained; i++) { + target.add(dequeue()); + } + return drained; + } finally { + lock.unlock(); + } + } + + public void close() { + lock.lock(); + try { + if (closed) { + return; + } + closed = true; + notEmpty.signalAll(); + notFull.signalAll(); + } finally { + lock.unlock(); + } + } + + public boolean isClosed() { + lock.lock(); + try { + return closed; + } finally { + lock.unlock(); + } + } + + public int size() { + lock.lock(); + try { + return size; + } finally { + lock.unlock(); + } + } + + public int capacity() { + return elements.length; + } + + public int remainingCapacity() { + lock.lock(); + try { + return elements.length - size; + } finally { + lock.unlock(); + } + } + + private void ensureOpenForWrite() { + if (closed) { + throw new QueueClosedException("queue is closed for writes"); + } + } + + private void enqueue(E element) { + elements[tail] = element; + tail = (tail + 1) % elements.length; + size++; + notEmpty.signal(); + } + + @SuppressWarnings("unchecked") + private E dequeue() { + E element = (E) elements[head]; + elements[head] = null; + head = (head + 1) % elements.length; + size--; + notFull.signal(); + return element; + } + + private static long toNanosSaturated(Duration timeout) { + try { + return timeout.toNanos(); + } catch (ArithmeticException ignored) { + return Long.MAX_VALUE; + } + } +} diff --git a/src/main/java/io/github/azamir911/concurrent/QueueClosedException.java b/src/main/java/io/github/azamir911/concurrent/QueueClosedException.java new file mode 100644 index 0000000..f80ad6d --- /dev/null +++ b/src/main/java/io/github/azamir911/concurrent/QueueClosedException.java @@ -0,0 +1,8 @@ +package io.github.azamir911.concurrent; + +/** Signals that an operation cannot continue because a queue has been closed. */ +public final class QueueClosedException extends IllegalStateException { + public QueueClosedException(String message) { + super(message); + } +} diff --git a/src/main/java/io/github/azamir911/concurrent/ResettableLongAccumulator.java b/src/main/java/io/github/azamir911/concurrent/ResettableLongAccumulator.java new file mode 100644 index 0000000..8f00bf9 --- /dev/null +++ b/src/main/java/io/github/azamir911/concurrent/ResettableLongAccumulator.java @@ -0,0 +1,47 @@ +package io.github.azamir911.concurrent; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.LongBinaryOperator; + +/** + * A lock-free long accumulator with an atomic reset operation. + * + *

The supplied function should be side-effect free and associative because it may be invoked multiple + * times when concurrent updates retry. + */ +public final class ResettableLongAccumulator { + private final LongBinaryOperator function; + private final long identity; + private final AtomicLong value; + + public ResettableLongAccumulator(LongBinaryOperator function, long identity) { + this.function = Objects.requireNonNull(function, "function"); + this.identity = identity; + value = new AtomicLong(identity); + } + + public static ResettableLongAccumulator sum() { + return new ResettableLongAccumulator(Long::sum, 0); + } + + public static ResettableLongAccumulator max(long identity) { + return new ResettableLongAccumulator(Math::max, identity); + } + + public long accumulate(long operand) { + return value.accumulateAndGet(operand, function); + } + + public long get() { + return value.get(); + } + + public long getAndReset() { + return value.getAndSet(identity); + } + + public void reset() { + value.set(identity); + } +} diff --git a/src/test/java/io/github/azamir911/concurrent/BoundedBlockingQueueTest.java b/src/test/java/io/github/azamir911/concurrent/BoundedBlockingQueueTest.java new file mode 100644 index 0000000..6134407 --- /dev/null +++ b/src/test/java/io/github/azamir911/concurrent/BoundedBlockingQueueTest.java @@ -0,0 +1,210 @@ +package io.github.azamir911.concurrent; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; + +class BoundedBlockingQueueTest { + @Test + void preservesFifoOrderAndCapacityAccounting() throws Exception { + BoundedBlockingQueue queue = new BoundedBlockingQueue<>(3); + + queue.put(10); + queue.put(20); + queue.put(30); + + assertEquals(3, queue.size()); + assertEquals(0, queue.remainingCapacity()); + assertEquals(10, queue.take()); + assertEquals(20, queue.take()); + assertEquals(30, queue.take()); + assertEquals(0, queue.size()); + assertEquals(3, queue.remainingCapacity()); + } + + @Test + void producerWaitsUntilCapacityIsAvailable() throws Exception { + BoundedBlockingQueue queue = new BoundedBlockingQueue<>(1); + queue.put(1); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + CountDownLatch started = new CountDownLatch(1); + try { + Future producer = + executor.submit( + () -> { + started.countDown(); + queue.put(2); + return null; + }); + + assertTrue(started.await(1, SECONDS)); + Thread.sleep(100); + assertFalse(producer.isDone()); + + assertEquals(1, queue.take()); + producer.get(2, SECONDS); + assertEquals(2, queue.take()); + } finally { + executor.shutdownNow(); + } + } + + @Test + void consumerWaitsUntilAnElementArrives() throws Exception { + BoundedBlockingQueue queue = new BoundedBlockingQueue<>(1); + ExecutorService executor = Executors.newSingleThreadExecutor(); + CountDownLatch started = new CountDownLatch(1); + try { + Future consumer = + executor.submit( + () -> { + started.countDown(); + return queue.take(); + }); + + assertTrue(started.await(1, SECONDS)); + Thread.sleep(100); + assertFalse(consumer.isDone()); + + queue.put(42); + assertEquals(42, consumer.get(2, SECONDS)); + } finally { + executor.shutdownNow(); + } + } + + @Test + void closeRejectsWritesButAllowsAcceptedElementsToDrain() throws Exception { + BoundedBlockingQueue queue = new BoundedBlockingQueue<>(2); + queue.put(1); + queue.put(2); + + queue.close(); + + assertTrue(queue.isClosed()); + assertThrows(QueueClosedException.class, () -> queue.put(3)); + assertEquals(1, queue.take()); + assertEquals(2, queue.take()); + assertThrows(QueueClosedException.class, queue::take); + assertTrue(queue.poll().isEmpty()); + } + + @Test + void closeWakesBlockedConsumers() throws Exception { + BoundedBlockingQueue queue = new BoundedBlockingQueue<>(1); + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + Future consumer = executor.submit(queue::take); + Thread.sleep(100); + queue.close(); + + ExecutionException error = + assertThrows(ExecutionException.class, () -> consumer.get(2, SECONDS)); + assertInstanceOf(QueueClosedException.class, error.getCause()); + } finally { + executor.shutdownNow(); + } + } + + @Test + void timedOperationsRespectTimeouts() throws Exception { + BoundedBlockingQueue queue = new BoundedBlockingQueue<>(1); + queue.put(1); + + assertFalse(queue.offer(2, Duration.ofMillis(30))); + assertEquals(1, queue.take()); + assertTrue(queue.poll(Duration.ofMillis(30)).isEmpty()); + } + + @Test + void drainToRemovesAtMostRequestedElements() throws Exception { + BoundedBlockingQueue queue = new BoundedBlockingQueue<>(4); + queue.put(1); + queue.put(2); + queue.put(3); + List target = new ArrayList<>(); + + assertEquals(2, queue.drainTo(target, 2)); + assertEquals(List.of(1, 2), target); + assertEquals(1, queue.size()); + assertEquals(3, queue.take()); + } + + @Test + void concurrentProducersAndConsumersDoNotLoseOrDuplicateElements() throws Exception { + int producers = 4; + int consumers = 4; + int elementsPerProducer = 2_000; + int expected = producers * elementsPerProducer; + BoundedBlockingQueue queue = new BoundedBlockingQueue<>(128); + ExecutorService executor = Executors.newFixedThreadPool(producers + consumers); + Set consumed = ConcurrentHashMap.newKeySet(); + AtomicBoolean duplicate = new AtomicBoolean(); + List> consumerFutures = new ArrayList<>(); + List> producerFutures = new ArrayList<>(); + + try { + for (int i = 0; i < consumers; i++) { + consumerFutures.add( + executor.submit( + () -> { + try { + while (true) { + if (!consumed.add(queue.take())) { + duplicate.set(true); + } + } + } catch (QueueClosedException ignored) { + // Expected after all accepted elements are drained. + } + })); + } + + for (int producer = 0; producer < producers; producer++) { + int producerId = producer; + producerFutures.add( + executor.submit( + () -> { + for (int i = 0; i < elementsPerProducer; i++) { + queue.put(producerId * elementsPerProducer + i); + } + return null; + })); + } + + for (Future future : producerFutures) { + future.get(10, SECONDS); + } + queue.close(); + for (Future future : consumerFutures) { + future.get(10, SECONDS); + } + + assertFalse(duplicate.get()); + assertEquals(expected, consumed.size()); + for (int value = 0; value < expected; value++) { + assertTrue(consumed.contains(value)); + } + } finally { + queue.close(); + executor.shutdownNow(); + } + } +} diff --git a/src/test/java/io/github/azamir911/concurrent/ResettableLongAccumulatorTest.java b/src/test/java/io/github/azamir911/concurrent/ResettableLongAccumulatorTest.java new file mode 100644 index 0000000..f98b497 --- /dev/null +++ b/src/test/java/io/github/azamir911/concurrent/ResettableLongAccumulatorTest.java @@ -0,0 +1,64 @@ +package io.github.azamir911.concurrent; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.junit.jupiter.api.Test; + +class ResettableLongAccumulatorTest { + @Test + void accumulatesAndResetsAtomically() { + ResettableLongAccumulator accumulator = ResettableLongAccumulator.sum(); + + accumulator.accumulate(10); + accumulator.accumulate(15); + + assertEquals(25, accumulator.get()); + assertEquals(25, accumulator.getAndReset()); + assertEquals(0, accumulator.get()); + } + + @Test + void supportsCustomAssociativeFunctions() { + ResettableLongAccumulator maximum = ResettableLongAccumulator.max(Long.MIN_VALUE); + + maximum.accumulate(10); + maximum.accumulate(4); + maximum.accumulate(30); + + assertEquals(30, maximum.get()); + } + + @Test + void concurrentUpdatesAreNotLost() throws Exception { + int workers = 8; + int incrementsPerWorker = 50_000; + ResettableLongAccumulator accumulator = ResettableLongAccumulator.sum(); + ExecutorService executor = Executors.newFixedThreadPool(workers); + List> futures = new ArrayList<>(); + + try { + for (int worker = 0; worker < workers; worker++) { + futures.add( + executor.submit( + () -> { + for (int i = 0; i < incrementsPerWorker; i++) { + accumulator.accumulate(1); + } + })); + } + for (Future future : futures) { + future.get(10, SECONDS); + } + + assertEquals((long) workers * incrementsPerWorker, accumulator.get()); + } finally { + executor.shutdownNow(); + } + } +} From da1c1ee04f2361f61a2ab00a833da4c4687338f7 Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:26:41 +0200 Subject: [PATCH 02/34] Handle interrupted consumers in stress test --- .../github/azamir911/concurrent/BoundedBlockingQueueTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/io/github/azamir911/concurrent/BoundedBlockingQueueTest.java b/src/test/java/io/github/azamir911/concurrent/BoundedBlockingQueueTest.java index 6134407..04194ab 100644 --- a/src/test/java/io/github/azamir911/concurrent/BoundedBlockingQueueTest.java +++ b/src/test/java/io/github/azamir911/concurrent/BoundedBlockingQueueTest.java @@ -173,6 +173,8 @@ void concurrentProducersAndConsumersDoNotLoseOrDuplicateElements() throws Except } } catch (QueueClosedException ignored) { // Expected after all accepted elements are drained. + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); } })); } From 9a7b1a86ecf809b4b5f4b8969d931897407c2b32 Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:47:44 +0200 Subject: [PATCH 03/34] Add shared closeable queue contract --- .../concurrent/CloseableBlockingQueue.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/main/java/io/github/azamir911/concurrent/CloseableBlockingQueue.java diff --git a/src/main/java/io/github/azamir911/concurrent/CloseableBlockingQueue.java b/src/main/java/io/github/azamir911/concurrent/CloseableBlockingQueue.java new file mode 100644 index 0000000..edd6c7b --- /dev/null +++ b/src/main/java/io/github/azamir911/concurrent/CloseableBlockingQueue.java @@ -0,0 +1,45 @@ +package io.github.azamir911.concurrent; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/** + * A bounded queue contract with explicit shutdown semantics. + * + *

Implementations reject writes after close, allow already accepted elements to drain, and wake or + * eventually release blocked callers when the queue lifecycle changes. + */ +public interface CloseableBlockingQueue extends AutoCloseable { + void put(E element) throws InterruptedException; + + boolean offer(E element, Duration timeout) throws InterruptedException; + + E take() throws InterruptedException; + + Optional poll(); + + Optional poll(Duration timeout) throws InterruptedException; + + int drainTo(Collection target, int maxElements); + + int clear(); + + boolean contains(Object value); + + int indexOf(Object value); + + List snapshot(); + + boolean isClosed(); + + int size(); + + int capacity(); + + int remainingCapacity(); + + @Override + void close(); +} From 558bfb29072b94548b2a669737a01a00d6d7e4c1 Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:48:19 +0200 Subject: [PATCH 04/34] Complete condition-based queue API --- .../concurrent/BoundedBlockingQueue.java | 76 ++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/azamir911/concurrent/BoundedBlockingQueue.java b/src/main/java/io/github/azamir911/concurrent/BoundedBlockingQueue.java index 2327b56..b0d17cc 100644 --- a/src/main/java/io/github/azamir911/concurrent/BoundedBlockingQueue.java +++ b/src/main/java/io/github/azamir911/concurrent/BoundedBlockingQueue.java @@ -1,14 +1,16 @@ package io.github.azamir911.concurrent; import java.time.Duration; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /** - * A bounded FIFO queue with blocking operations and an explicit close lifecycle. + * A bounded FIFO ring buffer with condition-based blocking and an explicit close lifecycle. * *

Closing the queue rejects new elements, wakes blocked producers and consumers, and still allows * consumers to drain elements that were already accepted. Once closed and drained, {@link #take()} @@ -16,7 +18,7 @@ * * @param element type */ -public final class BoundedBlockingQueue { +public final class BoundedBlockingQueue implements CloseableBlockingQueue { private final Object[] elements; private final ReentrantLock lock; private final Condition notEmpty; @@ -41,6 +43,7 @@ public BoundedBlockingQueue(int capacity, boolean fair) { notFull = lock.newCondition(); } + @Override public void put(E element) throws InterruptedException { Objects.requireNonNull(element, "element"); lock.lockInterruptibly(); @@ -55,6 +58,7 @@ public void put(E element) throws InterruptedException { } } + @Override public boolean offer(E element, Duration timeout) throws InterruptedException { Objects.requireNonNull(element, "element"); Objects.requireNonNull(timeout, "timeout"); @@ -79,6 +83,7 @@ public boolean offer(E element, Duration timeout) throws InterruptedException { } } + @Override public E take() throws InterruptedException { lock.lockInterruptibly(); try { @@ -94,6 +99,7 @@ public E take() throws InterruptedException { } } + @Override public Optional poll() { lock.lock(); try { @@ -103,6 +109,7 @@ public Optional poll() { } } + @Override public Optional poll(Duration timeout) throws InterruptedException { Objects.requireNonNull(timeout, "timeout"); if (timeout.isNegative()) { @@ -124,6 +131,7 @@ public Optional poll(Duration timeout) throws InterruptedException { } } + @Override public int drainTo(Collection target, int maxElements) { Objects.requireNonNull(target, "target"); if (maxElements < 0) { @@ -142,6 +150,61 @@ public int drainTo(Collection target, int maxElements) { } } + @Override + public int clear() { + lock.lock(); + try { + int removed = size; + for (int i = 0; i < removed; i++) { + elements[(head + i) % elements.length] = null; + } + head = 0; + tail = 0; + size = 0; + if (removed > 0) { + notFull.signalAll(); + } + return removed; + } finally { + lock.unlock(); + } + } + + @Override + public boolean contains(Object value) { + return indexOf(value) >= 0; + } + + @Override + public int indexOf(Object value) { + lock.lock(); + try { + for (int i = 0; i < size; i++) { + if (Objects.equals(value, elements[(head + i) % elements.length])) { + return i; + } + } + return -1; + } finally { + lock.unlock(); + } + } + + @Override + public List snapshot() { + lock.lock(); + try { + List copy = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + copy.add(elementAt(i)); + } + return List.copyOf(copy); + } finally { + lock.unlock(); + } + } + + @Override public void close() { lock.lock(); try { @@ -156,6 +219,7 @@ public void close() { } } + @Override public boolean isClosed() { lock.lock(); try { @@ -165,6 +229,7 @@ public boolean isClosed() { } } + @Override public int size() { lock.lock(); try { @@ -174,10 +239,12 @@ public int size() { } } + @Override public int capacity() { return elements.length; } + @Override public int remainingCapacity() { lock.lock(); try { @@ -210,6 +277,11 @@ private E dequeue() { return element; } + @SuppressWarnings("unchecked") + private E elementAt(int logicalIndex) { + return (E) elements[(head + logicalIndex) % elements.length]; + } + private static long toNanosSaturated(Duration timeout) { try { return timeout.toNanos(); From 40a17a63be3104b26621c4471b4b868be6c2293c Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:49:00 +0200 Subject: [PATCH 05/34] Add polling backoff queue implementation --- .../concurrent/PollingBackoffQueue.java | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 src/main/java/io/github/azamir911/concurrent/PollingBackoffQueue.java diff --git a/src/main/java/io/github/azamir911/concurrent/PollingBackoffQueue.java b/src/main/java/io/github/azamir911/concurrent/PollingBackoffQueue.java new file mode 100644 index 0000000..3fa9667 --- /dev/null +++ b/src/main/java/io/github/azamir911/concurrent/PollingBackoffQueue.java @@ -0,0 +1,301 @@ +package io.github.azamir911.concurrent; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +/** + * A bounded FIFO queue that uses configurable polling backoff instead of condition signaling. + * + *

This strategy is intentionally less efficient than {@link BoundedBlockingQueue}, but it is useful + * when callers need a simple polling lifecycle or want to compare wait strategies. Closing the queue + * becomes visible to blocked callers no later than one backoff interval later. + */ +public final class PollingBackoffQueue implements CloseableBlockingQueue { + private final Object[] elements; + private final ReentrantLock lock = new ReentrantLock(); + private final long backoffNanos; + + private int head; + private int tail; + private int size; + private boolean closed; + + public PollingBackoffQueue(int capacity, Duration backoff) { + if (capacity <= 0) { + throw new IllegalArgumentException("capacity must be greater than zero"); + } + Objects.requireNonNull(backoff, "backoff"); + if (backoff.isZero() || backoff.isNegative()) { + throw new IllegalArgumentException("backoff must be greater than zero"); + } + elements = new Object[capacity]; + backoffNanos = toNanosSaturated(backoff); + } + + @Override + public void put(E element) throws InterruptedException { + Objects.requireNonNull(element, "element"); + while (true) { + lock.lockInterruptibly(); + try { + ensureOpenForWrite(); + if (size < elements.length) { + enqueue(element); + return; + } + } finally { + lock.unlock(); + } + pause(backoffNanos); + } + } + + @Override + public boolean offer(E element, Duration timeout) throws InterruptedException { + Objects.requireNonNull(element, "element"); + long remaining = validateTimeout(timeout); + while (true) { + long started = System.nanoTime(); + lock.lockInterruptibly(); + try { + ensureOpenForWrite(); + if (size < elements.length) { + enqueue(element); + return true; + } + } finally { + lock.unlock(); + } + if (remaining <= 0) { + return false; + } + pause(Math.min(backoffNanos, remaining)); + remaining -= elapsedSince(started); + } + } + + @Override + public E take() throws InterruptedException { + while (true) { + lock.lockInterruptibly(); + try { + if (size > 0) { + return dequeue(); + } + if (closed) { + throw new QueueClosedException("queue is closed and drained"); + } + } finally { + lock.unlock(); + } + pause(backoffNanos); + } + } + + @Override + public Optional poll() { + lock.lock(); + try { + return size == 0 ? Optional.empty() : Optional.of(dequeue()); + } finally { + lock.unlock(); + } + } + + @Override + public Optional poll(Duration timeout) throws InterruptedException { + long remaining = validateTimeout(timeout); + while (true) { + long started = System.nanoTime(); + lock.lockInterruptibly(); + try { + if (size > 0) { + return Optional.of(dequeue()); + } + if (closed) { + return Optional.empty(); + } + } finally { + lock.unlock(); + } + if (remaining <= 0) { + return Optional.empty(); + } + pause(Math.min(backoffNanos, remaining)); + remaining -= elapsedSince(started); + } + } + + @Override + public int drainTo(Collection target, int maxElements) { + Objects.requireNonNull(target, "target"); + if (maxElements < 0) { + throw new IllegalArgumentException("maxElements must not be negative"); + } + lock.lock(); + try { + int drained = Math.min(size, maxElements); + for (int i = 0; i < drained; i++) { + target.add(dequeue()); + } + return drained; + } finally { + lock.unlock(); + } + } + + @Override + public int clear() { + lock.lock(); + try { + int removed = size; + for (int i = 0; i < removed; i++) { + elements[(head + i) % elements.length] = null; + } + head = 0; + tail = 0; + size = 0; + return removed; + } finally { + lock.unlock(); + } + } + + @Override + public boolean contains(Object value) { + return indexOf(value) >= 0; + } + + @Override + public int indexOf(Object value) { + lock.lock(); + try { + for (int i = 0; i < size; i++) { + if (Objects.equals(value, elements[(head + i) % elements.length])) { + return i; + } + } + return -1; + } finally { + lock.unlock(); + } + } + + @Override + public List snapshot() { + lock.lock(); + try { + List copy = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + copy.add(elementAt(i)); + } + return List.copyOf(copy); + } finally { + lock.unlock(); + } + } + + @Override + public void close() { + lock.lock(); + try { + closed = true; + } finally { + lock.unlock(); + } + } + + @Override + public boolean isClosed() { + lock.lock(); + try { + return closed; + } finally { + lock.unlock(); + } + } + + @Override + public int size() { + lock.lock(); + try { + return size; + } finally { + lock.unlock(); + } + } + + @Override + public int capacity() { + return elements.length; + } + + @Override + public int remainingCapacity() { + lock.lock(); + try { + return elements.length - size; + } finally { + lock.unlock(); + } + } + + private void ensureOpenForWrite() { + if (closed) { + throw new QueueClosedException("queue is closed for writes"); + } + } + + private void enqueue(E element) { + elements[tail] = element; + tail = (tail + 1) % elements.length; + size++; + } + + @SuppressWarnings("unchecked") + private E dequeue() { + E element = (E) elements[head]; + elements[head] = null; + head = (head + 1) % elements.length; + size--; + return element; + } + + @SuppressWarnings("unchecked") + private E elementAt(int logicalIndex) { + return (E) elements[(head + logicalIndex) % elements.length]; + } + + private static long validateTimeout(Duration timeout) { + Objects.requireNonNull(timeout, "timeout"); + if (timeout.isNegative()) { + throw new IllegalArgumentException("timeout must not be negative"); + } + return toNanosSaturated(timeout); + } + + private static void pause(long nanos) throws InterruptedException { + if (nanos > 0) { + TimeUnit.NANOSECONDS.sleep(nanos); + } + } + + private static long elapsedSince(long started) { + long elapsed = System.nanoTime() - started; + return Math.max(elapsed, 1); + } + + private static long toNanosSaturated(Duration duration) { + try { + return duration.toNanos(); + } catch (ArithmeticException ignored) { + return Long.MAX_VALUE; + } + } +} From 9b52756e48aecc316825f8eea0ae948ac8d24c72 Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:49:18 +0200 Subject: [PATCH 06/34] Add queue wait strategy model --- .../io/github/azamir911/concurrent/QueueWaitStrategy.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/io/github/azamir911/concurrent/QueueWaitStrategy.java diff --git a/src/main/java/io/github/azamir911/concurrent/QueueWaitStrategy.java b/src/main/java/io/github/azamir911/concurrent/QueueWaitStrategy.java new file mode 100644 index 0000000..dad2fbe --- /dev/null +++ b/src/main/java/io/github/azamir911/concurrent/QueueWaitStrategy.java @@ -0,0 +1,7 @@ +package io.github.azamir911.concurrent; + +/** Selects how a bounded queue waits for capacity or data. */ +public enum QueueWaitStrategy { + CONDITION_SIGNALING, + POLLING_BACKOFF +} From d75c21c3a3eb9d2c393aa224416f6a8feb1b4bd8 Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:49:31 +0200 Subject: [PATCH 07/34] Add queue factory --- .../azamir911/concurrent/CloseableQueues.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/java/io/github/azamir911/concurrent/CloseableQueues.java diff --git a/src/main/java/io/github/azamir911/concurrent/CloseableQueues.java b/src/main/java/io/github/azamir911/concurrent/CloseableQueues.java new file mode 100644 index 0000000..e79e3c6 --- /dev/null +++ b/src/main/java/io/github/azamir911/concurrent/CloseableQueues.java @@ -0,0 +1,24 @@ +package io.github.azamir911.concurrent; + +import java.time.Duration; +import java.util.Objects; + +/** Factory methods for the supported bounded queue wait strategies. */ +public final class CloseableQueues { + public static final Duration DEFAULT_POLLING_BACKOFF = Duration.ofMillis(10); + + private CloseableQueues() {} + + public static CloseableBlockingQueue bounded(int capacity, QueueWaitStrategy strategy) { + return bounded(capacity, strategy, DEFAULT_POLLING_BACKOFF); + } + + public static CloseableBlockingQueue bounded( + int capacity, QueueWaitStrategy strategy, Duration pollingBackoff) { + Objects.requireNonNull(strategy, "strategy"); + return switch (strategy) { + case CONDITION_SIGNALING -> new BoundedBlockingQueue<>(capacity); + case POLLING_BACKOFF -> new PollingBackoffQueue<>(capacity, pollingBackoff); + }; + } +} From f0438564ff2a9366e13d3e6a88b31d1c1d677072 Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:49:58 +0200 Subject: [PATCH 08/34] Add immutable statistics snapshot --- .../azamir911/statistics/StatisticsSnapshot.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/io/github/azamir911/statistics/StatisticsSnapshot.java diff --git a/src/main/java/io/github/azamir911/statistics/StatisticsSnapshot.java b/src/main/java/io/github/azamir911/statistics/StatisticsSnapshot.java new file mode 100644 index 0000000..7d4bd33 --- /dev/null +++ b/src/main/java/io/github/azamir911/statistics/StatisticsSnapshot.java @@ -0,0 +1,13 @@ +package io.github.azamir911.statistics; + +import java.util.OptionalDouble; + +/** Immutable view of a streaming statistics accumulator. */ +public record StatisticsSnapshot( + long count, + double sum, + OptionalDouble mean, + OptionalDouble sampleVariance, + OptionalDouble sampleStandardDeviation, + OptionalDouble min, + OptionalDouble max) {} From ddac52a2a0a0cf2b64b99d0adc96b2ad28a95fc0 Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:50:22 +0200 Subject: [PATCH 09/34] Add thread-safe streaming statistics --- .../statistics/StreamingStatistics.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/main/java/io/github/azamir911/statistics/StreamingStatistics.java diff --git a/src/main/java/io/github/azamir911/statistics/StreamingStatistics.java b/src/main/java/io/github/azamir911/statistics/StreamingStatistics.java new file mode 100644 index 0000000..4761cf8 --- /dev/null +++ b/src/main/java/io/github/azamir911/statistics/StreamingStatistics.java @@ -0,0 +1,98 @@ +package io.github.azamir911.statistics; + +import java.util.OptionalDouble; + +/** + * Thread-safe one-pass descriptive statistics using Welford's algorithm for variance and compensated + * summation for improved numerical stability. + */ +public final class StreamingStatistics { + private long count; + private double mean; + private double squaredDeviationSum; + private double sum; + private double sumCompensation; + private double min = Double.POSITIVE_INFINITY; + private double max = Double.NEGATIVE_INFINITY; + + public synchronized void add(double value) { + if (!Double.isFinite(value)) { + throw new IllegalArgumentException("value must be finite"); + } + + count++; + double delta = value - mean; + mean += delta / count; + double adjustedDelta = value - mean; + squaredDeviationSum += delta * adjustedDelta; + + double compensatedValue = value - sumCompensation; + double updatedSum = sum + compensatedValue; + sumCompensation = (updatedSum - sum) - compensatedValue; + sum = updatedSum; + + min = Math.min(min, value); + max = Math.max(max, value); + } + + public synchronized StatisticsSnapshot snapshot() { + OptionalDouble currentMean = count == 0 ? OptionalDouble.empty() : OptionalDouble.of(mean); + OptionalDouble variance = + count < 2 ? OptionalDouble.empty() : OptionalDouble.of(squaredDeviationSum / (count - 1)); + OptionalDouble standardDeviation = + variance.isEmpty() ? OptionalDouble.empty() : OptionalDouble.of(Math.sqrt(variance.getAsDouble())); + OptionalDouble currentMin = count == 0 ? OptionalDouble.empty() : OptionalDouble.of(min); + OptionalDouble currentMax = count == 0 ? OptionalDouble.empty() : OptionalDouble.of(max); + + return new StatisticsSnapshot( + count, sum, currentMean, variance, standardDeviation, currentMin, currentMax); + } + + public synchronized long count() { + return count; + } + + public synchronized double sum() { + return sum; + } + + public synchronized OptionalDouble mean() { + return count == 0 ? OptionalDouble.empty() : OptionalDouble.of(mean); + } + + public synchronized OptionalDouble sampleVariance() { + return count < 2 + ? OptionalDouble.empty() + : OptionalDouble.of(squaredDeviationSum / (count - 1)); + } + + public synchronized OptionalDouble sampleStandardDeviation() { + OptionalDouble variance = sampleVariance(); + return variance.isEmpty() + ? OptionalDouble.empty() + : OptionalDouble.of(Math.sqrt(variance.getAsDouble())); + } + + public synchronized OptionalDouble min() { + return count == 0 ? OptionalDouble.empty() : OptionalDouble.of(min); + } + + public synchronized OptionalDouble max() { + return count == 0 ? OptionalDouble.empty() : OptionalDouble.of(max); + } + + public synchronized void reset() { + count = 0; + mean = 0; + squaredDeviationSum = 0; + sum = 0; + sumCompensation = 0; + min = Double.POSITIVE_INFINITY; + max = Double.NEGATIVE_INFINITY; + } + + @Override + public synchronized String toString() { + return snapshot().toString(); + } +} From a23878c7085c23b8def5ca0b6203a33e96c1f78a Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:50:55 +0200 Subject: [PATCH 10/34] Test streaming statistics --- .../statistics/StreamingStatisticsTest.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/test/java/io/github/azamir911/statistics/StreamingStatisticsTest.java diff --git a/src/test/java/io/github/azamir911/statistics/StreamingStatisticsTest.java b/src/test/java/io/github/azamir911/statistics/StreamingStatisticsTest.java new file mode 100644 index 0000000..1cddfde --- /dev/null +++ b/src/test/java/io/github/azamir911/statistics/StreamingStatisticsTest.java @@ -0,0 +1,84 @@ +package io.github.azamir911.statistics; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.junit.jupiter.api.Test; + +class StreamingStatisticsTest { + @Test + void calculatesStableOnePassStatistics() { + StreamingStatistics statistics = new StreamingStatistics(); + for (double value : new double[] {2, 4, 4, 4, 5, 5, 7, 9}) { + statistics.add(value); + } + + StatisticsSnapshot snapshot = statistics.snapshot(); + assertEquals(8, snapshot.count()); + assertEquals(40, snapshot.sum(), 1e-12); + assertEquals(5, snapshot.mean().orElseThrow(), 1e-12); + assertEquals(32.0 / 7.0, snapshot.sampleVariance().orElseThrow(), 1e-12); + assertEquals(Math.sqrt(32.0 / 7.0), snapshot.sampleStandardDeviation().orElseThrow(), 1e-12); + assertEquals(2, snapshot.min().orElseThrow(), 1e-12); + assertEquals(9, snapshot.max().orElseThrow(), 1e-12); + } + + @Test + void exposesEmptyStateAndSupportsReset() { + StreamingStatistics statistics = new StreamingStatistics(); + assertTrue(statistics.mean().isEmpty()); + assertTrue(statistics.sampleVariance().isEmpty()); + assertTrue(statistics.min().isEmpty()); + assertTrue(statistics.max().isEmpty()); + + statistics.add(12.5); + statistics.reset(); + + assertEquals(0, statistics.count()); + assertEquals(0, statistics.sum(), 0); + assertTrue(statistics.snapshot().mean().isEmpty()); + } + + @Test + void rejectsNonFiniteValues() { + StreamingStatistics statistics = new StreamingStatistics(); + assertThrows(IllegalArgumentException.class, () -> statistics.add(Double.NaN)); + assertThrows(IllegalArgumentException.class, () -> statistics.add(Double.POSITIVE_INFINITY)); + } + + @Test + void acceptsConcurrentUpdatesWithoutLosingValues() throws Exception { + StreamingStatistics statistics = new StreamingStatistics(); + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + List> futures = + List.of( + executor.submit(() -> addRepeatedly(statistics, 1_000)), + executor.submit(() -> addRepeatedly(statistics, 1_000)), + executor.submit(() -> addRepeatedly(statistics, 1_000)), + executor.submit(() -> addRepeatedly(statistics, 1_000))); + for (Future future : futures) { + future.get(5, SECONDS); + } + + assertEquals(4_000, statistics.count()); + assertEquals(4_000, statistics.sum(), 1e-12); + assertEquals(1, statistics.mean().orElseThrow(), 1e-12); + assertEquals(0, statistics.sampleVariance().orElseThrow(), 1e-12); + } finally { + executor.shutdownNow(); + } + } + + private static void addRepeatedly(StreamingStatistics statistics, int count) { + for (int i = 0; i < count; i++) { + statistics.add(1); + } + } +} From 396a9408b45b23acd6511f0c3a0a8639d7f24c4d Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:51:23 +0200 Subject: [PATCH 11/34] Add decimal ratio calculator --- .../math/DecimalRatioCalculator.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/main/java/io/github/azamir911/math/DecimalRatioCalculator.java diff --git a/src/main/java/io/github/azamir911/math/DecimalRatioCalculator.java b/src/main/java/io/github/azamir911/math/DecimalRatioCalculator.java new file mode 100644 index 0000000..a55b3de --- /dev/null +++ b/src/main/java/io/github/azamir911/math/DecimalRatioCalculator.java @@ -0,0 +1,60 @@ +package io.github.azamir911.math; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Objects; + +/** + * Calculates decimal ratios with explicit scale, rounding, and an optional additive adjustment. + * + *

The adjustment is applied after division, matching use cases where a calibrated factor must be + * added to a measured ratio. + */ +public final class DecimalRatioCalculator { + private final BigDecimal adjustment; + + public DecimalRatioCalculator() { + this(BigDecimal.ZERO); + } + + public DecimalRatioCalculator(BigDecimal adjustment) { + this.adjustment = Objects.requireNonNull(adjustment, "adjustment"); + } + + public BigDecimal calculate(BigDecimal numerator, BigDecimal denominator) { + return calculate(numerator, denominator, 1, RoundingMode.UP); + } + + public BigDecimal calculate(BigDecimal numerator, BigDecimal denominator, int scale) { + return calculate(numerator, denominator, scale, RoundingMode.UP); + } + + public BigDecimal calculate( + BigDecimal numerator, BigDecimal denominator, RoundingMode roundingMode) { + return calculate(numerator, denominator, 1, roundingMode); + } + + public BigDecimal calculate( + BigDecimal numerator, BigDecimal denominator, int scale, RoundingMode roundingMode) { + Objects.requireNonNull(numerator, "numerator"); + Objects.requireNonNull(denominator, "denominator"); + Objects.requireNonNull(roundingMode, "roundingMode"); + if (scale < 0) { + throw new IllegalArgumentException("scale must not be negative"); + } + if (denominator.signum() == 0) { + throw new ArithmeticException("denominator must not be zero"); + } + return numerator.divide(denominator, scale, roundingMode).add(adjustment); + } + + public double calculate(double numerator, double denominator, int scale, RoundingMode roundingMode) { + return calculate( + BigDecimal.valueOf(numerator), BigDecimal.valueOf(denominator), scale, roundingMode) + .doubleValue(); + } + + public BigDecimal adjustment() { + return adjustment; + } +} From 04a64c1742ffc64a94abcf724435d625fab4e5a3 Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:51:42 +0200 Subject: [PATCH 12/34] Test decimal ratio calculations --- .../math/DecimalRatioCalculatorTest.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/test/java/io/github/azamir911/math/DecimalRatioCalculatorTest.java diff --git a/src/test/java/io/github/azamir911/math/DecimalRatioCalculatorTest.java b/src/test/java/io/github/azamir911/math/DecimalRatioCalculatorTest.java new file mode 100644 index 0000000..5e07299 --- /dev/null +++ b/src/test/java/io/github/azamir911/math/DecimalRatioCalculatorTest.java @@ -0,0 +1,45 @@ +package io.github.azamir911.math; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import org.junit.jupiter.api.Test; + +class DecimalRatioCalculatorTest { + @Test + void appliesScaleRoundingAndAdjustment() { + DecimalRatioCalculator calculator = new DecimalRatioCalculator(new BigDecimal("0.10")); + + assertEquals( + new BigDecimal("0.77"), + calculator.calculate( + new BigDecimal("2"), new BigDecimal("3"), 2, RoundingMode.HALF_UP)); + } + + @Test + void preservesDefaultUpRoundingBehavior() { + DecimalRatioCalculator calculator = new DecimalRatioCalculator(); + assertEquals( + new BigDecimal("0.7"), + calculator.calculate(new BigDecimal("2"), new BigDecimal("3"))); + } + + @Test + void supportsDoubleConvenienceMethodWithoutBinaryConstructorArtifacts() { + DecimalRatioCalculator calculator = new DecimalRatioCalculator(new BigDecimal("0.05")); + assertEquals(0.72, calculator.calculate(2, 3, 2, RoundingMode.HALF_UP), 1e-12); + } + + @Test + void rejectsInvalidScaleAndZeroDenominator() { + DecimalRatioCalculator calculator = new DecimalRatioCalculator(); + assertThrows( + IllegalArgumentException.class, + () -> calculator.calculate(BigDecimal.ONE, BigDecimal.TEN, -1, RoundingMode.UP)); + assertThrows( + ArithmeticException.class, + () -> calculator.calculate(BigDecimal.ONE, BigDecimal.ZERO, 2, RoundingMode.UP)); + } +} From 2f35b10aa8cec6ca692db147b071df20c038ca3b Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:52:13 +0200 Subject: [PATCH 13/34] Add checked serialized size probe --- .../io/github/azamir911/batching/CheckedSizeProbe.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/io/github/azamir911/batching/CheckedSizeProbe.java diff --git a/src/main/java/io/github/azamir911/batching/CheckedSizeProbe.java b/src/main/java/io/github/azamir911/batching/CheckedSizeProbe.java new file mode 100644 index 0000000..51cdece --- /dev/null +++ b/src/main/java/io/github/azamir911/batching/CheckedSizeProbe.java @@ -0,0 +1,7 @@ +package io.github.azamir911.batching; + +/** Measures a serialized size and may fail with a checked exception. */ +@FunctionalInterface +public interface CheckedSizeProbe { + long measureBytes(T value) throws Exception; +} From c8b8f3208d148d2b20421f17dc7767b2ecc6d4d9 Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:52:30 +0200 Subject: [PATCH 14/34] Add size probe exception --- .../io/github/azamir911/batching/SizeProbeException.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/io/github/azamir911/batching/SizeProbeException.java diff --git a/src/main/java/io/github/azamir911/batching/SizeProbeException.java b/src/main/java/io/github/azamir911/batching/SizeProbeException.java new file mode 100644 index 0000000..51a7d9b --- /dev/null +++ b/src/main/java/io/github/azamir911/batching/SizeProbeException.java @@ -0,0 +1,8 @@ +package io.github.azamir911.batching; + +/** Raised when a serialized size cannot be measured within the configured retry budget. */ +public final class SizeProbeException extends RuntimeException { + public SizeProbeException(String message, Throwable cause) { + super(message, cause); + } +} From dd3df2099dfbb92f252b3f3b9ef737f4f5d26a5d Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:52:50 +0200 Subject: [PATCH 15/34] Add retrying size probe --- .../azamir911/batching/RetryingSizeProbe.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/main/java/io/github/azamir911/batching/RetryingSizeProbe.java diff --git a/src/main/java/io/github/azamir911/batching/RetryingSizeProbe.java b/src/main/java/io/github/azamir911/batching/RetryingSizeProbe.java new file mode 100644 index 0000000..efc53d4 --- /dev/null +++ b/src/main/java/io/github/azamir911/batching/RetryingSizeProbe.java @@ -0,0 +1,41 @@ +package io.github.azamir911.batching; + +import java.util.Objects; + +/** Retries transient serialized-size measurement failures up to a fixed attempt budget. */ +public final class RetryingSizeProbe { + private final CheckedSizeProbe delegate; + private final int maxAttempts; + + public RetryingSizeProbe(CheckedSizeProbe delegate, int maxAttempts) { + this.delegate = Objects.requireNonNull(delegate, "delegate"); + if (maxAttempts <= 0) { + throw new IllegalArgumentException("maxAttempts must be greater than zero"); + } + this.maxAttempts = maxAttempts; + } + + public long measureBytes(T value) { + Exception lastFailure = null; + for (int attempt = 1; attempt <= maxAttempts; attempt++) { + try { + long measured = delegate.measureBytes(value); + if (measured < 0) { + throw new IllegalStateException("measured size must not be negative"); + } + return measured; + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new SizeProbeException("size measurement was interrupted", interrupted); + } catch (Exception failure) { + lastFailure = failure; + } + } + throw new SizeProbeException( + "could not measure serialized size after " + maxAttempts + " attempts", lastFailure); + } + + public int maxAttempts() { + return maxAttempts; + } +} From 0496a8b162c624cef49319eb07cd5c965f1160b5 Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:53:10 +0200 Subject: [PATCH 16/34] Add adaptive batch sizing algorithm --- .../batching/AdaptiveBatchSizer.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/main/java/io/github/azamir911/batching/AdaptiveBatchSizer.java diff --git a/src/main/java/io/github/azamir911/batching/AdaptiveBatchSizer.java b/src/main/java/io/github/azamir911/batching/AdaptiveBatchSizer.java new file mode 100644 index 0000000..7790b6c --- /dev/null +++ b/src/main/java/io/github/azamir911/batching/AdaptiveBatchSizer.java @@ -0,0 +1,99 @@ +package io.github.azamir911.batching; + +import java.util.Objects; + +/** + * Estimates a safe batch size from a representative serialized sample. + * + *

The calculation separates fixed overhead from per-record payload, projects how many records fit + * in the target capacity, applies a safety factor, and clamps the result to configured limits. + */ +public final class AdaptiveBatchSizer { + private final long targetCapacityBytes; + private final double safetyFactor; + private final int minimumBatchSize; + private final int maximumBatchSize; + + public AdaptiveBatchSizer(long targetCapacityBytes, double safetyFactor) { + this(targetCapacityBytes, safetyFactor, 1, Integer.MAX_VALUE); + } + + public AdaptiveBatchSizer( + long targetCapacityBytes, + double safetyFactor, + int minimumBatchSize, + int maximumBatchSize) { + if (targetCapacityBytes <= 0) { + throw new IllegalArgumentException("targetCapacityBytes must be greater than zero"); + } + if (!Double.isFinite(safetyFactor) || safetyFactor <= 0 || safetyFactor > 1) { + throw new IllegalArgumentException("safetyFactor must be in the range (0, 1]"); + } + if (minimumBatchSize <= 0) { + throw new IllegalArgumentException("minimumBatchSize must be greater than zero"); + } + if (maximumBatchSize < minimumBatchSize) { + throw new IllegalArgumentException("maximumBatchSize must be at least minimumBatchSize"); + } + this.targetCapacityBytes = targetCapacityBytes; + this.safetyFactor = safetyFactor; + this.minimumBatchSize = minimumBatchSize; + this.maximumBatchSize = maximumBatchSize; + } + + public int estimate( + int sampleRecordCount, long serializedSampleBytes, long fixedOverheadBytes) { + if (sampleRecordCount < 0) { + throw new IllegalArgumentException("sampleRecordCount must not be negative"); + } + if (sampleRecordCount == 0) { + return 0; + } + if (serializedSampleBytes < 0 || fixedOverheadBytes < 0) { + throw new IllegalArgumentException("serialized sizes must not be negative"); + } + if (serializedSampleBytes < fixedOverheadBytes) { + throw new IllegalArgumentException( + "serializedSampleBytes must include and therefore be at least fixedOverheadBytes"); + } + + if (fixedOverheadBytes >= targetCapacityBytes) { + return clamp(sampleRecordCount); + } + + long payloadBytes = serializedSampleBytes - fixedOverheadBytes; + if (payloadBytes == 0) { + return clamp(sampleRecordCount); + } + + double averageRecordBytes = (double) payloadBytes / sampleRecordCount; + double usableCapacity = targetCapacityBytes - fixedOverheadBytes; + long projected = (long) Math.floor((usableCapacity / averageRecordBytes) * safetyFactor); + if (projected <= 0) { + return clamp(sampleRecordCount); + } + return clamp(projected); + } + + public int estimate( + int sampleRecordCount, + T serializedSample, + RetryingSizeProbe totalSizeProbe, + long fixedOverheadBytes) { + Objects.requireNonNull(totalSizeProbe, "totalSizeProbe"); + return estimate( + sampleRecordCount, totalSizeProbe.measureBytes(serializedSample), fixedOverheadBytes); + } + + public long targetCapacityBytes() { + return targetCapacityBytes; + } + + public double safetyFactor() { + return safetyFactor; + } + + private int clamp(long value) { + return (int) Math.max(minimumBatchSize, Math.min(maximumBatchSize, value)); + } +} From 089485dd1afa8b9818ae5d2d828a2bac45eaf34a Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:53:33 +0200 Subject: [PATCH 17/34] Test adaptive batch sizing and retries --- .../batching/AdaptiveBatchSizerTest.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/test/java/io/github/azamir911/batching/AdaptiveBatchSizerTest.java diff --git a/src/test/java/io/github/azamir911/batching/AdaptiveBatchSizerTest.java b/src/test/java/io/github/azamir911/batching/AdaptiveBatchSizerTest.java new file mode 100644 index 0000000..7605718 --- /dev/null +++ b/src/test/java/io/github/azamir911/batching/AdaptiveBatchSizerTest.java @@ -0,0 +1,64 @@ +package io.github.azamir911.batching; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +class AdaptiveBatchSizerTest { + @Test + void estimatesRecordsFromPayloadAndFixedOverhead() { + AdaptiveBatchSizer sizer = new AdaptiveBatchSizer(1_000, 0.8); + + assertEquals(18, sizer.estimate(10, 500, 100)); + } + + @Test + void fallsBackToSampleSizeWhenOverheadOrPayloadPreventsProjection() { + AdaptiveBatchSizer sizer = new AdaptiveBatchSizer(1_000, 0.8); + + assertEquals(10, sizer.estimate(10, 1_200, 1_100)); + assertEquals(10, sizer.estimate(10, 100, 100)); + assertEquals(0, sizer.estimate(0, 0, 0)); + } + + @Test + void clampsProjectedBatchSize() { + AdaptiveBatchSizer sizer = new AdaptiveBatchSizer(10_000, 1.0, 5, 50); + + assertEquals(50, sizer.estimate(10, 20, 10)); + } + + @Test + void retriesTransientProbeFailuresBeforeEstimating() { + AtomicInteger attempts = new AtomicInteger(); + RetryingSizeProbe probe = + new RetryingSizeProbe<>( + ignored -> { + if (attempts.incrementAndGet() < 3) { + throw new IllegalStateException("temporary serialization failure"); + } + return 500; + }, + 3); + AdaptiveBatchSizer sizer = new AdaptiveBatchSizer(1_000, 0.8); + + assertEquals(18, sizer.estimate(10, "sample", probe, 100)); + assertEquals(3, attempts.get()); + } + + @Test + void reportsExhaustedRetryBudget() { + RetryingSizeProbe probe = + new RetryingSizeProbe<>(ignored -> { throw new IllegalStateException("failed"); }, 2); + + assertThrows(SizeProbeException.class, () -> probe.measureBytes("sample")); + } + + @Test + void rejectsInconsistentMeasurements() { + AdaptiveBatchSizer sizer = new AdaptiveBatchSizer(1_000, 0.8); + assertThrows(IllegalArgumentException.class, () -> sizer.estimate(10, 99, 100)); + } +} From ff061e6da694945d03ef2b0eba01f5da59d9ce18 Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:54:09 +0200 Subject: [PATCH 18/34] Test queue strategies and collection operations --- .../concurrent/QueueStrategiesTest.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/test/java/io/github/azamir911/concurrent/QueueStrategiesTest.java diff --git a/src/test/java/io/github/azamir911/concurrent/QueueStrategiesTest.java b/src/test/java/io/github/azamir911/concurrent/QueueStrategiesTest.java new file mode 100644 index 0000000..57552f7 --- /dev/null +++ b/src/test/java/io/github/azamir911/concurrent/QueueStrategiesTest.java @@ -0,0 +1,81 @@ +package io.github.azamir911.concurrent; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.junit.jupiter.api.Test; + +class QueueStrategiesTest { + @Test + void factoryCreatesBothSupportedWaitStrategies() { + assertInstanceOf( + BoundedBlockingQueue.class, + CloseableQueues.bounded(4, QueueWaitStrategy.CONDITION_SIGNALING)); + assertInstanceOf( + PollingBackoffQueue.class, + CloseableQueues.bounded( + 4, QueueWaitStrategy.POLLING_BACKOFF, Duration.ofMillis(5))); + } + + @Test + void queueCollectionOperationsUseLogicalFifoOrder() throws Exception { + CloseableBlockingQueue queue = new BoundedBlockingQueue<>(4); + queue.put(10); + queue.put(20); + queue.put(30); + + assertEquals(List.of(10, 20, 30), queue.snapshot()); + assertTrue(queue.contains(20)); + assertEquals(1, queue.indexOf(20)); + assertEquals(-1, queue.indexOf(99)); + assertEquals(3, queue.clear()); + assertEquals(List.of(), queue.snapshot()); + assertEquals(4, queue.remainingCapacity()); + } + + @Test + void pollingProducerWaitsUntilCapacityBecomesAvailable() throws Exception { + CloseableBlockingQueue queue = + new PollingBackoffQueue<>(1, Duration.ofMillis(5)); + queue.put(1); + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + Future producer = executor.submit(() -> { queue.put(2); return null; }); + Thread.sleep(30); + assertEquals(1, queue.take()); + producer.get(2, SECONDS); + assertEquals(2, queue.take()); + } finally { + queue.close(); + executor.shutdownNow(); + } + } + + @Test + void pollingCloseReleasesBlockedConsumerWithinBackoffInterval() throws Exception { + CloseableBlockingQueue queue = + new PollingBackoffQueue<>(1, Duration.ofMillis(5)); + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + Future consumer = executor.submit(queue::take); + Thread.sleep(30); + queue.close(); + + ExecutionException failure = + assertThrows(ExecutionException.class, () -> consumer.get(2, SECONDS)); + assertInstanceOf(QueueClosedException.class, failure.getCause()); + } finally { + queue.close(); + executor.shutdownNow(); + } + } +} From 8feea3b721ed10acd3e9ab9fd315d199ba5815f1 Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:54:57 +0200 Subject: [PATCH 19/34] Rename project to Java backend primitives --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index a43bf71..db60dfc 100644 --- a/pom.xml +++ b/pom.xml @@ -5,11 +5,11 @@ 4.0.0 io.github.azamir911 - java-concurrency-primitives + java-backend-primitives 1.0.0-SNAPSHOT - Java Concurrency Primitives - Dependency-free Java concurrency primitives with explicit lifecycle semantics. - https://github.com/azamir911/java-concurrency-primitives + Java Backend Primitives + Dependency-free Java primitives for concurrency, streaming statistics, decimal math, and adaptive batching. + https://github.com/azamir911/java-backend-primitives From e3b0474ed65a98e011c5fa684ba6c873aa618d1b Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:55:52 +0200 Subject: [PATCH 20/34] Document complete backend primitive replacement --- README.md | 129 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 60b4cd1..d929a82 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,63 @@ -# Java Concurrency Primitives +# Java Backend Primitives -[![CI](https://github.com/azamir911/java-concurrency-primitives/actions/workflows/ci.yml/badge.svg)](https://github.com/azamir911/java-concurrency-primitives/actions/workflows/ci.yml) +[![CI](https://github.com/azamir911/java-backend-primitives/actions/workflows/ci.yml/badge.svg)](https://github.com/azamir911/java-backend-primitives/actions/workflows/ci.yml) -A small Java 21 library that explores explicit concurrency contracts instead of hiding lifecycle and backpressure behind a framework. +A dependency-free Java 21 library of small backend building blocks for concurrency, streaming statistics, precise decimal math, and adaptive batch sizing. -The project currently contains: +The project favors explicit contracts, deterministic behavior, and testable lifecycle rules over framework-specific integrations. -- `BoundedBlockingQueue`: a FIFO ring buffer backed by `ReentrantLock` and two `Condition` instances. -- `ResettableLongAccumulator`: a lock-free accumulator built on `AtomicLong` with atomic snapshot-and-reset semantics. -- deterministic unit tests, producer-consumer stress coverage, JaCoCo reports, and JMH benchmarks. +## Components -## Why this project exists +### Concurrent queues -Concurrent data structures become difficult when shutdown behavior is left implicit. This implementation treats lifecycle as part of the API: +- `CloseableBlockingQueue` defines bounded FIFO, timed operations, draining, search, snapshots, clearing, and explicit close semantics. +- `BoundedBlockingQueue` uses `ReentrantLock` and `Condition` signaling for efficient producer-consumer coordination. +- `PollingBackoffQueue` provides the same public contract with configurable polling backoff. +- `QueueWaitStrategy` and `CloseableQueues` select the desired wait model. +- `QueueClosedException` distinguishes normal closed-and-drained lifecycle completion from interruption. -- capacity provides backpressure rather than unbounded memory growth; -- producers block while the queue is full; -- consumers block while the queue is empty; -- `close()` rejects future writes and wakes all waiting threads; -- accepted elements remain available until the closed queue is drained; -- interruption is propagated through all blocking operations. +### Streaming statistics + +- `StreamingStatistics` calculates count, sum, mean, sample variance, sample standard deviation, minimum, and maximum in one pass. +- Welford's algorithm provides stable online variance. +- Compensated summation reduces floating-point error. +- All updates, snapshots, and resets are thread-safe. + +### Decimal ratios + +- `DecimalRatioCalculator` calculates ratios with explicit scale and `RoundingMode`. +- An optional additive adjustment supports calibrated ratio calculations. +- `BigDecimal.valueOf` is used by double convenience methods to avoid direct binary floating-point construction artifacts. + +### Adaptive batching + +- `AdaptiveBatchSizer` estimates how many records fit in a target serialized capacity. +- It separates fixed overhead from per-record payload, applies a safety factor, and enforces minimum and maximum batch sizes. +- `RetryingSizeProbe` retries transient serialization-size failures within a fixed attempt budget. +- `CheckedSizeProbe` keeps the sizing algorithm independent from any serializer or framework. + +### Additional utility + +- `ResettableLongAccumulator` provides lock-free associative accumulation with atomic snapshot-and-reset behavior. + +## Replacement map + +The repository was rebuilt as an independent project. The previous source files are removed, but every meaningful capability now has a clear replacement: + +| Previous responsibility | Independent replacement | Notes | +|---|---|---| +| Shared FIFO base queue | `CloseableBlockingQueue` | Common lifecycle and collection contract | +| Lock and condition queue | `BoundedBlockingQueue` | Efficient signaling, timed operations, close and drain | +| Sleeping or polling queue | `PollingBackoffQueue` | Configurable polling interval with the same queue contract | +| Queue mode selection | `QueueWaitStrategy` and `CloseableQueues` | Explicit factory-based strategy selection | +| Released queue exception | `QueueClosedException` | Clear closed-for-write and closed-and-drained semantics | +| Interrupted queue exception | Standard `InterruptedException` | Preserves Java interruption semantics | +| Statistical accumulator | `StreamingStatistics` | Count, sum, mean, variance, deviation, min, max, reset | +| Ratio calculation | `DecimalRatioCalculator` | Factor, scale, rounding mode, decimal precision | +| Chunk-size estimation | `AdaptiveBatchSizer` | General serialized batch-size projection | +| Serialization retries | `RetryingSizeProbe` | Framework-independent retry budget | + +No previous package names, internal identifiers, comments, framework types, or source implementations are reused. ## Requirements @@ -32,12 +70,13 @@ Concurrent data structures become difficult when shutdown behavior is left impli mvn verify ``` -The verification phase compiles the library, runs unit and concurrency stress tests, and creates a JaCoCo report under `target/site/jacoco`. +The verification phase compiles the library, runs deterministic unit tests and multi-threaded stress tests, and creates a JaCoCo report under `target/site/jacoco`. ## Queue example ```java -BoundedBlockingQueue queue = new BoundedBlockingQueue<>(128); +CloseableBlockingQueue queue = + CloseableQueues.bounded(128, QueueWaitStrategy.CONDITION_SIGNALING); Thread producer = Thread.ofVirtual().start(() -> { try { @@ -55,13 +94,13 @@ try { System.out.println(queue.take()); } } catch (QueueClosedException drained) { - // The producer closed the queue and all accepted elements were consumed. + // Closed and fully drained. } producer.join(); ``` -## Queue lifecycle +### Queue lifecycle | State | `put` / `offer` | `take` | `poll` | |---|---|---|---| @@ -70,42 +109,60 @@ producer.join(); | Closed with buffered data | Throws `QueueClosedException` | Drains buffered data | Drains buffered data | | Closed and drained | Throws `QueueClosedException` | Throws `QueueClosedException` | Returns empty | -Calling `close()` is idempotent. It does not discard elements that were already accepted. +`close()` is idempotent and does not discard accepted elements. `clear()` removes buffered elements without closing the queue. -## Accumulator example +## Streaming statistics example ```java -ResettableLongAccumulator bytes = ResettableLongAccumulator.sum(); +StreamingStatistics statistics = new StreamingStatistics(); +statistics.add(12.5); +statistics.add(18.0); +statistics.add(14.5); + +StatisticsSnapshot snapshot = statistics.snapshot(); +double mean = snapshot.mean().orElseThrow(); +double sampleDeviation = snapshot.sampleStandardDeviation().orElseThrow(); +``` -bytes.accumulate(1_024); -bytes.accumulate(2_048); +## Ratio example -long intervalTotal = bytes.getAndReset(); // 3072 +```java +DecimalRatioCalculator ratio = + new DecimalRatioCalculator(new BigDecimal("0.10")); + +BigDecimal result = ratio.calculate( + new BigDecimal("2"), + new BigDecimal("3"), + 2, + RoundingMode.HALF_UP); ``` -Custom associative operations are supported: +## Adaptive batch sizing example ```java -ResettableLongAccumulator peak = - ResettableLongAccumulator.max(Long.MIN_VALUE); +RetryingSizeProbe probe = + new RetryingSizeProbe<>(bytes -> bytes.length, 3); + +AdaptiveBatchSizer sizer = + new AdaptiveBatchSizer(1_048_576, 0.85, 1, 100_000); + +int recordsPerBatch = sizer.estimate( + 1_000, + serializedSample, + probe, + headerBytes); ``` ## Benchmarks -Build the JMH executable: +Build and run the JMH queue benchmark: ```bash mvn -Pbenchmarks -DskipTests package java -jar target/benchmarks.jar ``` -Benchmark results depend on the machine, JVM, queue capacity, and producer-consumer ratio. The repository intentionally includes the harness rather than publishing a hardware-independent performance claim. - -## Design notes - -`BoundedBlockingQueue` uses a fixed-size circular array. All structural state is protected by one lock, while separate `notEmpty` and `notFull` conditions avoid waking unrelated waiters. The implementation supports an optional fair lock when starvation resistance is more important than maximum throughput. - -`ResettableLongAccumulator` delegates updates to `AtomicLong.accumulateAndGet`. The function may be retried under contention, so it must be side-effect free and associative. +Benchmark results depend on the machine, JVM, queue capacity, wait strategy, and producer-consumer ratio. The repository includes a reproducible harness rather than publishing a hardware-independent performance claim. ## License From 6ebaf3d31b3532ffa129fc0ab6828a435ae7c849 Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:58:17 +0200 Subject: [PATCH 21/34] Complete queue lifecycle contract --- .../azamir911/concurrent/CloseableBlockingQueue.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/io/github/azamir911/concurrent/CloseableBlockingQueue.java b/src/main/java/io/github/azamir911/concurrent/CloseableBlockingQueue.java index edd6c7b..9359a65 100644 --- a/src/main/java/io/github/azamir911/concurrent/CloseableBlockingQueue.java +++ b/src/main/java/io/github/azamir911/concurrent/CloseableBlockingQueue.java @@ -22,10 +22,14 @@ public interface CloseableBlockingQueue extends AutoCloseable { Optional poll(Duration timeout) throws InterruptedException; + Optional peek(); + int drainTo(Collection target, int maxElements); int clear(); + int closeNow(); + boolean contains(Object value); int indexOf(Object value); @@ -36,6 +40,10 @@ public interface CloseableBlockingQueue extends AutoCloseable { int size(); + default boolean isEmpty() { + return size() == 0; + } + int capacity(); int remainingCapacity(); From 883a51217b5bff96f2688e83243720fa5591e7dd Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:58:54 +0200 Subject: [PATCH 22/34] Add complete queue inspection and immediate close semantics --- .../concurrent/BoundedBlockingQueue.java | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/github/azamir911/concurrent/BoundedBlockingQueue.java b/src/main/java/io/github/azamir911/concurrent/BoundedBlockingQueue.java index b0d17cc..a761588 100644 --- a/src/main/java/io/github/azamir911/concurrent/BoundedBlockingQueue.java +++ b/src/main/java/io/github/azamir911/concurrent/BoundedBlockingQueue.java @@ -131,6 +131,16 @@ public Optional poll(Duration timeout) throws InterruptedException { } } + @Override + public Optional peek() { + lock.lock(); + try { + return size == 0 ? Optional.empty() : Optional.of(elementAt(0)); + } finally { + lock.unlock(); + } + } + @Override public int drainTo(Collection target, int maxElements) { Objects.requireNonNull(target, "target"); @@ -154,13 +164,7 @@ public int drainTo(Collection target, int maxElements) { public int clear() { lock.lock(); try { - int removed = size; - for (int i = 0; i < removed; i++) { - elements[(head + i) % elements.length] = null; - } - head = 0; - tail = 0; - size = 0; + int removed = clearElements(); if (removed > 0) { notFull.signalAll(); } @@ -170,6 +174,20 @@ public int clear() { } } + @Override + public int closeNow() { + lock.lock(); + try { + closed = true; + int discarded = clearElements(); + notEmpty.signalAll(); + notFull.signalAll(); + return discarded; + } finally { + lock.unlock(); + } + } + @Override public boolean contains(Object value) { return indexOf(value) >= 0; @@ -277,6 +295,17 @@ private E dequeue() { return element; } + private int clearElements() { + int removed = size; + for (int i = 0; i < removed; i++) { + elements[(head + i) % elements.length] = null; + } + head = 0; + tail = 0; + size = 0; + return removed; + } + @SuppressWarnings("unchecked") private E elementAt(int logicalIndex) { return (E) elements[(head + logicalIndex) % elements.length]; From 059c48bbbdf616f0dcdb2cd141e82f6859179b0d Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 20:59:31 +0200 Subject: [PATCH 23/34] Complete polling queue lifecycle semantics --- .../concurrent/PollingBackoffQueue.java | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/src/main/java/io/github/azamir911/concurrent/PollingBackoffQueue.java b/src/main/java/io/github/azamir911/concurrent/PollingBackoffQueue.java index 3fa9667..f63db1f 100644 --- a/src/main/java/io/github/azamir911/concurrent/PollingBackoffQueue.java +++ b/src/main/java/io/github/azamir911/concurrent/PollingBackoffQueue.java @@ -132,6 +132,16 @@ public Optional poll(Duration timeout) throws InterruptedException { } } + @Override + public Optional peek() { + lock.lock(); + try { + return size == 0 ? Optional.empty() : Optional.of(elementAt(0)); + } finally { + lock.unlock(); + } + } + @Override public int drainTo(Collection target, int maxElements) { Objects.requireNonNull(target, "target"); @@ -154,14 +164,18 @@ public int drainTo(Collection target, int maxElements) { public int clear() { lock.lock(); try { - int removed = size; - for (int i = 0; i < removed; i++) { - elements[(head + i) % elements.length] = null; - } - head = 0; - tail = 0; - size = 0; - return removed; + return clearElements(); + } finally { + lock.unlock(); + } + } + + @Override + public int closeNow() { + lock.lock(); + try { + closed = true; + return clearElements(); } finally { lock.unlock(); } @@ -267,6 +281,17 @@ private E dequeue() { return element; } + private int clearElements() { + int removed = size; + for (int i = 0; i < removed; i++) { + elements[(head + i) % elements.length] = null; + } + head = 0; + tail = 0; + size = 0; + return removed; + } + @SuppressWarnings("unchecked") private E elementAt(int logicalIndex) { return (E) elements[(head + logicalIndex) % elements.length]; From e45a85ed2e347285c4c1ef3ad6e1357874010814 Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 21:00:10 +0200 Subject: [PATCH 24/34] Test complete queue lifecycle behavior --- .../concurrent/QueueStrategiesTest.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/test/java/io/github/azamir911/concurrent/QueueStrategiesTest.java b/src/test/java/io/github/azamir911/concurrent/QueueStrategiesTest.java index 57552f7..f8f885f 100644 --- a/src/test/java/io/github/azamir911/concurrent/QueueStrategiesTest.java +++ b/src/test/java/io/github/azamir911/concurrent/QueueStrategiesTest.java @@ -2,6 +2,7 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -33,15 +34,32 @@ void queueCollectionOperationsUseLogicalFifoOrder() throws Exception { queue.put(20); queue.put(30); + assertFalse(queue.isEmpty()); + assertEquals(10, queue.peek().orElseThrow()); assertEquals(List.of(10, 20, 30), queue.snapshot()); assertTrue(queue.contains(20)); assertEquals(1, queue.indexOf(20)); assertEquals(-1, queue.indexOf(99)); assertEquals(3, queue.clear()); + assertTrue(queue.isEmpty()); assertEquals(List.of(), queue.snapshot()); assertEquals(4, queue.remainingCapacity()); } + @Test + void closeNowAtomicallyDiscardsBufferedDataAndRejectsWrites() throws Exception { + CloseableBlockingQueue queue = new BoundedBlockingQueue<>(3); + queue.put(1); + queue.put(2); + + assertEquals(2, queue.closeNow()); + assertTrue(queue.isClosed()); + assertTrue(queue.isEmpty()); + assertTrue(queue.peek().isEmpty()); + assertThrows(QueueClosedException.class, () -> queue.put(3)); + assertThrows(QueueClosedException.class, queue::take); + } + @Test void pollingProducerWaitsUntilCapacityBecomesAvailable() throws Exception { CloseableBlockingQueue queue = @@ -49,7 +67,12 @@ void pollingProducerWaitsUntilCapacityBecomesAvailable() throws Exception { queue.put(1); ExecutorService executor = Executors.newSingleThreadExecutor(); try { - Future producer = executor.submit(() -> { queue.put(2); return null; }); + Future producer = + executor.submit( + () -> { + queue.put(2); + return null; + }); Thread.sleep(30); assertEquals(1, queue.take()); producer.get(2, SECONDS); From 97502404053ee8823d7b35ba42ea75748409ce8c Mon Sep 17 00:00:00 2001 From: Avishay Date: Sun, 19 Jul 2026 21:03:02 +0200 Subject: [PATCH 25/34] Benchmark both queue wait strategies --- .../concurrent/BoundedBlockingQueueBenchmark.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/jmh/java/io/github/azamir911/concurrent/BoundedBlockingQueueBenchmark.java b/src/jmh/java/io/github/azamir911/concurrent/BoundedBlockingQueueBenchmark.java index 30cfc3b..f1cc0c2 100644 --- a/src/jmh/java/io/github/azamir911/concurrent/BoundedBlockingQueueBenchmark.java +++ b/src/jmh/java/io/github/azamir911/concurrent/BoundedBlockingQueueBenchmark.java @@ -1,5 +1,6 @@ package io.github.azamir911.concurrent; +import java.time.Duration; import java.util.concurrent.TimeUnit; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; @@ -14,6 +15,7 @@ import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; import org.openjdk.jmh.annotations.Warmup; @BenchmarkMode(Mode.Throughput) @@ -27,11 +29,19 @@ public static class QueueState { @Param({"1024"}) int capacity; - BoundedBlockingQueue queue; + @Param({"CONDITION_SIGNALING", "POLLING_BACKOFF"}) + QueueWaitStrategy strategy; + + CloseableBlockingQueue queue; @Setup(Level.Iteration) public void setUp() { - queue = new BoundedBlockingQueue<>(capacity); + queue = CloseableQueues.bounded(capacity, strategy, Duration.ofNanos(1_000)); + } + + @TearDown(Level.Iteration) + public void tearDown() { + queue.closeNow(); } } From 37c2d0ea34f02fd37e2f71651d23e4902e34a644 Mon Sep 17 00:00:00 2001 From: Avishay Date: Mon, 20 Jul 2026 00:59:57 +0200 Subject: [PATCH 26/34] Rename Java package namespace --- .../workflows/rename-package-namespace.yml | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/rename-package-namespace.yml diff --git a/.github/workflows/rename-package-namespace.yml b/.github/workflows/rename-package-namespace.yml new file mode 100644 index 0000000..cbfbd04 --- /dev/null +++ b/.github/workflows/rename-package-namespace.yml @@ -0,0 +1,52 @@ +name: Rename package namespace + +on: + push: + branches: + - agent/rebuild-java-concurrency-primitives + +permissions: + contents: write + +jobs: + rename: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - name: Check out branch + uses: actions/checkout@v4 + with: + ref: agent/rebuild-java-concurrency-primitives + fetch-depth: 0 + + - name: Rename packages and source paths + shell: bash + run: | + set -euo pipefail + + old_path="io/github/azamir911" + new_path="dev/avishay/primitives" + + for source_root in src/main/java src/test/java src/jmh/java; do + if [ -d "$source_root/$old_path" ]; then + mkdir -p "$source_root/dev/avishay" + git mv "$source_root/$old_path" "$source_root/$new_path" + fi + done + + find src -type f -name '*.java' -print0 \ + | xargs -0 sed -i 's/io\.github\.azamir911/dev.avishay.primitives/g' + + sed -i 's#io.github.azamir911#dev.avishay#' pom.xml + + rm .github/workflows/rename-package-namespace.yml + + - name: Commit package rename + shell: bash + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "Use clean package namespace" + git push origin HEAD:agent/rebuild-java-concurrency-primitives From ad9699e8a643a739db322590f9bc0f0c31fe012f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:00:04 +0000 Subject: [PATCH 27/34] Use clean package namespace --- .../workflows/rename-package-namespace.yml | 52 ------------------- pom.xml | 2 +- .../BoundedBlockingQueueBenchmark.java | 2 +- .../batching/AdaptiveBatchSizer.java | 2 +- .../batching/CheckedSizeProbe.java | 2 +- .../batching/RetryingSizeProbe.java | 2 +- .../batching/SizeProbeException.java | 2 +- .../concurrent/BoundedBlockingQueue.java | 2 +- .../concurrent/CloseableBlockingQueue.java | 2 +- .../concurrent/CloseableQueues.java | 2 +- .../concurrent/PollingBackoffQueue.java | 2 +- .../concurrent/QueueClosedException.java | 2 +- .../concurrent/QueueWaitStrategy.java | 2 +- .../concurrent/ResettableLongAccumulator.java | 2 +- .../math/DecimalRatioCalculator.java | 2 +- .../statistics/StatisticsSnapshot.java | 2 +- .../statistics/StreamingStatistics.java | 2 +- .../batching/AdaptiveBatchSizerTest.java | 2 +- .../concurrent/BoundedBlockingQueueTest.java | 2 +- .../concurrent/QueueStrategiesTest.java | 2 +- .../ResettableLongAccumulatorTest.java | 2 +- .../math/DecimalRatioCalculatorTest.java | 2 +- .../statistics/StreamingStatisticsTest.java | 2 +- 23 files changed, 22 insertions(+), 74 deletions(-) delete mode 100644 .github/workflows/rename-package-namespace.yml rename src/jmh/java/{io/github/azamir911 => dev/avishay/primitives}/concurrent/BoundedBlockingQueueBenchmark.java (97%) rename src/main/java/{io/github/azamir911 => dev/avishay/primitives}/batching/AdaptiveBatchSizer.java (98%) rename src/main/java/{io/github/azamir911 => dev/avishay/primitives}/batching/CheckedSizeProbe.java (81%) rename src/main/java/{io/github/azamir911 => dev/avishay/primitives}/batching/RetryingSizeProbe.java (97%) rename src/main/java/{io/github/azamir911 => dev/avishay/primitives}/batching/SizeProbeException.java (86%) rename src/main/java/{io/github/azamir911 => dev/avishay/primitives}/concurrent/BoundedBlockingQueue.java (99%) rename src/main/java/{io/github/azamir911 => dev/avishay/primitives}/concurrent/CloseableBlockingQueue.java (96%) rename src/main/java/{io/github/azamir911 => dev/avishay/primitives}/concurrent/CloseableQueues.java (95%) rename src/main/java/{io/github/azamir911 => dev/avishay/primitives}/concurrent/PollingBackoffQueue.java (99%) rename src/main/java/{io/github/azamir911 => dev/avishay/primitives}/concurrent/QueueClosedException.java (84%) rename src/main/java/{io/github/azamir911 => dev/avishay/primitives}/concurrent/QueueWaitStrategy.java (76%) rename src/main/java/{io/github/azamir911 => dev/avishay/primitives}/concurrent/ResettableLongAccumulator.java (96%) rename src/main/java/{io/github/azamir911 => dev/avishay/primitives}/math/DecimalRatioCalculator.java (98%) rename src/main/java/{io/github/azamir911 => dev/avishay/primitives}/statistics/StatisticsSnapshot.java (88%) rename src/main/java/{io/github/azamir911 => dev/avishay/primitives}/statistics/StreamingStatistics.java (98%) rename src/test/java/{io/github/azamir911 => dev/avishay/primitives}/batching/AdaptiveBatchSizerTest.java (97%) rename src/test/java/{io/github/azamir911 => dev/avishay/primitives}/concurrent/BoundedBlockingQueueTest.java (99%) rename src/test/java/{io/github/azamir911 => dev/avishay/primitives}/concurrent/QueueStrategiesTest.java (98%) rename src/test/java/{io/github/azamir911 => dev/avishay/primitives}/concurrent/ResettableLongAccumulatorTest.java (97%) rename src/test/java/{io/github/azamir911 => dev/avishay/primitives}/math/DecimalRatioCalculatorTest.java (97%) rename src/test/java/{io/github/azamir911 => dev/avishay/primitives}/statistics/StreamingStatisticsTest.java (98%) diff --git a/.github/workflows/rename-package-namespace.yml b/.github/workflows/rename-package-namespace.yml deleted file mode 100644 index cbfbd04..0000000 --- a/.github/workflows/rename-package-namespace.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Rename package namespace - -on: - push: - branches: - - agent/rebuild-java-concurrency-primitives - -permissions: - contents: write - -jobs: - rename: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - name: Check out branch - uses: actions/checkout@v4 - with: - ref: agent/rebuild-java-concurrency-primitives - fetch-depth: 0 - - - name: Rename packages and source paths - shell: bash - run: | - set -euo pipefail - - old_path="io/github/azamir911" - new_path="dev/avishay/primitives" - - for source_root in src/main/java src/test/java src/jmh/java; do - if [ -d "$source_root/$old_path" ]; then - mkdir -p "$source_root/dev/avishay" - git mv "$source_root/$old_path" "$source_root/$new_path" - fi - done - - find src -type f -name '*.java' -print0 \ - | xargs -0 sed -i 's/io\.github\.azamir911/dev.avishay.primitives/g' - - sed -i 's#io.github.azamir911#dev.avishay#' pom.xml - - rm .github/workflows/rename-package-namespace.yml - - - name: Commit package rename - shell: bash - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git commit -m "Use clean package namespace" - git push origin HEAD:agent/rebuild-java-concurrency-primitives diff --git a/pom.xml b/pom.xml index db60dfc..7ba2b62 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - io.github.azamir911 + dev.avishay java-backend-primitives 1.0.0-SNAPSHOT Java Backend Primitives diff --git a/src/jmh/java/io/github/azamir911/concurrent/BoundedBlockingQueueBenchmark.java b/src/jmh/java/dev/avishay/primitives/concurrent/BoundedBlockingQueueBenchmark.java similarity index 97% rename from src/jmh/java/io/github/azamir911/concurrent/BoundedBlockingQueueBenchmark.java rename to src/jmh/java/dev/avishay/primitives/concurrent/BoundedBlockingQueueBenchmark.java index f1cc0c2..071a018 100644 --- a/src/jmh/java/io/github/azamir911/concurrent/BoundedBlockingQueueBenchmark.java +++ b/src/jmh/java/dev/avishay/primitives/concurrent/BoundedBlockingQueueBenchmark.java @@ -1,4 +1,4 @@ -package io.github.azamir911.concurrent; +package dev.avishay.primitives.concurrent; import java.time.Duration; import java.util.concurrent.TimeUnit; diff --git a/src/main/java/io/github/azamir911/batching/AdaptiveBatchSizer.java b/src/main/java/dev/avishay/primitives/batching/AdaptiveBatchSizer.java similarity index 98% rename from src/main/java/io/github/azamir911/batching/AdaptiveBatchSizer.java rename to src/main/java/dev/avishay/primitives/batching/AdaptiveBatchSizer.java index 7790b6c..3d8ab45 100644 --- a/src/main/java/io/github/azamir911/batching/AdaptiveBatchSizer.java +++ b/src/main/java/dev/avishay/primitives/batching/AdaptiveBatchSizer.java @@ -1,4 +1,4 @@ -package io.github.azamir911.batching; +package dev.avishay.primitives.batching; import java.util.Objects; diff --git a/src/main/java/io/github/azamir911/batching/CheckedSizeProbe.java b/src/main/java/dev/avishay/primitives/batching/CheckedSizeProbe.java similarity index 81% rename from src/main/java/io/github/azamir911/batching/CheckedSizeProbe.java rename to src/main/java/dev/avishay/primitives/batching/CheckedSizeProbe.java index 51cdece..1e0ecf0 100644 --- a/src/main/java/io/github/azamir911/batching/CheckedSizeProbe.java +++ b/src/main/java/dev/avishay/primitives/batching/CheckedSizeProbe.java @@ -1,4 +1,4 @@ -package io.github.azamir911.batching; +package dev.avishay.primitives.batching; /** Measures a serialized size and may fail with a checked exception. */ @FunctionalInterface diff --git a/src/main/java/io/github/azamir911/batching/RetryingSizeProbe.java b/src/main/java/dev/avishay/primitives/batching/RetryingSizeProbe.java similarity index 97% rename from src/main/java/io/github/azamir911/batching/RetryingSizeProbe.java rename to src/main/java/dev/avishay/primitives/batching/RetryingSizeProbe.java index efc53d4..a630222 100644 --- a/src/main/java/io/github/azamir911/batching/RetryingSizeProbe.java +++ b/src/main/java/dev/avishay/primitives/batching/RetryingSizeProbe.java @@ -1,4 +1,4 @@ -package io.github.azamir911.batching; +package dev.avishay.primitives.batching; import java.util.Objects; diff --git a/src/main/java/io/github/azamir911/batching/SizeProbeException.java b/src/main/java/dev/avishay/primitives/batching/SizeProbeException.java similarity index 86% rename from src/main/java/io/github/azamir911/batching/SizeProbeException.java rename to src/main/java/dev/avishay/primitives/batching/SizeProbeException.java index 51a7d9b..fc9a0e1 100644 --- a/src/main/java/io/github/azamir911/batching/SizeProbeException.java +++ b/src/main/java/dev/avishay/primitives/batching/SizeProbeException.java @@ -1,4 +1,4 @@ -package io.github.azamir911.batching; +package dev.avishay.primitives.batching; /** Raised when a serialized size cannot be measured within the configured retry budget. */ public final class SizeProbeException extends RuntimeException { diff --git a/src/main/java/io/github/azamir911/concurrent/BoundedBlockingQueue.java b/src/main/java/dev/avishay/primitives/concurrent/BoundedBlockingQueue.java similarity index 99% rename from src/main/java/io/github/azamir911/concurrent/BoundedBlockingQueue.java rename to src/main/java/dev/avishay/primitives/concurrent/BoundedBlockingQueue.java index a761588..8d3c39d 100644 --- a/src/main/java/io/github/azamir911/concurrent/BoundedBlockingQueue.java +++ b/src/main/java/dev/avishay/primitives/concurrent/BoundedBlockingQueue.java @@ -1,4 +1,4 @@ -package io.github.azamir911.concurrent; +package dev.avishay.primitives.concurrent; import java.time.Duration; import java.util.ArrayList; diff --git a/src/main/java/io/github/azamir911/concurrent/CloseableBlockingQueue.java b/src/main/java/dev/avishay/primitives/concurrent/CloseableBlockingQueue.java similarity index 96% rename from src/main/java/io/github/azamir911/concurrent/CloseableBlockingQueue.java rename to src/main/java/dev/avishay/primitives/concurrent/CloseableBlockingQueue.java index 9359a65..3a5faa7 100644 --- a/src/main/java/io/github/azamir911/concurrent/CloseableBlockingQueue.java +++ b/src/main/java/dev/avishay/primitives/concurrent/CloseableBlockingQueue.java @@ -1,4 +1,4 @@ -package io.github.azamir911.concurrent; +package dev.avishay.primitives.concurrent; import java.time.Duration; import java.util.Collection; diff --git a/src/main/java/io/github/azamir911/concurrent/CloseableQueues.java b/src/main/java/dev/avishay/primitives/concurrent/CloseableQueues.java similarity index 95% rename from src/main/java/io/github/azamir911/concurrent/CloseableQueues.java rename to src/main/java/dev/avishay/primitives/concurrent/CloseableQueues.java index e79e3c6..e05750e 100644 --- a/src/main/java/io/github/azamir911/concurrent/CloseableQueues.java +++ b/src/main/java/dev/avishay/primitives/concurrent/CloseableQueues.java @@ -1,4 +1,4 @@ -package io.github.azamir911.concurrent; +package dev.avishay.primitives.concurrent; import java.time.Duration; import java.util.Objects; diff --git a/src/main/java/io/github/azamir911/concurrent/PollingBackoffQueue.java b/src/main/java/dev/avishay/primitives/concurrent/PollingBackoffQueue.java similarity index 99% rename from src/main/java/io/github/azamir911/concurrent/PollingBackoffQueue.java rename to src/main/java/dev/avishay/primitives/concurrent/PollingBackoffQueue.java index f63db1f..a27f989 100644 --- a/src/main/java/io/github/azamir911/concurrent/PollingBackoffQueue.java +++ b/src/main/java/dev/avishay/primitives/concurrent/PollingBackoffQueue.java @@ -1,4 +1,4 @@ -package io.github.azamir911.concurrent; +package dev.avishay.primitives.concurrent; import java.time.Duration; import java.util.ArrayList; diff --git a/src/main/java/io/github/azamir911/concurrent/QueueClosedException.java b/src/main/java/dev/avishay/primitives/concurrent/QueueClosedException.java similarity index 84% rename from src/main/java/io/github/azamir911/concurrent/QueueClosedException.java rename to src/main/java/dev/avishay/primitives/concurrent/QueueClosedException.java index f80ad6d..4290345 100644 --- a/src/main/java/io/github/azamir911/concurrent/QueueClosedException.java +++ b/src/main/java/dev/avishay/primitives/concurrent/QueueClosedException.java @@ -1,4 +1,4 @@ -package io.github.azamir911.concurrent; +package dev.avishay.primitives.concurrent; /** Signals that an operation cannot continue because a queue has been closed. */ public final class QueueClosedException extends IllegalStateException { diff --git a/src/main/java/io/github/azamir911/concurrent/QueueWaitStrategy.java b/src/main/java/dev/avishay/primitives/concurrent/QueueWaitStrategy.java similarity index 76% rename from src/main/java/io/github/azamir911/concurrent/QueueWaitStrategy.java rename to src/main/java/dev/avishay/primitives/concurrent/QueueWaitStrategy.java index dad2fbe..6a52022 100644 --- a/src/main/java/io/github/azamir911/concurrent/QueueWaitStrategy.java +++ b/src/main/java/dev/avishay/primitives/concurrent/QueueWaitStrategy.java @@ -1,4 +1,4 @@ -package io.github.azamir911.concurrent; +package dev.avishay.primitives.concurrent; /** Selects how a bounded queue waits for capacity or data. */ public enum QueueWaitStrategy { diff --git a/src/main/java/io/github/azamir911/concurrent/ResettableLongAccumulator.java b/src/main/java/dev/avishay/primitives/concurrent/ResettableLongAccumulator.java similarity index 96% rename from src/main/java/io/github/azamir911/concurrent/ResettableLongAccumulator.java rename to src/main/java/dev/avishay/primitives/concurrent/ResettableLongAccumulator.java index 8f00bf9..95ece23 100644 --- a/src/main/java/io/github/azamir911/concurrent/ResettableLongAccumulator.java +++ b/src/main/java/dev/avishay/primitives/concurrent/ResettableLongAccumulator.java @@ -1,4 +1,4 @@ -package io.github.azamir911.concurrent; +package dev.avishay.primitives.concurrent; import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; diff --git a/src/main/java/io/github/azamir911/math/DecimalRatioCalculator.java b/src/main/java/dev/avishay/primitives/math/DecimalRatioCalculator.java similarity index 98% rename from src/main/java/io/github/azamir911/math/DecimalRatioCalculator.java rename to src/main/java/dev/avishay/primitives/math/DecimalRatioCalculator.java index a55b3de..a745714 100644 --- a/src/main/java/io/github/azamir911/math/DecimalRatioCalculator.java +++ b/src/main/java/dev/avishay/primitives/math/DecimalRatioCalculator.java @@ -1,4 +1,4 @@ -package io.github.azamir911.math; +package dev.avishay.primitives.math; import java.math.BigDecimal; import java.math.RoundingMode; diff --git a/src/main/java/io/github/azamir911/statistics/StatisticsSnapshot.java b/src/main/java/dev/avishay/primitives/statistics/StatisticsSnapshot.java similarity index 88% rename from src/main/java/io/github/azamir911/statistics/StatisticsSnapshot.java rename to src/main/java/dev/avishay/primitives/statistics/StatisticsSnapshot.java index 7d4bd33..055344b 100644 --- a/src/main/java/io/github/azamir911/statistics/StatisticsSnapshot.java +++ b/src/main/java/dev/avishay/primitives/statistics/StatisticsSnapshot.java @@ -1,4 +1,4 @@ -package io.github.azamir911.statistics; +package dev.avishay.primitives.statistics; import java.util.OptionalDouble; diff --git a/src/main/java/io/github/azamir911/statistics/StreamingStatistics.java b/src/main/java/dev/avishay/primitives/statistics/StreamingStatistics.java similarity index 98% rename from src/main/java/io/github/azamir911/statistics/StreamingStatistics.java rename to src/main/java/dev/avishay/primitives/statistics/StreamingStatistics.java index 4761cf8..b3ab7d3 100644 --- a/src/main/java/io/github/azamir911/statistics/StreamingStatistics.java +++ b/src/main/java/dev/avishay/primitives/statistics/StreamingStatistics.java @@ -1,4 +1,4 @@ -package io.github.azamir911.statistics; +package dev.avishay.primitives.statistics; import java.util.OptionalDouble; diff --git a/src/test/java/io/github/azamir911/batching/AdaptiveBatchSizerTest.java b/src/test/java/dev/avishay/primitives/batching/AdaptiveBatchSizerTest.java similarity index 97% rename from src/test/java/io/github/azamir911/batching/AdaptiveBatchSizerTest.java rename to src/test/java/dev/avishay/primitives/batching/AdaptiveBatchSizerTest.java index 7605718..d80e1b1 100644 --- a/src/test/java/io/github/azamir911/batching/AdaptiveBatchSizerTest.java +++ b/src/test/java/dev/avishay/primitives/batching/AdaptiveBatchSizerTest.java @@ -1,4 +1,4 @@ -package io.github.azamir911.batching; +package dev.avishay.primitives.batching; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/src/test/java/io/github/azamir911/concurrent/BoundedBlockingQueueTest.java b/src/test/java/dev/avishay/primitives/concurrent/BoundedBlockingQueueTest.java similarity index 99% rename from src/test/java/io/github/azamir911/concurrent/BoundedBlockingQueueTest.java rename to src/test/java/dev/avishay/primitives/concurrent/BoundedBlockingQueueTest.java index 04194ab..611be9a 100644 --- a/src/test/java/io/github/azamir911/concurrent/BoundedBlockingQueueTest.java +++ b/src/test/java/dev/avishay/primitives/concurrent/BoundedBlockingQueueTest.java @@ -1,4 +1,4 @@ -package io.github.azamir911.concurrent; +package dev.avishay.primitives.concurrent; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/test/java/io/github/azamir911/concurrent/QueueStrategiesTest.java b/src/test/java/dev/avishay/primitives/concurrent/QueueStrategiesTest.java similarity index 98% rename from src/test/java/io/github/azamir911/concurrent/QueueStrategiesTest.java rename to src/test/java/dev/avishay/primitives/concurrent/QueueStrategiesTest.java index f8f885f..2013557 100644 --- a/src/test/java/io/github/azamir911/concurrent/QueueStrategiesTest.java +++ b/src/test/java/dev/avishay/primitives/concurrent/QueueStrategiesTest.java @@ -1,4 +1,4 @@ -package io.github.azamir911.concurrent; +package dev.avishay.primitives.concurrent; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/test/java/io/github/azamir911/concurrent/ResettableLongAccumulatorTest.java b/src/test/java/dev/avishay/primitives/concurrent/ResettableLongAccumulatorTest.java similarity index 97% rename from src/test/java/io/github/azamir911/concurrent/ResettableLongAccumulatorTest.java rename to src/test/java/dev/avishay/primitives/concurrent/ResettableLongAccumulatorTest.java index f98b497..bbde4b3 100644 --- a/src/test/java/io/github/azamir911/concurrent/ResettableLongAccumulatorTest.java +++ b/src/test/java/dev/avishay/primitives/concurrent/ResettableLongAccumulatorTest.java @@ -1,4 +1,4 @@ -package io.github.azamir911.concurrent; +package dev.avishay.primitives.concurrent; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/test/java/io/github/azamir911/math/DecimalRatioCalculatorTest.java b/src/test/java/dev/avishay/primitives/math/DecimalRatioCalculatorTest.java similarity index 97% rename from src/test/java/io/github/azamir911/math/DecimalRatioCalculatorTest.java rename to src/test/java/dev/avishay/primitives/math/DecimalRatioCalculatorTest.java index 5e07299..7a2f101 100644 --- a/src/test/java/io/github/azamir911/math/DecimalRatioCalculatorTest.java +++ b/src/test/java/dev/avishay/primitives/math/DecimalRatioCalculatorTest.java @@ -1,4 +1,4 @@ -package io.github.azamir911.math; +package dev.avishay.primitives.math; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/src/test/java/io/github/azamir911/statistics/StreamingStatisticsTest.java b/src/test/java/dev/avishay/primitives/statistics/StreamingStatisticsTest.java similarity index 98% rename from src/test/java/io/github/azamir911/statistics/StreamingStatisticsTest.java rename to src/test/java/dev/avishay/primitives/statistics/StreamingStatisticsTest.java index 1cddfde..9d3f987 100644 --- a/src/test/java/io/github/azamir911/statistics/StreamingStatisticsTest.java +++ b/src/test/java/dev/avishay/primitives/statistics/StreamingStatisticsTest.java @@ -1,4 +1,4 @@ -package io.github.azamir911.statistics; +package dev.avishay.primitives.statistics; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; From b58820b6b6aeba508a5a362d179546b7165f6846 Mon Sep 17 00:00:00 2001 From: Avishay Date: Mon, 20 Jul 2026 01:24:56 +0200 Subject: [PATCH 28/34] Add temporary namespace migration --- .github/workflows/rename-namespace.yml | 64 ++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/rename-namespace.yml diff --git a/.github/workflows/rename-namespace.yml b/.github/workflows/rename-namespace.yml new file mode 100644 index 0000000..16268d4 --- /dev/null +++ b/.github/workflows/rename-namespace.yml @@ -0,0 +1,64 @@ +name: Rename Java namespace + +on: + push: + branches: + - agent/rebuild-java-concurrency-primitives + paths: + - .github/workflows/rename-namespace.yml + +permissions: + contents: write + +jobs: + rename: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Rename packages and Maven group + shell: bash + run: | + set -euo pipefail + + for source_root in src/main/java src/test/java src/jmh/java; do + old_path="$source_root/dev/avishay" + new_path="$source_root/dev/azamir" + if [[ -d "$old_path" ]]; then + if [[ -e "$new_path" ]]; then + echo "Destination already exists: $new_path" >&2 + exit 1 + fi + mv "$old_path" "$new_path" + fi + done + + mapfile -t files < <(git grep -Il 'dev\.avishay' || true) + if (( ${#files[@]} > 0 )); then + sed -i 's/dev\.avishay/dev.azamir/g' "${files[@]}" + fi + + rm .github/workflows/rename-namespace.yml + + if git grep -n 'dev\.avishay'; then + echo 'Old namespace is still present' >&2 + exit 1 + fi + + test -d src/main/java/dev/azamir/primitives + test ! -e src/main/java/dev/avishay + test ! -e src/test/java/dev/avishay + test ! -e src/jmh/java/dev/avishay + grep -q 'dev.azamir' pom.xml + + - name: Commit migration + shell: bash + run: | + set -euo pipefail + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add -A + git commit -m 'Use dev.azamir package namespace' + git push From 3104cea830aa5483a70bf61faf713c68f5e4c10c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:25:05 +0000 Subject: [PATCH 29/34] Use dev.azamir package namespace --- .github/workflows/rename-namespace.yml | 64 ------------------- pom.xml | 2 +- .../BoundedBlockingQueueBenchmark.java | 0 .../batching/AdaptiveBatchSizer.java | 0 .../primitives/batching/CheckedSizeProbe.java | 0 .../batching/RetryingSizeProbe.java | 0 .../batching/SizeProbeException.java | 0 .../concurrent/BoundedBlockingQueue.java | 0 .../concurrent/CloseableBlockingQueue.java | 0 .../concurrent/CloseableQueues.java | 0 .../concurrent/PollingBackoffQueue.java | 0 .../concurrent/QueueClosedException.java | 0 .../concurrent/QueueWaitStrategy.java | 0 .../concurrent/ResettableLongAccumulator.java | 0 .../math/DecimalRatioCalculator.java | 0 .../statistics/StatisticsSnapshot.java | 0 .../statistics/StreamingStatistics.java | 0 .../batching/AdaptiveBatchSizerTest.java | 0 .../concurrent/BoundedBlockingQueueTest.java | 0 .../concurrent/QueueStrategiesTest.java | 0 .../ResettableLongAccumulatorTest.java | 0 .../math/DecimalRatioCalculatorTest.java | 0 .../statistics/StreamingStatisticsTest.java | 0 23 files changed, 1 insertion(+), 65 deletions(-) delete mode 100644 .github/workflows/rename-namespace.yml rename src/jmh/java/dev/{avishay => azamir}/primitives/concurrent/BoundedBlockingQueueBenchmark.java (100%) rename src/main/java/dev/{avishay => azamir}/primitives/batching/AdaptiveBatchSizer.java (100%) rename src/main/java/dev/{avishay => azamir}/primitives/batching/CheckedSizeProbe.java (100%) rename src/main/java/dev/{avishay => azamir}/primitives/batching/RetryingSizeProbe.java (100%) rename src/main/java/dev/{avishay => azamir}/primitives/batching/SizeProbeException.java (100%) rename src/main/java/dev/{avishay => azamir}/primitives/concurrent/BoundedBlockingQueue.java (100%) rename src/main/java/dev/{avishay => azamir}/primitives/concurrent/CloseableBlockingQueue.java (100%) rename src/main/java/dev/{avishay => azamir}/primitives/concurrent/CloseableQueues.java (100%) rename src/main/java/dev/{avishay => azamir}/primitives/concurrent/PollingBackoffQueue.java (100%) rename src/main/java/dev/{avishay => azamir}/primitives/concurrent/QueueClosedException.java (100%) rename src/main/java/dev/{avishay => azamir}/primitives/concurrent/QueueWaitStrategy.java (100%) rename src/main/java/dev/{avishay => azamir}/primitives/concurrent/ResettableLongAccumulator.java (100%) rename src/main/java/dev/{avishay => azamir}/primitives/math/DecimalRatioCalculator.java (100%) rename src/main/java/dev/{avishay => azamir}/primitives/statistics/StatisticsSnapshot.java (100%) rename src/main/java/dev/{avishay => azamir}/primitives/statistics/StreamingStatistics.java (100%) rename src/test/java/dev/{avishay => azamir}/primitives/batching/AdaptiveBatchSizerTest.java (100%) rename src/test/java/dev/{avishay => azamir}/primitives/concurrent/BoundedBlockingQueueTest.java (100%) rename src/test/java/dev/{avishay => azamir}/primitives/concurrent/QueueStrategiesTest.java (100%) rename src/test/java/dev/{avishay => azamir}/primitives/concurrent/ResettableLongAccumulatorTest.java (100%) rename src/test/java/dev/{avishay => azamir}/primitives/math/DecimalRatioCalculatorTest.java (100%) rename src/test/java/dev/{avishay => azamir}/primitives/statistics/StreamingStatisticsTest.java (100%) diff --git a/.github/workflows/rename-namespace.yml b/.github/workflows/rename-namespace.yml deleted file mode 100644 index 16268d4..0000000 --- a/.github/workflows/rename-namespace.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Rename Java namespace - -on: - push: - branches: - - agent/rebuild-java-concurrency-primitives - paths: - - .github/workflows/rename-namespace.yml - -permissions: - contents: write - -jobs: - rename: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Rename packages and Maven group - shell: bash - run: | - set -euo pipefail - - for source_root in src/main/java src/test/java src/jmh/java; do - old_path="$source_root/dev/avishay" - new_path="$source_root/dev/azamir" - if [[ -d "$old_path" ]]; then - if [[ -e "$new_path" ]]; then - echo "Destination already exists: $new_path" >&2 - exit 1 - fi - mv "$old_path" "$new_path" - fi - done - - mapfile -t files < <(git grep -Il 'dev\.avishay' || true) - if (( ${#files[@]} > 0 )); then - sed -i 's/dev\.avishay/dev.azamir/g' "${files[@]}" - fi - - rm .github/workflows/rename-namespace.yml - - if git grep -n 'dev\.avishay'; then - echo 'Old namespace is still present' >&2 - exit 1 - fi - - test -d src/main/java/dev/azamir/primitives - test ! -e src/main/java/dev/avishay - test ! -e src/test/java/dev/avishay - test ! -e src/jmh/java/dev/avishay - grep -q 'dev.azamir' pom.xml - - - name: Commit migration - shell: bash - run: | - set -euo pipefail - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add -A - git commit -m 'Use dev.azamir package namespace' - git push diff --git a/pom.xml b/pom.xml index 7ba2b62..2b15b3d 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - dev.avishay + dev.azamir java-backend-primitives 1.0.0-SNAPSHOT Java Backend Primitives diff --git a/src/jmh/java/dev/avishay/primitives/concurrent/BoundedBlockingQueueBenchmark.java b/src/jmh/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueBenchmark.java similarity index 100% rename from src/jmh/java/dev/avishay/primitives/concurrent/BoundedBlockingQueueBenchmark.java rename to src/jmh/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueBenchmark.java diff --git a/src/main/java/dev/avishay/primitives/batching/AdaptiveBatchSizer.java b/src/main/java/dev/azamir/primitives/batching/AdaptiveBatchSizer.java similarity index 100% rename from src/main/java/dev/avishay/primitives/batching/AdaptiveBatchSizer.java rename to src/main/java/dev/azamir/primitives/batching/AdaptiveBatchSizer.java diff --git a/src/main/java/dev/avishay/primitives/batching/CheckedSizeProbe.java b/src/main/java/dev/azamir/primitives/batching/CheckedSizeProbe.java similarity index 100% rename from src/main/java/dev/avishay/primitives/batching/CheckedSizeProbe.java rename to src/main/java/dev/azamir/primitives/batching/CheckedSizeProbe.java diff --git a/src/main/java/dev/avishay/primitives/batching/RetryingSizeProbe.java b/src/main/java/dev/azamir/primitives/batching/RetryingSizeProbe.java similarity index 100% rename from src/main/java/dev/avishay/primitives/batching/RetryingSizeProbe.java rename to src/main/java/dev/azamir/primitives/batching/RetryingSizeProbe.java diff --git a/src/main/java/dev/avishay/primitives/batching/SizeProbeException.java b/src/main/java/dev/azamir/primitives/batching/SizeProbeException.java similarity index 100% rename from src/main/java/dev/avishay/primitives/batching/SizeProbeException.java rename to src/main/java/dev/azamir/primitives/batching/SizeProbeException.java diff --git a/src/main/java/dev/avishay/primitives/concurrent/BoundedBlockingQueue.java b/src/main/java/dev/azamir/primitives/concurrent/BoundedBlockingQueue.java similarity index 100% rename from src/main/java/dev/avishay/primitives/concurrent/BoundedBlockingQueue.java rename to src/main/java/dev/azamir/primitives/concurrent/BoundedBlockingQueue.java diff --git a/src/main/java/dev/avishay/primitives/concurrent/CloseableBlockingQueue.java b/src/main/java/dev/azamir/primitives/concurrent/CloseableBlockingQueue.java similarity index 100% rename from src/main/java/dev/avishay/primitives/concurrent/CloseableBlockingQueue.java rename to src/main/java/dev/azamir/primitives/concurrent/CloseableBlockingQueue.java diff --git a/src/main/java/dev/avishay/primitives/concurrent/CloseableQueues.java b/src/main/java/dev/azamir/primitives/concurrent/CloseableQueues.java similarity index 100% rename from src/main/java/dev/avishay/primitives/concurrent/CloseableQueues.java rename to src/main/java/dev/azamir/primitives/concurrent/CloseableQueues.java diff --git a/src/main/java/dev/avishay/primitives/concurrent/PollingBackoffQueue.java b/src/main/java/dev/azamir/primitives/concurrent/PollingBackoffQueue.java similarity index 100% rename from src/main/java/dev/avishay/primitives/concurrent/PollingBackoffQueue.java rename to src/main/java/dev/azamir/primitives/concurrent/PollingBackoffQueue.java diff --git a/src/main/java/dev/avishay/primitives/concurrent/QueueClosedException.java b/src/main/java/dev/azamir/primitives/concurrent/QueueClosedException.java similarity index 100% rename from src/main/java/dev/avishay/primitives/concurrent/QueueClosedException.java rename to src/main/java/dev/azamir/primitives/concurrent/QueueClosedException.java diff --git a/src/main/java/dev/avishay/primitives/concurrent/QueueWaitStrategy.java b/src/main/java/dev/azamir/primitives/concurrent/QueueWaitStrategy.java similarity index 100% rename from src/main/java/dev/avishay/primitives/concurrent/QueueWaitStrategy.java rename to src/main/java/dev/azamir/primitives/concurrent/QueueWaitStrategy.java diff --git a/src/main/java/dev/avishay/primitives/concurrent/ResettableLongAccumulator.java b/src/main/java/dev/azamir/primitives/concurrent/ResettableLongAccumulator.java similarity index 100% rename from src/main/java/dev/avishay/primitives/concurrent/ResettableLongAccumulator.java rename to src/main/java/dev/azamir/primitives/concurrent/ResettableLongAccumulator.java diff --git a/src/main/java/dev/avishay/primitives/math/DecimalRatioCalculator.java b/src/main/java/dev/azamir/primitives/math/DecimalRatioCalculator.java similarity index 100% rename from src/main/java/dev/avishay/primitives/math/DecimalRatioCalculator.java rename to src/main/java/dev/azamir/primitives/math/DecimalRatioCalculator.java diff --git a/src/main/java/dev/avishay/primitives/statistics/StatisticsSnapshot.java b/src/main/java/dev/azamir/primitives/statistics/StatisticsSnapshot.java similarity index 100% rename from src/main/java/dev/avishay/primitives/statistics/StatisticsSnapshot.java rename to src/main/java/dev/azamir/primitives/statistics/StatisticsSnapshot.java diff --git a/src/main/java/dev/avishay/primitives/statistics/StreamingStatistics.java b/src/main/java/dev/azamir/primitives/statistics/StreamingStatistics.java similarity index 100% rename from src/main/java/dev/avishay/primitives/statistics/StreamingStatistics.java rename to src/main/java/dev/azamir/primitives/statistics/StreamingStatistics.java diff --git a/src/test/java/dev/avishay/primitives/batching/AdaptiveBatchSizerTest.java b/src/test/java/dev/azamir/primitives/batching/AdaptiveBatchSizerTest.java similarity index 100% rename from src/test/java/dev/avishay/primitives/batching/AdaptiveBatchSizerTest.java rename to src/test/java/dev/azamir/primitives/batching/AdaptiveBatchSizerTest.java diff --git a/src/test/java/dev/avishay/primitives/concurrent/BoundedBlockingQueueTest.java b/src/test/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueTest.java similarity index 100% rename from src/test/java/dev/avishay/primitives/concurrent/BoundedBlockingQueueTest.java rename to src/test/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueTest.java diff --git a/src/test/java/dev/avishay/primitives/concurrent/QueueStrategiesTest.java b/src/test/java/dev/azamir/primitives/concurrent/QueueStrategiesTest.java similarity index 100% rename from src/test/java/dev/avishay/primitives/concurrent/QueueStrategiesTest.java rename to src/test/java/dev/azamir/primitives/concurrent/QueueStrategiesTest.java diff --git a/src/test/java/dev/avishay/primitives/concurrent/ResettableLongAccumulatorTest.java b/src/test/java/dev/azamir/primitives/concurrent/ResettableLongAccumulatorTest.java similarity index 100% rename from src/test/java/dev/avishay/primitives/concurrent/ResettableLongAccumulatorTest.java rename to src/test/java/dev/azamir/primitives/concurrent/ResettableLongAccumulatorTest.java diff --git a/src/test/java/dev/avishay/primitives/math/DecimalRatioCalculatorTest.java b/src/test/java/dev/azamir/primitives/math/DecimalRatioCalculatorTest.java similarity index 100% rename from src/test/java/dev/avishay/primitives/math/DecimalRatioCalculatorTest.java rename to src/test/java/dev/azamir/primitives/math/DecimalRatioCalculatorTest.java diff --git a/src/test/java/dev/avishay/primitives/statistics/StreamingStatisticsTest.java b/src/test/java/dev/azamir/primitives/statistics/StreamingStatisticsTest.java similarity index 100% rename from src/test/java/dev/avishay/primitives/statistics/StreamingStatisticsTest.java rename to src/test/java/dev/azamir/primitives/statistics/StreamingStatisticsTest.java From 299900d4ff4c6a591acae6b2c55af19042200e88 Mon Sep 17 00:00:00 2001 From: Avishay Date: Mon, 20 Jul 2026 01:26:24 +0200 Subject: [PATCH 30/34] Add temporary package namespace fix --- .github/workflows/fix-package-namespace.yml | 52 +++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/fix-package-namespace.yml diff --git a/.github/workflows/fix-package-namespace.yml b/.github/workflows/fix-package-namespace.yml new file mode 100644 index 0000000..3c60c7b --- /dev/null +++ b/.github/workflows/fix-package-namespace.yml @@ -0,0 +1,52 @@ +name: Fix Java package namespace + +on: + push: + branches: + - agent/rebuild-java-concurrency-primitives + paths: + - .github/workflows/fix-package-namespace.yml + +permissions: + contents: write + +jobs: + fix: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Replace namespace in all source files + shell: bash + run: | + set -euo pipefail + + while IFS= read -r -d '' file; do + sed -i 's/dev\.avishay/dev.azamir/g' "$file" + done < <(find src -type f -name '*.java' -print0) + + sed -i 's/dev\.avishay/dev.azamir/g' pom.xml README.md + rm .github/workflows/fix-package-namespace.yml + + if grep -RIn --exclude-dir=.git 'dev\.avishay' .; then + echo 'Old namespace is still present' >&2 + exit 1 + fi + + grep -q '^package dev.azamir.primitives.concurrent;' \ + src/main/java/dev/azamir/primitives/concurrent/BoundedBlockingQueue.java + grep -q '^package dev.azamir.primitives.statistics;' \ + src/main/java/dev/azamir/primitives/statistics/StreamingStatistics.java + grep -q 'dev.azamir' pom.xml + + - name: Commit namespace correction + shell: bash + run: | + set -euo pipefail + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add -A + git commit -m 'Use dev.azamir in Java packages' + git push From 94ccdb9ecf0a153346859e0abf345fbd4afd0b52 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:26:31 +0000 Subject: [PATCH 31/34] Use dev.azamir in Java packages --- .github/workflows/fix-package-namespace.yml | 52 ------------------- .../BoundedBlockingQueueBenchmark.java | 2 +- .../batching/AdaptiveBatchSizer.java | 2 +- .../primitives/batching/CheckedSizeProbe.java | 2 +- .../batching/RetryingSizeProbe.java | 2 +- .../batching/SizeProbeException.java | 2 +- .../concurrent/BoundedBlockingQueue.java | 2 +- .../concurrent/CloseableBlockingQueue.java | 2 +- .../concurrent/CloseableQueues.java | 2 +- .../concurrent/PollingBackoffQueue.java | 2 +- .../concurrent/QueueClosedException.java | 2 +- .../concurrent/QueueWaitStrategy.java | 2 +- .../concurrent/ResettableLongAccumulator.java | 2 +- .../math/DecimalRatioCalculator.java | 2 +- .../statistics/StatisticsSnapshot.java | 2 +- .../statistics/StreamingStatistics.java | 2 +- .../batching/AdaptiveBatchSizerTest.java | 2 +- .../concurrent/BoundedBlockingQueueTest.java | 2 +- .../concurrent/QueueStrategiesTest.java | 2 +- .../ResettableLongAccumulatorTest.java | 2 +- .../math/DecimalRatioCalculatorTest.java | 2 +- .../statistics/StreamingStatisticsTest.java | 2 +- 22 files changed, 21 insertions(+), 73 deletions(-) delete mode 100644 .github/workflows/fix-package-namespace.yml diff --git a/.github/workflows/fix-package-namespace.yml b/.github/workflows/fix-package-namespace.yml deleted file mode 100644 index 3c60c7b..0000000 --- a/.github/workflows/fix-package-namespace.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Fix Java package namespace - -on: - push: - branches: - - agent/rebuild-java-concurrency-primitives - paths: - - .github/workflows/fix-package-namespace.yml - -permissions: - contents: write - -jobs: - fix: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Replace namespace in all source files - shell: bash - run: | - set -euo pipefail - - while IFS= read -r -d '' file; do - sed -i 's/dev\.avishay/dev.azamir/g' "$file" - done < <(find src -type f -name '*.java' -print0) - - sed -i 's/dev\.avishay/dev.azamir/g' pom.xml README.md - rm .github/workflows/fix-package-namespace.yml - - if grep -RIn --exclude-dir=.git 'dev\.avishay' .; then - echo 'Old namespace is still present' >&2 - exit 1 - fi - - grep -q '^package dev.azamir.primitives.concurrent;' \ - src/main/java/dev/azamir/primitives/concurrent/BoundedBlockingQueue.java - grep -q '^package dev.azamir.primitives.statistics;' \ - src/main/java/dev/azamir/primitives/statistics/StreamingStatistics.java - grep -q 'dev.azamir' pom.xml - - - name: Commit namespace correction - shell: bash - run: | - set -euo pipefail - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add -A - git commit -m 'Use dev.azamir in Java packages' - git push diff --git a/src/jmh/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueBenchmark.java b/src/jmh/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueBenchmark.java index 071a018..c54c342 100644 --- a/src/jmh/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueBenchmark.java +++ b/src/jmh/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueBenchmark.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.concurrent; +package dev.azamir.primitives.concurrent; import java.time.Duration; import java.util.concurrent.TimeUnit; diff --git a/src/main/java/dev/azamir/primitives/batching/AdaptiveBatchSizer.java b/src/main/java/dev/azamir/primitives/batching/AdaptiveBatchSizer.java index 3d8ab45..fc52003 100644 --- a/src/main/java/dev/azamir/primitives/batching/AdaptiveBatchSizer.java +++ b/src/main/java/dev/azamir/primitives/batching/AdaptiveBatchSizer.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.batching; +package dev.azamir.primitives.batching; import java.util.Objects; diff --git a/src/main/java/dev/azamir/primitives/batching/CheckedSizeProbe.java b/src/main/java/dev/azamir/primitives/batching/CheckedSizeProbe.java index 1e0ecf0..5c326eb 100644 --- a/src/main/java/dev/azamir/primitives/batching/CheckedSizeProbe.java +++ b/src/main/java/dev/azamir/primitives/batching/CheckedSizeProbe.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.batching; +package dev.azamir.primitives.batching; /** Measures a serialized size and may fail with a checked exception. */ @FunctionalInterface diff --git a/src/main/java/dev/azamir/primitives/batching/RetryingSizeProbe.java b/src/main/java/dev/azamir/primitives/batching/RetryingSizeProbe.java index a630222..523930d 100644 --- a/src/main/java/dev/azamir/primitives/batching/RetryingSizeProbe.java +++ b/src/main/java/dev/azamir/primitives/batching/RetryingSizeProbe.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.batching; +package dev.azamir.primitives.batching; import java.util.Objects; diff --git a/src/main/java/dev/azamir/primitives/batching/SizeProbeException.java b/src/main/java/dev/azamir/primitives/batching/SizeProbeException.java index fc9a0e1..163f609 100644 --- a/src/main/java/dev/azamir/primitives/batching/SizeProbeException.java +++ b/src/main/java/dev/azamir/primitives/batching/SizeProbeException.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.batching; +package dev.azamir.primitives.batching; /** Raised when a serialized size cannot be measured within the configured retry budget. */ public final class SizeProbeException extends RuntimeException { diff --git a/src/main/java/dev/azamir/primitives/concurrent/BoundedBlockingQueue.java b/src/main/java/dev/azamir/primitives/concurrent/BoundedBlockingQueue.java index 8d3c39d..c9c09f0 100644 --- a/src/main/java/dev/azamir/primitives/concurrent/BoundedBlockingQueue.java +++ b/src/main/java/dev/azamir/primitives/concurrent/BoundedBlockingQueue.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.concurrent; +package dev.azamir.primitives.concurrent; import java.time.Duration; import java.util.ArrayList; diff --git a/src/main/java/dev/azamir/primitives/concurrent/CloseableBlockingQueue.java b/src/main/java/dev/azamir/primitives/concurrent/CloseableBlockingQueue.java index 3a5faa7..bff10c8 100644 --- a/src/main/java/dev/azamir/primitives/concurrent/CloseableBlockingQueue.java +++ b/src/main/java/dev/azamir/primitives/concurrent/CloseableBlockingQueue.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.concurrent; +package dev.azamir.primitives.concurrent; import java.time.Duration; import java.util.Collection; diff --git a/src/main/java/dev/azamir/primitives/concurrent/CloseableQueues.java b/src/main/java/dev/azamir/primitives/concurrent/CloseableQueues.java index e05750e..7f2e9ac 100644 --- a/src/main/java/dev/azamir/primitives/concurrent/CloseableQueues.java +++ b/src/main/java/dev/azamir/primitives/concurrent/CloseableQueues.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.concurrent; +package dev.azamir.primitives.concurrent; import java.time.Duration; import java.util.Objects; diff --git a/src/main/java/dev/azamir/primitives/concurrent/PollingBackoffQueue.java b/src/main/java/dev/azamir/primitives/concurrent/PollingBackoffQueue.java index a27f989..d2a0c6d 100644 --- a/src/main/java/dev/azamir/primitives/concurrent/PollingBackoffQueue.java +++ b/src/main/java/dev/azamir/primitives/concurrent/PollingBackoffQueue.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.concurrent; +package dev.azamir.primitives.concurrent; import java.time.Duration; import java.util.ArrayList; diff --git a/src/main/java/dev/azamir/primitives/concurrent/QueueClosedException.java b/src/main/java/dev/azamir/primitives/concurrent/QueueClosedException.java index 4290345..ec36c78 100644 --- a/src/main/java/dev/azamir/primitives/concurrent/QueueClosedException.java +++ b/src/main/java/dev/azamir/primitives/concurrent/QueueClosedException.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.concurrent; +package dev.azamir.primitives.concurrent; /** Signals that an operation cannot continue because a queue has been closed. */ public final class QueueClosedException extends IllegalStateException { diff --git a/src/main/java/dev/azamir/primitives/concurrent/QueueWaitStrategy.java b/src/main/java/dev/azamir/primitives/concurrent/QueueWaitStrategy.java index 6a52022..9a642e5 100644 --- a/src/main/java/dev/azamir/primitives/concurrent/QueueWaitStrategy.java +++ b/src/main/java/dev/azamir/primitives/concurrent/QueueWaitStrategy.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.concurrent; +package dev.azamir.primitives.concurrent; /** Selects how a bounded queue waits for capacity or data. */ public enum QueueWaitStrategy { diff --git a/src/main/java/dev/azamir/primitives/concurrent/ResettableLongAccumulator.java b/src/main/java/dev/azamir/primitives/concurrent/ResettableLongAccumulator.java index 95ece23..414a8ac 100644 --- a/src/main/java/dev/azamir/primitives/concurrent/ResettableLongAccumulator.java +++ b/src/main/java/dev/azamir/primitives/concurrent/ResettableLongAccumulator.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.concurrent; +package dev.azamir.primitives.concurrent; import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; diff --git a/src/main/java/dev/azamir/primitives/math/DecimalRatioCalculator.java b/src/main/java/dev/azamir/primitives/math/DecimalRatioCalculator.java index a745714..b1a40db 100644 --- a/src/main/java/dev/azamir/primitives/math/DecimalRatioCalculator.java +++ b/src/main/java/dev/azamir/primitives/math/DecimalRatioCalculator.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.math; +package dev.azamir.primitives.math; import java.math.BigDecimal; import java.math.RoundingMode; diff --git a/src/main/java/dev/azamir/primitives/statistics/StatisticsSnapshot.java b/src/main/java/dev/azamir/primitives/statistics/StatisticsSnapshot.java index 055344b..d1d2ff9 100644 --- a/src/main/java/dev/azamir/primitives/statistics/StatisticsSnapshot.java +++ b/src/main/java/dev/azamir/primitives/statistics/StatisticsSnapshot.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.statistics; +package dev.azamir.primitives.statistics; import java.util.OptionalDouble; diff --git a/src/main/java/dev/azamir/primitives/statistics/StreamingStatistics.java b/src/main/java/dev/azamir/primitives/statistics/StreamingStatistics.java index b3ab7d3..d9cc82f 100644 --- a/src/main/java/dev/azamir/primitives/statistics/StreamingStatistics.java +++ b/src/main/java/dev/azamir/primitives/statistics/StreamingStatistics.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.statistics; +package dev.azamir.primitives.statistics; import java.util.OptionalDouble; diff --git a/src/test/java/dev/azamir/primitives/batching/AdaptiveBatchSizerTest.java b/src/test/java/dev/azamir/primitives/batching/AdaptiveBatchSizerTest.java index d80e1b1..5f9afa9 100644 --- a/src/test/java/dev/azamir/primitives/batching/AdaptiveBatchSizerTest.java +++ b/src/test/java/dev/azamir/primitives/batching/AdaptiveBatchSizerTest.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.batching; +package dev.azamir.primitives.batching; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/src/test/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueTest.java b/src/test/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueTest.java index 611be9a..91e8ec5 100644 --- a/src/test/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueTest.java +++ b/src/test/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueTest.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.concurrent; +package dev.azamir.primitives.concurrent; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/test/java/dev/azamir/primitives/concurrent/QueueStrategiesTest.java b/src/test/java/dev/azamir/primitives/concurrent/QueueStrategiesTest.java index 2013557..4c697f6 100644 --- a/src/test/java/dev/azamir/primitives/concurrent/QueueStrategiesTest.java +++ b/src/test/java/dev/azamir/primitives/concurrent/QueueStrategiesTest.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.concurrent; +package dev.azamir.primitives.concurrent; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/test/java/dev/azamir/primitives/concurrent/ResettableLongAccumulatorTest.java b/src/test/java/dev/azamir/primitives/concurrent/ResettableLongAccumulatorTest.java index bbde4b3..b4182c4 100644 --- a/src/test/java/dev/azamir/primitives/concurrent/ResettableLongAccumulatorTest.java +++ b/src/test/java/dev/azamir/primitives/concurrent/ResettableLongAccumulatorTest.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.concurrent; +package dev.azamir.primitives.concurrent; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/test/java/dev/azamir/primitives/math/DecimalRatioCalculatorTest.java b/src/test/java/dev/azamir/primitives/math/DecimalRatioCalculatorTest.java index 7a2f101..5eb3a26 100644 --- a/src/test/java/dev/azamir/primitives/math/DecimalRatioCalculatorTest.java +++ b/src/test/java/dev/azamir/primitives/math/DecimalRatioCalculatorTest.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.math; +package dev.azamir.primitives.math; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/src/test/java/dev/azamir/primitives/statistics/StreamingStatisticsTest.java b/src/test/java/dev/azamir/primitives/statistics/StreamingStatisticsTest.java index 9d3f987..0bc919e 100644 --- a/src/test/java/dev/azamir/primitives/statistics/StreamingStatisticsTest.java +++ b/src/test/java/dev/azamir/primitives/statistics/StreamingStatisticsTest.java @@ -1,4 +1,4 @@ -package dev.avishay.primitives.statistics; +package dev.azamir.primitives.statistics; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; From 22064777fae931961412f83cf1702894735d99d4 Mon Sep 17 00:00:00 2001 From: Avishay Date: Mon, 20 Jul 2026 10:22:36 +0200 Subject: [PATCH 32/34] Document public library usage --- README.md | 183 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 127 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index d929a82..1d8145a 100644 --- a/README.md +++ b/README.md @@ -6,75 +6,77 @@ A dependency-free Java 21 library of small backend building blocks for concurren The project favors explicit contracts, deterministic behavior, and testable lifecycle rules over framework-specific integrations. -## Components +## Status -### Concurrent queues +This project is currently a pre-release library. The source code is public and licensed under MIT, but the artifact is not yet published to Maven Central or another public package registry. -- `CloseableBlockingQueue` defines bounded FIFO, timed operations, draining, search, snapshots, clearing, and explicit close semantics. -- `BoundedBlockingQueue` uses `ReentrantLock` and `Condition` signaling for efficient producer-consumer coordination. -- `PollingBackoffQueue` provides the same public contract with configurable polling backoff. -- `QueueWaitStrategy` and `CloseableQueues` select the desired wait model. -- `QueueClosedException` distinguishes normal closed-and-drained lifecycle completion from interruption. +To use it today, clone the repository and install the artifact into your local Maven repository. The current coordinates are: -### Streaming statistics +```text +dev.azamir:java-backend-primitives:1.0.0-SNAPSHOT +``` -- `StreamingStatistics` calculates count, sum, mean, sample variance, sample standard deviation, minimum, and maximum in one pass. -- Welford's algorithm provides stable online variance. -- Compensated summation reduces floating-point error. -- All updates, snapshots, and resets are thread-safe. +## Requirements -### Decimal ratios +- Java 21 or later +- Maven 3.9 or later -- `DecimalRatioCalculator` calculates ratios with explicit scale and `RoundingMode`. -- An optional additive adjustment supports calibrated ratio calculations. -- `BigDecimal.valueOf` is used by double convenience methods to avoid direct binary floating-point construction artifacts. +## Install locally -### Adaptive batching +```bash +git clone https://github.com/azamir911/java-backend-primitives.git +cd java-backend-primitives +mvn clean install +``` -- `AdaptiveBatchSizer` estimates how many records fit in a target serialized capacity. -- It separates fixed overhead from per-record payload, applies a safety factor, and enforces minimum and maximum batch sizes. -- `RetryingSizeProbe` retries transient serialization-size failures within a fixed attempt budget. -- `CheckedSizeProbe` keeps the sizing algorithm independent from any serializer or framework. +This installs the library into your local Maven repository, normally under `~/.m2/repository`. -### Additional utility +### Maven -- `ResettableLongAccumulator` provides lock-free associative accumulation with atomic snapshot-and-reset behavior. +After running `mvn clean install`, add: -## Replacement map +```xml + + dev.azamir + java-backend-primitives + 1.0.0-SNAPSHOT + +``` -The repository was rebuilt as an independent project. The previous source files are removed, but every meaningful capability now has a clear replacement: +### Gradle -| Previous responsibility | Independent replacement | Notes | -|---|---|---| -| Shared FIFO base queue | `CloseableBlockingQueue` | Common lifecycle and collection contract | -| Lock and condition queue | `BoundedBlockingQueue` | Efficient signaling, timed operations, close and drain | -| Sleeping or polling queue | `PollingBackoffQueue` | Configurable polling interval with the same queue contract | -| Queue mode selection | `QueueWaitStrategy` and `CloseableQueues` | Explicit factory-based strategy selection | -| Released queue exception | `QueueClosedException` | Clear closed-for-write and closed-and-drained semantics | -| Interrupted queue exception | Standard `InterruptedException` | Preserves Java interruption semantics | -| Statistical accumulator | `StreamingStatistics` | Count, sum, mean, variance, deviation, min, max, reset | -| Ratio calculation | `DecimalRatioCalculator` | Factor, scale, rounding mode, decimal precision | -| Chunk-size estimation | `AdaptiveBatchSizer` | General serialized batch-size projection | -| Serialization retries | `RetryingSizeProbe` | Framework-independent retry budget | - -No previous package names, internal identifiers, comments, framework types, or source implementations are reused. +Use the local Maven repository: -## Requirements +```kotlin +repositories { + mavenLocal() +} -- Java 21 or later -- Maven 3.9 or later +dependencies { + implementation("dev.azamir:java-backend-primitives:1.0.0-SNAPSHOT") +} +``` -## Build and test +## Components -```bash -mvn verify -``` +| Area | Main types | Purpose | +|---|---|---| +| Concurrent queues | `CloseableBlockingQueue`, `BoundedBlockingQueue`, `PollingBackoffQueue` | Bounded FIFO coordination with explicit shutdown behavior | +| Streaming statistics | `StreamingStatistics`, `StatisticsSnapshot` | One-pass descriptive statistics with thread-safe snapshots | +| Decimal ratios | `DecimalRatioCalculator` | Ratio calculations with explicit scale and rounding | +| Adaptive batching | `AdaptiveBatchSizer`, `RetryingSizeProbe` | Estimate safe batch sizes from serialized samples | +| Atomic accumulation | `ResettableLongAccumulator` | Lock-free accumulation with atomic snapshot and reset | -The verification phase compiles the library, runs deterministic unit tests and multi-threaded stress tests, and creates a JaCoCo report under `target/site/jacoco`. +## Quick start -## Queue example +### Bounded producer-consumer queue ```java +import dev.azamir.primitives.concurrent.CloseableBlockingQueue; +import dev.azamir.primitives.concurrent.CloseableQueues; +import dev.azamir.primitives.concurrent.QueueClosedException; +import dev.azamir.primitives.concurrent.QueueWaitStrategy; + CloseableBlockingQueue queue = CloseableQueues.bounded(128, QueueWaitStrategy.CONDITION_SIGNALING); @@ -94,12 +96,14 @@ try { System.out.println(queue.take()); } } catch (QueueClosedException drained) { - // Closed and fully drained. + // The queue was closed and all accepted elements were consumed. } producer.join(); ``` +Choose `QueueWaitStrategy.POLLING_BACKOFF` when polling behavior is intentionally required. The default condition-signaling implementation is more efficient for normal producer-consumer workloads. + ### Queue lifecycle | State | `put` / `offer` | `take` | `poll` | @@ -109,24 +113,40 @@ producer.join(); | Closed with buffered data | Throws `QueueClosedException` | Drains buffered data | Drains buffered data | | Closed and drained | Throws `QueueClosedException` | Throws `QueueClosedException` | Returns empty | -`close()` is idempotent and does not discard accepted elements. `clear()` removes buffered elements without closing the queue. +`close()` is idempotent and preserves buffered elements for draining. `closeNow()` closes the queue and discards buffered elements. `clear()` removes buffered elements without closing the queue. -## Streaming statistics example +All blocking methods propagate `InterruptedException` and preserve standard Java interruption semantics. + +### Streaming statistics ```java +import dev.azamir.primitives.statistics.StatisticsSnapshot; +import dev.azamir.primitives.statistics.StreamingStatistics; + StreamingStatistics statistics = new StreamingStatistics(); statistics.add(12.5); statistics.add(18.0); statistics.add(14.5); StatisticsSnapshot snapshot = statistics.snapshot(); + +long count = snapshot.count(); +double sum = snapshot.sum(); double mean = snapshot.mean().orElseThrow(); -double sampleDeviation = snapshot.sampleStandardDeviation().orElseThrow(); +double deviation = snapshot.sampleStandardDeviation().orElseThrow(); ``` -## Ratio example +`StreamingStatistics` calculates count, sum, mean, sample variance, sample standard deviation, minimum, and maximum in one pass. It uses Welford's algorithm for stable online variance and compensated summation to reduce floating-point error. + +Updates, snapshots, and resets are thread-safe. + +### Decimal ratio calculation ```java +import dev.azamir.primitives.math.DecimalRatioCalculator; +import java.math.BigDecimal; +import java.math.RoundingMode; + DecimalRatioCalculator ratio = new DecimalRatioCalculator(new BigDecimal("0.10")); @@ -137,14 +157,28 @@ BigDecimal result = ratio.calculate( RoundingMode.HALF_UP); ``` -## Adaptive batch sizing example +The constructor argument is an optional additive adjustment applied after division. Use `BigDecimal.ZERO` for a plain ratio. + +The calculator validates zero denominators and invalid scales rather than relying on implicit arithmetic failures. + +### Adaptive batch sizing ```java +import dev.azamir.primitives.batching.AdaptiveBatchSizer; +import dev.azamir.primitives.batching.RetryingSizeProbe; + +byte[] serializedSample = loadSerializedSample(); +int headerBytes = 256; + RetryingSizeProbe probe = new RetryingSizeProbe<>(bytes -> bytes.length, 3); AdaptiveBatchSizer sizer = - new AdaptiveBatchSizer(1_048_576, 0.85, 1, 100_000); + new AdaptiveBatchSizer( + 1_048_576, // target serialized capacity + 0.85, // safety factor + 1, // minimum batch size + 100_000); // maximum batch size int recordsPerBatch = sizer.estimate( 1_000, @@ -153,6 +187,37 @@ int recordsPerBatch = sizer.estimate( headerBytes); ``` +The estimator separates fixed overhead from per-record payload, applies a safety factor, and clamps the result to configured minimum and maximum batch sizes. + +`RetryingSizeProbe` allows transient measurement or serialization failures to be retried within a fixed attempt budget while keeping the sizing algorithm independent from any serializer or framework. + +### Resettable atomic accumulator + +```java +import dev.azamir.primitives.concurrent.ResettableLongAccumulator; + +ResettableLongAccumulator bytes = ResettableLongAccumulator.sum(); +bytes.accumulate(1_024); +bytes.accumulate(2_048); + +long intervalTotal = bytes.getAndReset(); // 3072 +``` + +Custom associative operations are also supported. The supplied operation must be side-effect free because it may be retried during concurrent updates. + +## Build and test + +```bash +mvn verify +``` + +The verification phase: + +- compiles the library with Java 21 +- runs unit tests +- runs multi-threaded stress tests +- creates a JaCoCo report under `target/site/jacoco` + ## Benchmarks Build and run the JMH queue benchmark: @@ -162,7 +227,13 @@ mvn -Pbenchmarks -DskipTests package java -jar target/benchmarks.jar ``` -Benchmark results depend on the machine, JVM, queue capacity, wait strategy, and producer-consumer ratio. The repository includes a reproducible harness rather than publishing a hardware-independent performance claim. +The benchmark compares condition signaling and polling backoff. Results depend on the machine, JVM, queue capacity, wait strategy, and producer-consumer ratio. The repository includes a reproducible harness rather than publishing a hardware-independent performance claim. + +## Versioning and compatibility + +The current `1.0.0-SNAPSHOT` version is a development build. Public APIs may change before the first tagged stable release. + +A stable release should use a non-SNAPSHOT version and be published to a public artifact registry before consumers rely on it as a remote dependency. ## License From 1611a7d7709a6904be4fe7937ffb32c4a23b8b6a Mon Sep 17 00:00:00 2001 From: Avishay Date: Mon, 20 Jul 2026 10:24:27 +0200 Subject: [PATCH 33/34] Make usage examples self-contained --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1d8145a..8b70c8b 100644 --- a/README.md +++ b/README.md @@ -166,9 +166,12 @@ The calculator validates zero denominators and invalid scales rather than relyin ```java import dev.azamir.primitives.batching.AdaptiveBatchSizer; import dev.azamir.primitives.batching.RetryingSizeProbe; +import java.nio.charset.StandardCharsets; -byte[] serializedSample = loadSerializedSample(); -int headerBytes = 256; +byte[] serializedSample = + "record-1,record-2".getBytes(StandardCharsets.UTF_8); +int sampledRecords = 2; +int headerBytes = 4; RetryingSizeProbe probe = new RetryingSizeProbe<>(bytes -> bytes.length, 3); @@ -181,7 +184,7 @@ AdaptiveBatchSizer sizer = 100_000); // maximum batch size int recordsPerBatch = sizer.estimate( - 1_000, + sampledRecords, serializedSample, probe, headerBytes); From a985a1ed619ed866fe7c70cbbaf9f02872756089 Mon Sep 17 00:00:00 2001 From: Avishay Date: Mon, 20 Jul 2026 10:31:12 +0200 Subject: [PATCH 34/34] Bound benchmark queue operations --- .../concurrent/BoundedBlockingQueueBenchmark.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/jmh/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueBenchmark.java b/src/jmh/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueBenchmark.java index c54c342..986002c 100644 --- a/src/jmh/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueBenchmark.java +++ b/src/jmh/java/dev/azamir/primitives/concurrent/BoundedBlockingQueueBenchmark.java @@ -24,6 +24,8 @@ @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) @Fork(1) public class BoundedBlockingQueueBenchmark { + private static final Duration OPERATION_TIMEOUT = Duration.ofMillis(100); + @State(Scope.Group) public static class QueueState { @Param({"1024"}) @@ -48,14 +50,14 @@ public void tearDown() { @Benchmark @Group("producerConsumer") @GroupThreads(1) - public void put(QueueState state) throws InterruptedException { - state.queue.put(1); + public boolean offer(QueueState state) throws InterruptedException { + return state.queue.offer(1, OPERATION_TIMEOUT); } @Benchmark @Group("producerConsumer") @GroupThreads(1) - public int take(QueueState state) throws InterruptedException { - return state.queue.take(); + public int poll(QueueState state) throws InterruptedException { + return state.queue.poll(OPERATION_TIMEOUT).orElse(0); } }