@@ -120,28 +120,35 @@ public final void startResponse(List<Attribute> schema) {
120120 return new ColumnInfoImpl (c .name (), c .dataType ().outputType (), originalTypes );
121121 }).toList ();
122122
123- doStartResponse (columns );
123+ sendChunks (doStartResponse (columns ));
124+
124125 initialStreamChunkSent = true ;
125126 }
126127
127128 public final void sendPages (Iterable <Page > pages ) {
128129 assert finished == false : "sendPages() called on a finished stream" ;
129130
130131 if (initialStreamChunkSent ) {
131- doSendPages (pages );
132+ sendChunks ( doSendPages (pages ) );
132133 }
133134 }
134135
135136 public final void finishResponse (EsqlQueryResponse response ) {
136137 assert finished == false : "finishResponse() called more than once" ;
137138
138- try (response ) {
139+ // TODO: Also, is this closing right? EsqlResponseListener uses releasableFromResponse(), which increments the ref first
140+ boolean success = false ;
141+ try {
139142 if (initialStreamChunkSent ) {
140- doFinishResponse (response );
143+ sendChunks ( doFinishResponse (response ), response );
141144 } else {
142- doSendEverything (response );
145+ sendChunks ( doSendEverything (response ), response );
143146 }
147+ success = true ;
144148 } finally {
149+ if (success == false ) {
150+ response .close ();
151+ }
145152 finished = true ;
146153 }
147154 }
@@ -152,6 +159,8 @@ public final void handleException(Exception e) {
152159 // TODO: To be overridden by subclasses. This should append the error to the stream, if possible
153160 LOGGER .error ("Error while streaming response" , e );
154161
162+ sendChunks (doHandleException (e ));
163+
155164 finished = true ;
156165 }
157166
@@ -185,25 +194,65 @@ public void onFailure(Exception e) {
185194 */
186195 protected abstract boolean canBeStreamed ();
187196
188- protected abstract void doStartResponse (List <ColumnInfoImpl > columns );
197+ /**
198+ * Returns the chunks to be sent at the beginning of the response. Called once, at the start.
199+ * <p>
200+ * Only called if {@link #canBeStreamed()} returns {@code true}.
201+ * </p>
202+ */
203+ protected abstract Iterator <? extends ToXContent > doStartResponse (List <ColumnInfoImpl > columns );
189204
190- protected abstract void doSendPages (Iterable <Page > pages );
205+ /**
206+ * Returns the chunks for the given page. Called 0 to N times, after {@link #doStartResponse} and before {@link #doFinishResponse}.
207+ * <p>
208+ * Only called if {@link #canBeStreamed()} returns {@code true}.
209+ * </p>
210+ */
211+ protected abstract Iterator <? extends ToXContent > doSendPages (Iterable <Page > pages );
191212
192- protected abstract void doFinishResponse (EsqlQueryResponse response );
213+ /**
214+ * Returns the remaining chunks of the response. Called once, at the end of the response.
215+ * <p>
216+ * Only called if {@link #canBeStreamed()} returns {@code true}.
217+ * </p>
218+ */
219+ protected abstract Iterator <? extends ToXContent > doFinishResponse (EsqlQueryResponse response );
193220
194- protected abstract void doHandleException (Exception e );
221+ /**
222+ * Returns the chunks to be sent for the given exception.
223+ * <p>
224+ * This may be called at any time, so the code should track what was sent already
225+ * and how to send a meaningful response given the chunks sent in previous calls.
226+ * </p>
227+ */
228+ protected abstract Iterator <? extends ToXContent > doHandleException (Exception e );
195229
196- protected void doSendEverything (EsqlQueryResponse response ) {
197- // TODO: Is this safe? Should this be abstract to ensure proper implementation? Add tests for both cases
198- doStartResponse (response .columns ());
199- doSendPages (response .pages ());
200- doFinishResponse (response );
230+ /**
231+ * Returns the chunks of the full response. Called once for the full response.
232+ * <p>
233+ * Only called if {@link #canBeStreamed()} returns {@code false}.
234+ * </p>
235+ */
236+ protected Iterator <? extends ToXContent > doSendEverything (EsqlQueryResponse response ) {
237+ // TODO: Is this safe? Should this be abstract to ensure proper implementation? Add tests for both streamed and "everything" cases
238+ return Iterators .concat (
239+ doStartResponse (response .columns ()),
240+ doSendPages (response .pages ()),
241+ doFinishResponse (response )
242+ );
201243 }
202244
203245 @ SuppressWarnings ("unchecked" )
204- protected final void sendChunks (List <Iterator <? extends ToXContent >> chunkedContent ) {
205- // TODO: Maybe accept a single chunk here, and do a flush() inside of each method?
206- streamingXContentResponse .writeFragment (p0 -> Iterators .concat (chunkedContent .toArray (Iterator []::new )), () -> {});
246+ protected static Iterator <? extends ToXContent > asIterator (List <Iterator <? extends ToXContent >> chunks ) {
247+ return Iterators .concat (chunks .toArray (Iterator []::new ));
248+ }
249+
250+ private void sendChunks (Iterator <? extends ToXContent > chunks ) {
251+ sendChunks (chunks , () -> {});
252+ }
253+
254+ private void sendChunks (Iterator <? extends ToXContent > chunks , Releasable releasable ) {
255+ streamingXContentResponse .writeFragment (p0 -> chunks , releasable );
207256 }
208257
209258 @ Override
0 commit comments