2525import org .apache .iotdb .commons .concurrent .threadpool .ScheduledExecutorUtil ;
2626import org .apache .iotdb .commons .conf .CommonDescriptor ;
2727import org .apache .iotdb .commons .exception .IoTDBException ;
28+ import org .apache .iotdb .commons .exception .IoTDBRuntimeException ;
2829import org .apache .iotdb .db .conf .IoTDBDescriptor ;
2930import org .apache .iotdb .db .exception .query .QueryTimeoutRuntimeException ;
3031import org .apache .iotdb .db .queryengine .common .FragmentInstanceId ;
4546import org .apache .iotdb .db .storageengine .dataregion .IDataRegionForQuery ;
4647import org .apache .iotdb .db .utils .SetThreadName ;
4748import org .apache .iotdb .mpp .rpc .thrift .TFetchFragmentInstanceStatisticsResp ;
49+ import org .apache .iotdb .rpc .TSStatusCode ;
4850
4951import io .airlift .units .Duration ;
5052import org .slf4j .Logger ;
@@ -143,22 +145,29 @@ public FragmentInstanceInfo execDataQueryFragmentInstance(
143145 FragmentInstanceStateMachine stateMachine =
144146 new FragmentInstanceStateMachine (instanceId , instanceNotificationExecutor );
145147
146- int dataNodeFINum = instance .getDataNodeFINum ();
147- DataNodeQueryContext dataNodeQueryContext =
148- getOrCreateDataNodeQueryContext (instanceId .getQueryId (), dataNodeFINum );
149-
148+ boolean [] contextCreated = new boolean [] {false };
149+ DataNodeQueryContext [] dataNodeQueryContexts = new DataNodeQueryContext [1 ];
150150 FragmentInstanceContext context =
151151 instanceContext .computeIfAbsent (
152152 instanceId ,
153- fragmentInstanceId ->
154- createFragmentInstanceContext (
155- fragmentInstanceId ,
156- stateMachine ,
157- instance .getSessionInfo (),
158- dataRegion ,
159- instance .getGlobalTimePredicate (),
160- dataNodeQueryContextMap ,
161- instance .isDebug ()));
153+ fragmentInstanceId -> {
154+ contextCreated [0 ] = true ;
155+ // Only ensure the DataNodeQueryContext when we actually create the
156+ // FragmentInstanceContext, so the repeated-dispatch path (which rejects
157+ // without creating a context) does not leak a context entry.
158+ dataNodeQueryContexts [0 ] =
159+ getOrCreateDataNodeQueryContext (
160+ instanceId .getQueryId (), instance .getDataNodeFINum ());
161+ return createFragmentInstanceContext (
162+ fragmentInstanceId ,
163+ stateMachine ,
164+ instance .getSessionInfo (),
165+ dataRegion ,
166+ instance .getGlobalTimePredicate (),
167+ dataNodeQueryContextMap ,
168+ instance .isDebug ());
169+ });
170+ rejectIfRepeatedDispatch (contextCreated [0 ], instanceId );
162171 context .setHighestPriority (instance .isHighestPriority ());
163172
164173 try {
@@ -167,7 +176,7 @@ public FragmentInstanceInfo execDataQueryFragmentInstance(
167176 instance .getFragment ().getPlanNodeTree (),
168177 instance .getFragment ().getTypeProvider (),
169178 context ,
170- dataNodeQueryContext );
179+ dataNodeQueryContexts [ 0 ] );
171180
172181 List <IDriver > drivers = new ArrayList <>();
173182 driverFactories .forEach (factory -> drivers .add (factory .createDriver ()));
@@ -231,6 +240,30 @@ public FragmentInstanceInfo execDataQueryFragmentInstance(
231240 }
232241 }
233242
243+ /**
244+ * If {@code instanceContext.computeIfAbsent} returned an existing {@link FragmentInstanceContext}
245+ * for this {@code instanceId} (i.e. {@code contextCreated} is false), the same FragmentInstance
246+ * has been dispatched before (e.g. an RPC retry in {@code
247+ * FragmentInstanceDispatcherImpl#dispatchRemote}). The previous execution may have already
248+ * released its resources (dataRegion == null), so reusing this cached context would run a fresh
249+ * driver against a released context and trigger an NPE. Reject the duplicated dispatch with
250+ * REPEATED_RPC_CALL instead of reusing it.
251+ *
252+ * <p>This must be called before the planning try block on purpose, so it propagates up
253+ * (RegionReadExecutor carries the status code) without touching the first execution's cached
254+ * resources.
255+ */
256+ private static void rejectIfRepeatedDispatch (
257+ boolean contextCreated , FragmentInstanceId instanceId ) {
258+ if (!contextCreated ) {
259+ throw new IoTDBRuntimeException (
260+ String .format (
261+ "Repeated RPC call detected for FragmentInstance %s, reject the duplicated dispatch." ,
262+ instanceId .getFullId ()),
263+ TSStatusCode .REPEATED_RPC_CALL .getStatusCode ());
264+ }
265+ }
266+
234267 private void clearFIRelatedResources (FragmentInstanceId instanceId ) {
235268 // close and remove all the handles of the fragment instance
236269 exchangeManager .forceDeregisterFragmentInstance (instanceId .toThrift ());
@@ -255,15 +288,19 @@ public FragmentInstanceInfo execSchemaQueryFragmentInstance(
255288 FragmentInstanceStateMachine stateMachine =
256289 new FragmentInstanceStateMachine (instanceId , instanceNotificationExecutor );
257290
291+ boolean [] contextCreated = new boolean [] {false };
258292 FragmentInstanceContext context =
259293 instanceContext .computeIfAbsent (
260294 instanceId ,
261- fragmentInstanceId ->
262- createFragmentInstanceContext (
263- fragmentInstanceId ,
264- stateMachine ,
265- instance .getSessionInfo (),
266- instance .isDebug ()));
295+ fragmentInstanceId -> {
296+ contextCreated [0 ] = true ;
297+ return createFragmentInstanceContext (
298+ fragmentInstanceId ,
299+ stateMachine ,
300+ instance .getSessionInfo (),
301+ instance .isDebug ());
302+ });
303+ rejectIfRepeatedDispatch (contextCreated [0 ], instanceId );
267304 context .setHighestPriority (instance .isHighestPriority ());
268305
269306 try {
0 commit comments