Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this

## [Unreleased]

### Changed

- **Recycled read dispatch** — Disabled the low-concurrency fast-recycle bypass introduced in v5.7.0 because directly resubmitting the same in-flight operations could restrict a duration-based read to a working set equal to concurrency. Completed operations now return through shared generator circulation, trading some low-concurrency dispatch efficiency for complete manifest coverage.

### Fixed

- **Recycled read working-set coverage** — Low-concurrency duration reads now circulate through the complete input manifest before repeating objects.

## [5.14.1] - 2026-08-11

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,23 @@ default void buildItemPath(final I item, final String itemPath) {
void resetTiming();

/**
* Returns {@code true} if the storage driver has already recycled this operation
* via the fast-recycle path (re-submitted directly without going through the
* LoadGenerator recycle queue). When set, {@code LoadStepContextImpl} must
* skip calling {@code generator.recycle()} to avoid double-recycling.
* Legacy compatibility accessor for the removed direct fast-recycle path.
*
* @deprecated always returns {@code false}
*/
boolean driverRecycled();
@Deprecated
default boolean driverRecycled() {
return false;
}

void driverRecycled(boolean flag);
/**
* Legacy compatibility mutator for the removed direct fast-recycle path.
*
* @param flag ignored
* @deprecated always a no-op
*/
@Deprecated
default void driverRecycled(final boolean flag) {}

/**
* Number of times this operation has been retried after a failure via {@code
Expand All @@ -136,7 +145,7 @@ default void incrementOpRetryCount() {}

/**
* Reset the whole-operation retry counter, e.g. after a terminal success, so a
* recycled/fast-recycled operation starts its next logical attempt with a clean budget
* recycled operation starts its next logical attempt with a clean budget
* instead of accumulating retry counts across unrelated cycles.
*/
default void resetOpRetryCount() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
protected volatile long reqTimeDone;
protected volatile long respTimeStart;
protected volatile long respTimeDone;
/**
* @deprecated retained for binary compatibility only; SPT never reads this field and writes
* have no effect
*/
@Deprecated
protected volatile boolean driverRecycled;
protected volatile int opRetryCount;
protected volatile String requestedVersionId;
Expand Down Expand Up @@ -95,7 +100,6 @@
this.reqTimeDone = other.reqTimeDone;
this.respTimeStart = other.respTimeStart;
this.respTimeDone = other.respTimeDone;
this.driverRecycled = other.driverRecycled;
// Deliberately propagated (not reset in reset() below): this must survive across
// the reset()+redispatch cycle a retried operation goes through, or the retry
// counter added for load-op-retry could never reach its limit.
Expand Down Expand Up @@ -127,17 +131,6 @@
returnedVersionId = null;
responseRequestId = null;
integrityVerificationResult = null;
driverRecycled = false;
}

@Override
public final boolean driverRecycled() {
return driverRecycled;
}

@Override
public final void driverRecycled(final boolean flag) {
this.driverRecycled = flag;
}

@Override
Expand All @@ -147,7 +140,7 @@

