Skip to content

Commit 8a74450

Browse files
authored
[Pipe] Distribute parser admission across processor workers (#19217) (#19231)
(cherry picked from commit 7666083)
1 parent 82b01f4 commit 8a74450

4 files changed

Lines changed: 231 additions & 6 deletions

File tree

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEvent.java‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,7 +1076,8 @@ private void releaseTsFileParserMemoryIfReserved() {
10761076
synchronized (isTsFileParserMemoryReserved) {
10771077
if (isTsFileParserMemoryReserved.compareAndSet(true, false)) {
10781078
PipeDataNodeResourceManager.memory()
1079-
.releaseTsFileParserMemory(pipeName, creationTime, dataRegionId);
1079+
.releaseTsFileParserMemory(
1080+
pipeName, creationTime, dataRegionId, tsFileParserMemoryReservationKey);
10801081
}
10811082
}
10821083
}
@@ -1339,7 +1340,8 @@ protected void finalizeResource() {
13391340
synchronized (isTsFileParserMemoryReserved) {
13401341
if (isTsFileParserMemoryReserved.compareAndSet(true, false)) {
13411342
PipeDataNodeResourceManager.memory()
1342-
.releaseTsFileParserMemory(pipeName, creationTime, dataRegionId);
1343+
.releaseTsFileParserMemory(
1344+
pipeName, creationTime, dataRegionId, tsFileParserMemoryReservationKey);
13431345
}
13441346
}
13451347
} catch (final Exception e) {

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/resource/memory/PipeMemoryManager.java‎

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import java.util.Collections;
3737
import java.util.HashMap;
3838
import java.util.HashSet;
39+
import java.util.LinkedHashMap;
3940
import java.util.LinkedHashSet;
4041
import java.util.List;
4142
import java.util.Map;
@@ -74,6 +75,9 @@ public class PipeMemoryManager {
7475
private final Map<PipeIdentity, Integer> reservedTsFileParserCountByPipe = new HashMap<>();
7576
private final Map<PipeRegionIdentity, Integer> reservedTsFileParserCountByPipeRegion =
7677
new HashMap<>();
78+
private final Map<PipeRegionIdentity, Map<TsFileParserMemoryReservation, Runnable>>
79+
reservedTsFileParserWorkersByPipeRegion = new HashMap<>();
80+
private final Map<Runnable, Integer> reservedTsFileParserCountByWorker = new HashMap<>();
7781
private final Map<PipeRegionIdentity, LinkedHashSet<TsFileParserMemoryReservation>>
7882
waitingTsFileParserRequestsByPipeRegion = new HashMap<>();
7983
private final Map<PipeIdentity, ArrayDeque<PipeRegionIdentity>>
@@ -242,8 +246,10 @@ public synchronized boolean tryReserveTsFileParserMemory(
242246
1, Math.min(globalLimit, PIPE_CONFIG.getPipeTsFileParserInFlightMaxNumPerPipeRegion()));
243247
final int reservedCountOfPipeRegion =
244248
reservedTsFileParserCountByPipeRegion.getOrDefault(pipeRegionIdentity, 0);
249+
final Runnable worker = reservationKey.wakeUpCallback;
245250
if (reservedTsFileParserCount >= globalLimit
246-
|| reservedCountOfPipeRegion >= perPipeRegionLimit) {
251+
|| reservedCountOfPipeRegion >= perPipeRegionLimit
252+
|| (worker != null && reservedTsFileParserCountByWorker.containsKey(worker))) {
247253
PipeResourceMetrics.getInstance()
248254
.recordTsFileParserAdmissionReject(TsFileParserAdmissionRejectReason.CAP);
249255
notifyNextTsFileParserMemoryReservationInternal();
@@ -291,6 +297,12 @@ public synchronized boolean tryReserveTsFileParserMemory(
291297
reservedTsFileParserCount++;
292298
reservedTsFileParserCountByPipe.merge(pipeIdentity, 1, Integer::sum);
293299
reservedTsFileParserCountByPipeRegion.put(pipeRegionIdentity, reservedCountOfPipeRegion + 1);
300+
reservedTsFileParserWorkersByPipeRegion
301+
.computeIfAbsent(pipeRegionIdentity, key -> new LinkedHashMap<>())
302+
.put(reservationKey, worker);
303+
if (worker != null) {
304+
reservedTsFileParserCountByWorker.merge(worker, 1, Integer::sum);
305+
}
294306
notifyNextTsFileParserMemoryReservationInternal();
295307
return true;
296308
}
@@ -312,12 +324,23 @@ public synchronized void cancelTsFileParserMemoryReservation(
312324

313325
public synchronized void releaseTsFileParserMemory(
314326
final String pipeName, final long creationTime, final String dataRegionId) {
327+
releaseTsFileParserMemory(pipeName, creationTime, dataRegionId, null);
328+
}
329+
330+
public synchronized void releaseTsFileParserMemory(
331+
final String pipeName,
332+
final long creationTime,
333+
final String dataRegionId,
334+
final TsFileParserMemoryReservation reservationKey) {
315335
final PipeIdentity pipeIdentity = new PipeIdentity(pipeName, creationTime);
316336
final PipeRegionIdentity pipeRegionIdentity =
317337
new PipeRegionIdentity(pipeIdentity, dataRegionId);
318338
final int reservedCountOfPipeRegion =
319339
reservedTsFileParserCountByPipeRegion.getOrDefault(pipeRegionIdentity, 0);
320-
if (reservedCountOfPipeRegion <= 0) {
340+
final Map<TsFileParserMemoryReservation, Runnable> workersOfPipeRegion =
341+
reservedTsFileParserWorkersByPipeRegion.get(pipeRegionIdentity);
342+
if (reservedCountOfPipeRegion <= 0
343+
|| (reservationKey != null && !workersOfPipeRegion.containsKey(reservationKey))) {
321344
LOGGER.warn(
322345
"Failed to release TsFile parser memory for pipe {} (creation time {}) in DataRegion {} because no reservation exists.",
323346
pipeName,
@@ -326,6 +349,19 @@ public synchronized void releaseTsFileParserMemory(
326349
return;
327350
}
328351

352+
final Runnable worker =
353+
workersOfPipeRegion.remove(
354+
reservationKey != null
355+
? reservationKey
356+
: workersOfPipeRegion.keySet().iterator().next());
357+
if (workersOfPipeRegion.isEmpty()) {
358+
reservedTsFileParserWorkersByPipeRegion.remove(pipeRegionIdentity);
359+
}
360+
if (worker != null) {
361+
reservedTsFileParserCountByWorker.computeIfPresent(
362+
worker, (key, count) -> count == 1 ? null : count - 1);
363+
}
364+
329365
if (reservedCountOfPipeRegion == 1) {
330366
reservedTsFileParserCountByPipeRegion.remove(pipeRegionIdentity);
331367
} else {
@@ -406,6 +442,7 @@ private List<TsFileParserMemoryReservation> getEligibleTsFileParserMemoryReserva
406442
final List<TsFileParserMemoryReservation> eligibleRequests = new ArrayList<>();
407443
final Map<PipeRegionIdentity, Integer> selectedRequestsByPipeRegion = new HashMap<>();
408444
final Set<PipeIdentity> selectedPipes = new HashSet<>();
445+
final Set<Runnable> selectedWorkers = new HashSet<>();
409446
PipeIdentity lastSelectedPipe = lastAdmittedWaitingTsFileParserPipe;
410447
// Expose one fair turn per available slot, instead of serializing all free slots behind a
411448
// single worker. Account for every earlier turn's memory even if its worker has not run yet.
@@ -429,6 +466,7 @@ private List<TsFileParserMemoryReservation> getEligibleTsFileParserMemoryReserva
429466
lastSelectedPipe,
430467
selectedRequestsByPipeRegion,
431468
selectedPipes,
469+
selectedWorkers,
432470
true);
433471
final PipeRegionIdentity nextRegionWhenNoOtherPipeIsEligible =
434472
nextPipeRegion == null
@@ -438,6 +476,7 @@ private List<TsFileParserMemoryReservation> getEligibleTsFileParserMemoryReserva
438476
lastSelectedPipe,
439477
selectedRequestsByPipeRegion,
440478
selectedPipes,
479+
selectedWorkers,
441480
false)
442481
: nextPipeRegion;
443482
if (nextRegionWhenNoOtherPipeIsEligible == null) {
@@ -453,7 +492,11 @@ private List<TsFileParserMemoryReservation> getEligibleTsFileParserMemoryReserva
453492
i--) {
454493
requests.next();
455494
}
456-
eligibleRequests.add(requests.next());
495+
final TsFileParserMemoryReservation request = requests.next();
496+
eligibleRequests.add(request);
497+
if (request.wakeUpCallback != null) {
498+
selectedWorkers.add(request.wakeUpCallback);
499+
}
457500
selectedRequestsByPipeRegion.merge(nextRegionWhenNoOtherPipeIsEligible, 1, Integer::sum);
458501
selectedPipes.add(nextRegionWhenNoOtherPipeIsEligible.pipeIdentity);
459502
lastSelectedPipe = nextRegionWhenNoOtherPipeIsEligible.pipeIdentity;
@@ -467,6 +510,7 @@ private PipeRegionIdentity getNextEligibleTsFileParserPipeRegion(
467510
final PipeIdentity lastSelectedPipe,
468511
final Map<PipeRegionIdentity, Integer> selectedRequestsByPipeRegion,
469512
final Set<PipeIdentity> selectedPipes,
513+
final Set<Runnable> selectedWorkers,
470514
final boolean requireUnselectedPipe) {
471515
PipeRegionIdentity firstEligiblePipeRegion = null;
472516
boolean hasVisitedLastAdmittedPipe = lastSelectedPipe == null;
@@ -499,6 +543,20 @@ private PipeRegionIdentity getNextEligibleTsFileParserPipeRegion(
499543
&& reservedTsFileParserCountByPipeRegion.getOrDefault(pipeRegionIdentity, 0)
500544
+ selectedCount
501545
< perPipeRegionLimit) {
546+
final java.util.Iterator<TsFileParserMemoryReservation> requests =
547+
waitingTsFileParserRequestsByPipeRegion.get(pipeRegionIdentity).iterator();
548+
for (int i = 0; i < selectedCount; i++) {
549+
requests.next();
550+
}
551+
final Runnable worker = requests.next().wakeUpCallback;
552+
// A processor worker executes its subtasks serially. Leaving multiple fair turns for
553+
// that worker strands free parser slots while other workers have runnable requests.
554+
// Requests without a processor worker (for example subscriptions) remain independent.
555+
if (worker != null
556+
&& (reservedTsFileParserCountByWorker.containsKey(worker)
557+
|| selectedWorkers.contains(worker))) {
558+
continue;
559+
}
502560
if (eligiblePipeRegion == null) {
503561
eligiblePipeRegion = pipeRegionIdentity;
504562
}
@@ -579,7 +637,10 @@ public static final class TsFileParserMemoryReservation {
579637
// Accessed only under the PipeMemoryManager monitor while this request is queued.
580638
private long waitingStartTimeInNanos;
581639

582-
/** The callback must only wake the worker; it runs while the memory manager monitor is held. */
640+
/**
641+
* The callback must only wake the worker; it runs while the memory manager monitor is held.
642+
* Requests on the same serial processor worker must share the same callback instance.
643+
*/
583644
public void setWakeUpCallback(final Runnable wakeUpCallback) {
584645
this.wakeUpCallback = wakeUpCallback;
585646
}

‎iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskWorkerTest.java‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,104 @@ public static void initializeSubtaskMock() {
6666
mock(PipeProcessorSubtask.class);
6767
}
6868

69+
@Test(timeout = 30000)
70+
public void testRequestsOnOneWorkerDoNotStrandOtherWorkersParserSlots() throws Exception {
71+
final CommonConfig config = CommonDescriptor.getInstance().getConfig();
72+
final PipeMemoryManager manager = PipeDataNodeResourceManager.memory();
73+
final long originalParserMemory = config.getPipeTsFileParserMemory();
74+
final int originalGlobalLimit = config.getPipeTsFileParserInFlightMaxNum();
75+
final int originalRegionLimit = config.getPipeTsFileParserInFlightMaxNumPerPipeRegion();
76+
final List<PipeProcessorSubtaskWorker> workers = new ArrayList<>();
77+
final List<TsFileParserMemoryReservation> requests = new ArrayList<>();
78+
final boolean[] reservedBlockers = new boolean[4];
79+
final CountDownLatch parsing = new CountDownLatch(4);
80+
final CountDownLatch finishParsing = new CountDownLatch(1);
81+
final ExecutorService executor = Executors.newFixedThreadPool(4);
82+
try {
83+
config.setPipeTsFileParserMemory(1);
84+
config.setPipeTsFileParserInFlightMaxNum(4);
85+
config.setPipeTsFileParserInFlightMaxNumPerPipeRegion(1);
86+
for (int i = 0; i < 4; i++) {
87+
reservedBlockers[i] =
88+
manager.tryReserveTsFileParserMemory(
89+
"blocker" + i, 0, "0", new TsFileParserMemoryReservation());
90+
Assert.assertTrue(reservedBlockers[i]);
91+
workers.add(new PipeProcessorSubtaskWorker(new LinkedHashSet<>()));
92+
}
93+
// Queue four pipes on the same serial worker before the other workers' requests. A fair
94+
// window based only on pipe order leaves all four free slots waiting for that one worker.
95+
for (int i = 0; i < 7; i++) {
96+
final String pipeName = "skewed" + i;
97+
final TsFileParserMemoryReservation request = new TsFileParserMemoryReservation();
98+
requests.add(request);
99+
final PipeProcessorSubtask subtask = createRunnableSubtask(pipeName);
100+
final AtomicBoolean processed = new AtomicBoolean();
101+
when(subtask.isWaitingForTsFileParserMemory())
102+
.thenAnswer(ignored -> request.isWaitingForSignal());
103+
doAnswer(
104+
invocation -> {
105+
request.setWakeUpCallback(invocation.getArgument(0));
106+
return null;
107+
})
108+
.when(subtask)
109+
.setWorkerWakeUpCallback(any());
110+
when(subtask.call())
111+
.thenAnswer(
112+
ignored -> {
113+
if (processed.get()) {
114+
return false;
115+
}
116+
if (!manager.tryReserveTsFileParserMemory(pipeName, 0, "0", request)) {
117+
throw PipeProcessorSubtaskYieldException.parserNotAdmitted();
118+
}
119+
try {
120+
parsing.countDown();
121+
Assert.assertTrue(finishParsing.await(10, TimeUnit.SECONDS));
122+
processed.set(true);
123+
return true;
124+
} finally {
125+
manager.releaseTsFileParserMemory(pipeName, 0, "0");
126+
}
127+
});
128+
workers.get(i < 4 ? 0 : i - 3).schedule(subtask);
129+
Assert.assertFalse(manager.tryReserveTsFileParserMemory(pipeName, 0, "0", request));
130+
}
131+
for (int i = 0; i < 4; i++) {
132+
manager.releaseTsFileParserMemory("blocker" + i, 0, "0");
133+
reservedBlockers[i] = false;
134+
}
135+
final List<Future<?>> rounds = new ArrayList<>();
136+
for (final PipeProcessorSubtaskWorker worker : workers) {
137+
rounds.add(executor.submit(worker::runSubtasks));
138+
}
139+
Assert.assertTrue(
140+
"Free parser slots must run on distinct available workers",
141+
parsing.await(5, TimeUnit.SECONDS));
142+
Assert.assertEquals(4, manager.getReservedTsFileParserCount());
143+
finishParsing.countDown();
144+
for (final Future<?> round : rounds) {
145+
round.get(5, TimeUnit.SECONDS);
146+
}
147+
Assert.assertEquals(0, manager.getReservedTsFileParserCount());
148+
Assert.assertEquals(0, manager.getWaitingTsFileParserCount());
149+
} finally {
150+
finishParsing.countDown();
151+
executor.shutdownNow();
152+
Assert.assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
153+
for (int i = 0; i < requests.size(); i++) {
154+
manager.cancelTsFileParserMemoryReservation("skewed" + i, 0, "0", requests.get(i));
155+
}
156+
for (int i = 0; i < reservedBlockers.length; i++) {
157+
if (reservedBlockers[i]) {
158+
manager.releaseTsFileParserMemory("blocker" + i, 0, "0");
159+
}
160+
}
161+
config.setPipeTsFileParserMemory(originalParserMemory);
162+
config.setPipeTsFileParserInFlightMaxNum(originalGlobalLimit);
163+
config.setPipeTsFileParserInFlightMaxNumPerPipeRegion(originalRegionLimit);
164+
}
165+
}
166+
69167
@Test(timeout = 30000)
70168
public void testOneHundredWaitingPipesDoNotWaitForWorkerPolling() throws Exception {
71169
final CommonConfig config = CommonDescriptor.getInstance().getConfig();

‎iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/resource/memory/PipeMemoryManagerTest.java‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,70 @@ public void testFreeSlotsAreAvailableWithoutWaitingForTheFirstWorker() {
143143
Assert.assertEquals(96, memoryManager.getWaitingTsFileParserCount());
144144
}
145145

146+
@Test
147+
public void testReleasingParserOnlyFreesItsOwnWorker() {
148+
commonConfig.setPipeTsFileParserInFlightMaxNum(4);
149+
commonConfig.setPipeTsFileParserInFlightMaxNumPerPipeRegion(2);
150+
final PipeMemoryManager manager = createTestMemoryManager();
151+
final Reservation firstWorker = new Reservation("pipe", 0);
152+
final Reservation secondWorker = new Reservation("pipe", 0);
153+
final Reservation waitingOnFirstWorker = new Reservation("otherPipe", 1);
154+
final Reservation waitingOnSecondWorker = new Reservation("otherPipe", 2);
155+
final Runnable firstWakeUp = new AtomicInteger()::incrementAndGet;
156+
final Runnable secondWakeUp = new AtomicInteger()::incrementAndGet;
157+
firstWorker.key.setWakeUpCallback(firstWakeUp);
158+
waitingOnFirstWorker.key.setWakeUpCallback(firstWakeUp);
159+
secondWorker.key.setWakeUpCallback(secondWakeUp);
160+
waitingOnSecondWorker.key.setWakeUpCallback(secondWakeUp);
161+
final List<Reservation> testReservations =
162+
List.of(firstWorker, secondWorker, waitingOnFirstWorker, waitingOnSecondWorker);
163+
try {
164+
Assert.assertTrue(tryAcquire(manager, firstWorker));
165+
Assert.assertTrue(tryAcquire(manager, secondWorker));
166+
Assert.assertFalse(tryAcquire(manager, waitingOnFirstWorker));
167+
Assert.assertFalse(tryAcquire(manager, waitingOnSecondWorker));
168+
169+
// The two parsers share a pipe/region but finish in reverse order. Releasing the second
170+
// reservation must wake only its worker, rather than freeing the first worker's slot.
171+
manager.releaseTsFileParserMemory(
172+
secondWorker.pipeName,
173+
secondWorker.creationTime,
174+
secondWorker.dataRegionId,
175+
secondWorker.key);
176+
secondWorker.acquired = false;
177+
Assert.assertTrue(waitingOnFirstWorker.key.isWaitingForSignal());
178+
Assert.assertFalse(waitingOnSecondWorker.key.isWaitingForSignal());
179+
Assert.assertTrue(tryAcquire(manager, waitingOnSecondWorker));
180+
Assert.assertFalse(tryAcquire(manager, waitingOnFirstWorker));
181+
182+
manager.releaseTsFileParserMemory(
183+
firstWorker.pipeName,
184+
firstWorker.creationTime,
185+
firstWorker.dataRegionId,
186+
firstWorker.key);
187+
firstWorker.acquired = false;
188+
Assert.assertTrue(tryAcquire(manager, waitingOnFirstWorker));
189+
Assert.assertEquals(2, manager.getReservedTsFileParserCount());
190+
} finally {
191+
for (final Reservation reservation : testReservations) {
192+
manager.cancelTsFileParserMemoryReservation(
193+
reservation.pipeName,
194+
reservation.creationTime,
195+
reservation.dataRegionId,
196+
reservation.key);
197+
if (reservation.acquired) {
198+
manager.releaseTsFileParserMemory(
199+
reservation.pipeName,
200+
reservation.creationTime,
201+
reservation.dataRegionId,
202+
reservation.key);
203+
}
204+
}
205+
}
206+
Assert.assertEquals(0, manager.getReservedTsFileParserCount());
207+
Assert.assertEquals(0, manager.getWaitingTsFileParserCount());
208+
}
209+
146210
@Test
147211
public void testAdmissionWindowRespectsPipeFairnessAcrossRegions() {
148212
commonConfig.setPipeTsFileParserInFlightMaxNum(2);

0 commit comments

Comments
 (0)