4444import java .io .IOException ;
4545import java .io .InputStream ;
4646import java .io .OutputStream ;
47- import java .io .PipedReader ;
48- import java .io .PipedWriter ;
4947import java .io .Reader ;
5048import java .net .InetAddress ;
5149import java .net .URI ;
6260import java .util .Objects ;
6361import java .util .concurrent .CompletableFuture ;
6462import java .util .concurrent .Executors ;
63+ import java .util .concurrent .LinkedBlockingQueue ;
6564import java .util .concurrent .ScheduledExecutorService ;
6665import java .util .concurrent .ScheduledFuture ;
6766import java .util .concurrent .TimeUnit ;
@@ -165,16 +164,12 @@ public void checkServerTrusted(X509Certificate[] certs, String authType) {}
165164
166165 private class WebSocketListener implements WebSocket .Listener {
167166 private final StringBuilder messageBuffer = new StringBuilder ();
168- private PipedWriter importPipeWriter = null ;
167+ private ImportFrameQueue importFrameQueue = null ;
169168
170169 private void closePipe () {
171- if (importPipeWriter != null ) {
172- try {
173- importPipeWriter .close ();
174- } catch (IOException e ) {
175- logger .debug ("Error closing import pipe writer" , e );
176- }
177- importPipeWriter = null ;
170+ if (importFrameQueue != null ) {
171+ importFrameQueue .finish ();
172+ importFrameQueue = null ;
178173 }
179174 }
180175
@@ -188,12 +183,14 @@ public void onOpen(WebSocket ws) {
188183
189184 @ Override
190185 public CompletableFuture <?> onText (WebSocket ws , CharSequence data , boolean last ) {
191- // Streaming import path: pipe each frame directly to the worker thread
192- if (importPipeWriter != null ) {
186+ logger .debug ("onText data size {} last {}" , data .length (), last );
187+
188+ // Streaming import path: queue each frame for the worker thread (non-blocking)
189+ if (importFrameQueue != null ) {
193190 try {
194- importPipeWriter . append (data );
195- } catch (IOException e ) {
196- logger .error ("Error writing frame to import pipe ; streaming import aborted" , e );
191+ importFrameQueue . addFrame (data );
192+ } catch (Exception e ) {
193+ logger .error ("Error queuing frame for import; streaming import aborted" , e );
197194 closePipe ();
198195 }
199196 if (last ) {
@@ -205,21 +202,18 @@ public CompletableFuture<?> onText(WebSocket ws, CharSequence data, boolean last
205202
206203 // Detect import_workflow on the first frame of a new message
207204 if (messageBuffer .length () == 0 && isImportMessage (data )) {
208- try {
209- PipedReader pipeReader = new PipedReader (8 * 1024 );
210- importPipeWriter = new PipedWriter (pipeReader );
211- importPipeWriter .append (data );
212- if (last ) {
213- closePipe ();
214- }
215- streamImportAsync (Conductor .this , ws , pipeReader );
216- ws .request (1 );
217- return null ;
218- } catch (IOException e ) {
219- logger .error ("Failed to start streaming import; falling back to buffered path" , e );
205+ logger .debug ("import message detected" );
206+
207+ importFrameQueue = new ImportFrameQueue ();
208+ importFrameQueue .addFrame (data );
209+ streamImportAsync (Conductor .this , ws , importFrameQueue );
210+ logger .debug ("streamImportAsync started" );
211+ if (last ) {
220212 closePipe ();
221- // fall through to buffered path
222213 }
214+
215+ ws .request (1 );
216+ return null ;
223217 }
224218
225219 messageBuffer .append (data );
@@ -343,6 +337,62 @@ public void onError(WebSocket ws, Throwable error) {
343337 }
344338 }
345339
340+ /**
341+ * A Reader backed by an unbounded queue of CharSequence frames. The producer side (onText) calls
342+ * addFrame/finish which never block. The consumer side (streamImportAsync) reads data normally,
343+ * blocking only when waiting for more frames to arrive. This avoids blocking the WebSocket I/O
344+ * thread, which would prevent the TCP receive buffer from draining and cause the conductor's pong
345+ * writes to time out.
346+ */
347+ private static class ImportFrameQueue extends Reader {
348+ private final LinkedBlockingQueue <CharSequence > frames = new LinkedBlockingQueue <>();
349+ private final AtomicBoolean done = new AtomicBoolean (false );
350+ private CharSequence current = null ;
351+ private int pos = 0 ;
352+
353+ void addFrame (CharSequence data ) {
354+ frames .add (data );
355+ }
356+
357+ void finish () {
358+ if (done .compareAndSet (false , true )) {
359+ frames .add ("" ); // wake up a blocked reader
360+ }
361+ }
362+
363+ @ Override
364+ public int read (char [] cbuf , int off , int len ) throws IOException {
365+ while (true ) {
366+ if (current != null && pos < current .length ()) {
367+ int n = Math .min (len , current .length () - pos );
368+ for (int i = 0 ; i < n ; i ++) {
369+ cbuf [off + i ] = current .charAt (pos + i );
370+ }
371+ pos += n ;
372+ return n ;
373+ }
374+ try {
375+ CharSequence next = frames .poll (100 , TimeUnit .MILLISECONDS );
376+ if (next == null ) {
377+ if (done .get () && frames .isEmpty ()) return -1 ;
378+ continue ;
379+ }
380+ if (next .length () == 0 && done .get ()) return -1 ;
381+ current = next ;
382+ pos = 0 ;
383+ } catch (InterruptedException e ) {
384+ Thread .currentThread ().interrupt ();
385+ throw new IOException ("Interrupted reading import stream" , e );
386+ }
387+ }
388+ }
389+
390+ @ Override
391+ public void close () {
392+ finish ();
393+ }
394+ }
395+
346396 private static void writeFragmentedResponse (WebSocket ws , BaseResponse response )
347397 throws Exception {
348398 int fragmentSize = 128 * 1024 ; // 128k
@@ -372,7 +422,7 @@ private static class FragmentingOutputStream extends OutputStream {
372422 this .ws = ws ;
373423 this .fragmentSize = fragmentSize ;
374424 this .buffer = new byte [fragmentSize ];
375- logger .debug ("Created JdkFragmentingOutputStream with fragment size: {}" , fragmentSize );
425+ logger .debug ("Created FragmentingOutputStream with fragment size: {}" , fragmentSize );
376426 }
377427
378428 @ Override
0 commit comments