@Override
public final void incrementOpRetryCount() {
opRetryCount++;

Check warning on line 143 in engine/core/spt-base/src/main/java/com/dell/spt/base/item/op/OperationImpl.java

View workflow job for this annotation

GitHub Actions / Engine Build & Tests

[NonAtomicVolatileUpdate] This update of a volatile variable is non-atomic
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,11 @@ default List<O> drainPendingRetries() {
}

/**
* Enable idle-task quiescing for fast-recycle workloads. When enabled, the
* generator parks its task thread (instead of spin-waiting/yielding) when the recycle
* queue is empty, because the driver is handling recycling inline. The
* {@link #recycle} method will unpark the task immediately if an op falls back
* to the normal path.
* Legacy compatibility hook for the removed direct fast-recycle path.
*
* @deprecated always a no-op; completed operations must return through {@link #recycle}
*/
@Deprecated
default void enableFastRecycleQuiesce() {}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.LongAdder;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.ThreadContext;
Expand Down Expand Up @@ -71,9 +70,6 @@ public class LoadGeneratorImpl<I extends Item, O extends Operation<I>> extends T
private final boolean shuffleFlag;
private final Random rnd;
private final String name;
private volatile boolean fastRecycleQuiesce = false;
private volatile boolean generatorParked = false;
private volatile Thread generatorThread;
private final ThreadLocal<CircularBuffer<O>> threadLocalOpBuff;
private final LongAdder builtTasksCounter = new LongAdder();
private final LongAdder recycledOpCounter = new LongAdder();
Expand Down Expand Up @@ -144,7 +140,6 @@ public LoadGeneratorImpl(
@Override
protected void doInit() {
ThreadContext.put(KEY_CLASS_NAME, CLS_NAME);
generatorThread = Thread.currentThread();
}

@Override
Expand Down Expand Up @@ -194,20 +189,9 @@ protected final void doWork() throws Exception {
pendingOpCount += n;
recycledOpCounter.add(n);
} else {
// No recycled ops available right now.
if (fastRecycleQuiesce) {
// Fast-recycle is handling ops inline in the driver;
// park this task thread until recycle() unparks us or the
// timeout expires. 10ms keeps it idle
// while still bounding wake-up latency for fallback ops.
generatorParked = true;
LockSupport.parkNanos(10_000_000);
generatorParked = false;
} else {
// Yield the task thread so in-flight ops can complete
// without a timed parking syscall.
yieldThread();
}
// Yield so in-flight operations can complete and return through
// recycleQueue without imposing a timed parking syscall.
yieldThread();
}
}
} else {
Expand Down Expand Up @@ -453,21 +437,11 @@ public final void recycle(final O op) {
recycleQueueFullState = true;
Loggers.ERR.warn("{}: recycle queue exceeded configured capacity ({})", name, recycleQueueCapacity);
}
// Wake the generator task if it's actually parked in the quiesce state.
// Checking generatorParked avoids spurious unpark() calls when the
// generator is running (e.g. at higher concurrency where fast-recycle
// doesn't handle all ops).
if (fastRecycleQuiesce && generatorParked) {
LockSupport.unpark(generatorThread);
}
}

@Override
public final void retry(final O op) {
retryQueue.add(op);
if (fastRecycleQuiesce && generatorParked) {
LockSupport.unpark(generatorThread);
}
}

/**
Expand Down Expand Up @@ -563,12 +537,6 @@ public final List<O> drainPendingRetries() {
return drained;
}

@Override
public void enableFastRecycleQuiesce() {
fastRecycleQuiesce = true;
Loggers.MSG.info("{}: fast-recycle quiesce enabled (generator task will park when idle)", name);
}

private boolean isFinished() {
// Never actually finish while a load-op-retry redispatch is still waiting to be
// drained - otherwise stop() below would tear this generator down before
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,25 +281,6 @@ public LoadStepContextImpl(
com.dell.spt.base.metrics.MetricsConstants.METADATA_LIST_SHARD_METRICS,
this.listShardMetricsRecorder);
}
// Enable fast-recycle on the driver when recycling simple data ops
// without content updates. The threshold is the configured concurrency
// limit (or a cap of 8 for unlimited). updateContents is excluded
// because the driver re-submits the original op directly and cannot
// safely mutate the shared DataItem offset concurrently with metrics.
if (this.recycleFlag && !this.updateContents && !this.listPathWorkload) {
final int driverConcurrency = this.driver.concurrencyLimit();
final int threshold = driverConcurrency > 0 ? Math.min(driverConcurrency, 8) : 8;
this.driver.enableFastRecycle(threshold);
// Only quiesce when the configured concurrency is low enough that
// fast-recycle handles most operations inline. At higher concurrency
// (T8+), fast-recycle rarely fires and the generator/dispatch VTs
// need to stay responsive — yield is faster than park+unpark when
// ops flow through the recycleQueue continuously.
if (driverConcurrency > 0 && driverConcurrency <= 4) {
this.generator.enableFastRecycleQuiesce();
this.driver.enableFastRecycleQuiesce();
}
}
}

/** Resolve the MetricsContext for a given operation type (mixed-mode routing). */
Expand Down Expand Up @@ -522,9 +503,9 @@ public final boolean put(final O opResult) {
final Status status = opResult.status();
if (Status.SUCC.equals(status)) {
// A terminal success ends this operation's current retry episode - clear the
// counter now so a recycled/fast-recycled reuse of this object (read-recycle
// mode, or Netty's fast-recycle short-circuit) starts its next attempt with a
// clean budget instead of accumulating retry counts across unrelated cycles.
// counter now so a recycled reuse of this object (read-recycle mode) starts its
// next attempt with a clean budget instead of accumulating retry counts across
// unrelated cycles.
opResult.resetOpRetryCount();
final long reqDuration = opResult.duration();
final long respLatency = opResult.latency();
Expand Down Expand Up @@ -572,12 +553,7 @@ public final boolean put(final O opResult) {
// TODO: possible change: remove dataItem.offset() to improve perf and increase variability
dataItem.offset(dataItem.offset() + rand.get().nextLong());
}
// Skip generator.recycle() when the driver already re-submitted the
// original op via the fast-recycle path — calling recycle here would
// create a duplicate in-flight operation.
if (!opResult.driverRecycled()) {
generator.recycle(opResult);
}
generator.recycle(opResult);
}

// each recycled op's lat and dur should be written to file each time
Expand Down Expand Up @@ -694,9 +670,7 @@ public final int put(final List<O> opResults, final int from, final int to) {
listShardMetricsRecorder.onRequeue(shardRef);
}
}
if (!opResult.driverRecycled()) {
generator.recycle(opResult);
}
generator.recycle(opResult);
}

