3030import io .questdb .client .std .QuietCloseable ;
3131import org .jetbrains .annotations .TestOnly ;
3232
33+ import java .util .ArrayDeque ;
34+ import java .util .concurrent .ThreadFactory ;
3335import java .util .concurrent .atomic .AtomicBoolean ;
3436import java .util .concurrent .locks .LockSupport ;
3537
@@ -65,7 +67,15 @@ public final class CursorSendEngine implements QuietCloseable {
6567 public static final long DEFAULT_APPEND_DEADLINE_NANOS = 30_000_000_000L ;
6668 private static final org .slf4j .Logger LOG =
6769 org .slf4j .LoggerFactory .getLogger (CursorSendEngine .class );
70+ private static final ThreadFactory DEFAULT_FLOCK_RELEASE_RETRY_THREAD_FACTORY =
71+ runnable -> new Thread (runnable , "qdb-sf-flock-release-retry" );
72+ private static final Object FLOCK_RELEASE_RETRY_LOCK = new Object ();
73+ private static final ArrayDeque <CursorSendEngine > FLOCK_RELEASE_RETRY_QUEUE = new ArrayDeque <>();
74+ private static volatile Runnable afterFlockReleaseRetryFailureHook ;
6875 private static volatile Runnable beforeDeferredCloseCreationHook ;
76+ private static Thread flockReleaseRetryThread ;
77+ private static volatile ThreadFactory flockReleaseRetryThreadFactory =
78+ DEFAULT_FLOCK_RELEASE_RETRY_THREAD_FACTORY ;
6979 private final long appendDeadlineNanos ;
7080 // Number of times appendBlocking observed BACKPRESSURE_NO_SPARE on its first
7181 // ring.appendOrFsn attempt. One increment per blocking-call that had to wait
@@ -136,15 +146,18 @@ public final class CursorSendEngine implements QuietCloseable {
136146 // isCloseCompleted() from pool threads re-probing a retired slot (see the
137147 // getter for why it must not synchronize).
138148 private volatile boolean closeCompleted ;
149+ // Invoked after closeCompleted publishes a confirmed flock release. Used
150+ // by an owning sender pool to wake capacity-starved borrowers immediately.
151+ private volatile Runnable slotLockReleaseListener ;
139152 // Test-only hook run by finishClose() between the terminal cleanup and
140153 // the flock release. Lets a test park the releasing thread inside the
141154 // cleanup/release window and assert that closeCompleted stays false —
142155 // i.e. that completion is never observable while the flock is still
143156 // held. volatile: finishClose may run on the manager worker's exit
144157 // thread while the hook is installed from a test thread.
145158 private volatile Runnable beforeFlockReleaseHook ;
146- // Exactly one daemon drives a failed flock release to completion. The
147- // error path only: ordinary closes never allocate or start a thread .
159+ // Ensures this engine has at most one entry in the shared flock-release
160+ // retry driver. The error path only: ordinary closes never enqueue work .
148161 private final AtomicBoolean flockReleaseRetryStarted = new AtomicBoolean ();
149162 // Published before deferredClose is registered. The manager lock provides
150163 // the callback handoff fence; volatile also covers a direct test/retry read.
@@ -781,6 +794,15 @@ private void finishClose(boolean fullyDrained) {
781794 }
782795 }
783796
797+ /**
798+ * Installs a hook invoked after each failed shared-driver flock release.
799+ * Test-only: makes persistent retry progress observable without sleeps.
800+ */
801+ @ TestOnly
802+ public static void setAfterFlockReleaseRetryFailureHook (Runnable hook ) {
803+ afterFlockReleaseRetryFailureHook = hook ;
804+ }
805+
784806 /**
785807 * Installs a constructor fault hook immediately before the bound deferred
786808 * close callback is created. Test-only: proves callback allocation failure
@@ -791,6 +813,23 @@ public static void setBeforeDeferredCloseCreationHook(Runnable hook) {
791813 beforeDeferredCloseCreationHook = hook ;
792814 }
793815
816+ /**
817+ * Overrides creation of the single shared flock-release retry thread.
818+ * Test-only: makes thread creation/start failure and persistent retry
819+ * scalability deterministic without relying on scheduler timing.
820+ */
821+ @ TestOnly
822+ public static void setFlockReleaseRetryThreadFactory (ThreadFactory factory ) {
823+ synchronized (FLOCK_RELEASE_RETRY_LOCK ) {
824+ if (flockReleaseRetryThread != null || !FLOCK_RELEASE_RETRY_QUEUE .isEmpty ()) {
825+ throw new IllegalStateException ("flock-release retry driver is active" );
826+ }
827+ flockReleaseRetryThreadFactory = factory == null
828+ ? DEFAULT_FLOCK_RELEASE_RETRY_THREAD_FACTORY
829+ : factory ;
830+ }
831+ }
832+
794833 /**
795834 * Installs a hook that {@link #finishClose} runs between the terminal
796835 * cleanup and the slot-flock release. Test-only: makes the otherwise
@@ -849,42 +888,97 @@ private boolean retryFlockReleaseIfReady() {
849888 }
850889 if (released ) {
851890 closeCompleted = true ;
891+ Runnable listener = slotLockReleaseListener ;
892+ if (listener != null ) {
893+ try {
894+ listener .run ();
895+ } catch (Throwable ignored ) {
896+ // A notification failure must not invalidate a confirmed release.
897+ }
898+ }
852899 return true ;
853900 }
854901 startFlockReleaseRetry ();
855902 return false ;
856903 }
857904
905+ private static void runFlockReleaseRetryDriver () {
906+ while (true ) {
907+ final int roundSize ;
908+ synchronized (FLOCK_RELEASE_RETRY_LOCK ) {
909+ roundSize = FLOCK_RELEASE_RETRY_QUEUE .size ();
910+ if (roundSize == 0 ) {
911+ flockReleaseRetryThread = null ;
912+ return ;
913+ }
914+ }
915+ boolean hasFailures = false ;
916+ for (int i = 0 ; i < roundSize ; i ++) {
917+ final CursorSendEngine engine ;
918+ synchronized (FLOCK_RELEASE_RETRY_LOCK ) {
919+ engine = FLOCK_RELEASE_RETRY_QUEUE .pollFirst ();
920+ }
921+ if (engine .retryFlockReleaseIfReady ()) {
922+ engine .flockReleaseRetryStarted .set (false );
923+ } else {
924+ synchronized (FLOCK_RELEASE_RETRY_LOCK ) {
925+ FLOCK_RELEASE_RETRY_QUEUE .addLast (engine );
926+ }
927+ hasFailures = true ;
928+ Runnable hook = afterFlockReleaseRetryFailureHook ;
929+ if (hook != null ) {
930+ hook .run ();
931+ }
932+ }
933+ }
934+ if (hasFailures ) {
935+ // Interruption must not abandon a retained flock, but clear
936+ // the flag so subsequent parks still throttle retries.
937+ Thread .interrupted ();
938+ LockSupport .parkNanos (100_000_000L );
939+ }
940+ }
941+ }
942+
858943 private void startFlockReleaseRetry () {
859944 if (!flockReleaseRetryStarted .compareAndSet (false , true )) {
860945 return ;
861946 }
862- try {
863- Thread retryThread = new Thread (() -> {
864- while (!retryFlockReleaseIfReady ()) {
865- try {
866- Thread .sleep (100L );
867- } catch (InterruptedException ignored ) {
868- // A failed flock release must remain retryable until
869- // confirmed or process exit; interruption is not a
870- // safe reason to abandon the slot permanently.
947+ Throwable startFailure = null ;
948+ synchronized (FLOCK_RELEASE_RETRY_LOCK ) {
949+ FLOCK_RELEASE_RETRY_QUEUE .addLast (this );
950+ if (flockReleaseRetryThread == null ) {
951+ try {
952+ Thread retryThread = flockReleaseRetryThreadFactory .newThread (
953+ CursorSendEngine ::runFlockReleaseRetryDriver );
954+ if (retryThread == null ) {
955+ throw new IllegalStateException ("retry thread factory returned null" );
956+ }
957+ retryThread .setDaemon (true );
958+ flockReleaseRetryThread = retryThread ;
959+ retryThread .start ();
960+ } catch (Throwable t ) {
961+ startFailure = t ;
962+ flockReleaseRetryThread = null ;
963+ CursorSendEngine queued ;
964+ while ((queued = FLOCK_RELEASE_RETRY_QUEUE .pollFirst ()) != null ) {
965+ queued .flockReleaseRetryStarted .set (false );
871966 }
872967 }
873- }, "qdb-sf-flock-release-retry" );
874- retryThread . setDaemon ( true );
875- retryThread . start ();
968+ }
969+ }
970+ if ( startFailure == null ) {
876971 LOG .error ("SF slot flock release failed during engine close; keeping "
877- + "closeCompleted=false and retrying outside the pool lock so "
972+ + "closeCompleted=false and retrying on the shared driver so "
878973 + "retired capacity recovers after the transient failure [slot={}]" ,
879974 sfDir == null ? "<memory>" : sfDir );
880- } catch ( Throwable t ) {
975+ } else {
881976 // A later explicit close() can still retry without repeating the
882- // one-time ring/watermark cleanup. The kernel remains the final
883- // process-exit backstop if thread creation and all retries fail.
884- flockReleaseRetryStarted .set (false );
977+ // one-time ring/watermark cleanup. The failed queue is cleared so
978+ // the driver does not retain engines it cannot service.
885979 LOG .error ("Could not start SF flock-release retry driver; close() may be "
886980 + "invoked again to retry [slot={}, error={}]" ,
887- sfDir == null ? "<memory>" : sfDir , String .valueOf (t ));
981+ sfDir == null ? "<memory>" : sfDir , String .valueOf (startFailure ));
888982 }
889983 }
890984
@@ -910,6 +1004,17 @@ public boolean isCloseCompleted() {
9101004 return closeCompleted ;
9111005 }
9121006
1007+ /**
1008+ * Registers a callback for confirmed SF slot-lock release. If release
1009+ * already completed, invokes the callback before returning.
1010+ */
1011+ public void setSlotLockReleaseListener (Runnable listener ) {
1012+ slotLockReleaseListener = listener ;
1013+ if (listener != null && closeCompleted ) {
1014+ listener .run ();
1015+ }
1016+ }
1017+
9131018 /**
9141019 * Pass-through to {@link SegmentRing#findSegmentContaining(long)}.
9151020 */
0 commit comments