// each recycled op's lat and dur should be written to file each time
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,29 +88,22 @@ default IntegrityTerminalException terminalFailure() {
void adjustIoBuffers(final long avgTransferSize, final OpType opType);

/**
* Enable the fast-recycle dispatch path. When active and the current
* active-op count is at or below {@code concurrencyThreshold}, the driver
* will re-submit a successfully completed simple operation directly on the
* I/O thread instead of returning it through the LoadGenerator recycle queue.
* <p>
* The default implementation is a no-op; subclasses that support the
* optimisation (e.g. Netty-based drivers) override this.
* Legacy compatibility hook. Direct driver resubmission bypasses the shared
* generator and can prevent undispatched input items from entering circulation,
* so this method no longer enables an optimization.
*
* @param concurrencyThreshold maximum active-op count at which the
* fast-recycle path is used (0 = disabled)
* @param concurrencyThreshold ignored
* @deprecated always a no-op; completed operations must return through the load generator
*/
@Deprecated
default void enableFastRecycle(final int concurrencyThreshold) {}

/**
* Signal that the fast-recycle path is expected to handle most operations
* and the dispatch/generator VTs may quiesce (park on long waits). Only
* called when the configured concurrency is low enough that fast-recycle
* dominates; at higher concurrency the normal pipeline needs to stay
* responsive.
* <p>
* Default is a no-op; cooperative drivers override to inform their
* dispatch task.
* Legacy compatibility hook for the removed direct fast-recycle path.
*
* @deprecated always a no-op; generator and dispatch tasks retain their normal lifecycle
*/
@Deprecated
default void enableFastRecycleQuiesce() {}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,36 +142,4 @@ void latencyNeverReturnsEpochTimestamp() {
"latency must not be epoch-scale in normal lifecycle");
}

// ---------- driverRecycled flag tests ----------

@Test
void driverRecycled_defaultIsFalse() {
final var op = new OperationImpl<>(0, OpType.READ, new ItemImpl("item"), null, null, null);
assertFalse(op.driverRecycled(), "driverRecycled should default to false");
}

@Test
void driverRecycled_setAndGet() {
final var op = new OperationImpl<>(0, OpType.READ, new ItemImpl("item"), null, null, null);
op.driverRecycled(true);
assertTrue(op.driverRecycled(), "driverRecycled should be true after setting");
op.driverRecycled(false);
assertFalse(op.driverRecycled(), "driverRecycled should be false after clearing");
}

@Test
void driverRecycled_copiedByResult() {
final var op = new OperationImpl<>(0, OpType.READ, new ItemImpl("item"), null, "/path", null);
op.driverRecycled(true);
final var copy = op.result();
assertTrue(copy.driverRecycled(), "driverRecycled should be preserved in result copy");
}

@Test
void driverRecycled_clearedByReset() {
final var op = new OperationImpl<>(0, OpType.READ, new ItemImpl("item"), null, "/path", null);
op.driverRecycled(true);
op.reset();
assertFalse(op.driverRecycled(), "driverRecycled should be cleared by reset()");
}
}
Loading
Loading