1 /*
2 * Copyright 2026 The Netty Project
3 *
4 * The Netty Project licenses this file to you under the Apache License,
5 * version 2.0 (the "License"); you may not use this file except in compliance
6 * with the License. You may obtain a copy of the License at:
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16 package io.netty.channel.uring;
17
18 import io.netty.util.ReferenceCounted;
19 import io.netty.util.collection.LongObjectHashMap;
20 import io.netty.util.internal.MathUtil;
21 import io.netty.util.internal.logging.InternalLogger;
22 import io.netty.util.internal.logging.InternalLoggerFactory;
23
24 import java.util.Arrays;
25
26 /**
27 * Owns every in-flight write operation a channel tracks, across the four namespaces a channel can have live at
28 * once: a pooled slot array (ids this class hands out itself), an overflow map (a long-id fallback once the
29 * pooled range is exhausted), a foreign slot array (ids an allocator this class does not own hands the caller,
30 * such as a {@link MsgHdrMemoryArray} index), and a single slot for the one non-zero-copy write a stream channel
31 * can have outstanding at a time. One instance per channel, created unconditionally in the channel constructor.
32 */
33 final class WriteOperationTracker {
34 private static final InternalLogger logger = InternalLoggerFactory.getInstance(WriteOperationTracker.class);
35
36 // Ids index the pooled array, so they run from 1 (0 is reserved for "no id") to Short.MAX_VALUE, the same
37 // bound scheduleWrite(...) already puts on the number of outstanding writes.
38 private static final int MAX_POOLED_ID = Short.MAX_VALUE;
39
40 // Ids issued by nextId()/nextZeroCopyId(). Dense, reused through freeIds, so allocating one never searches.
41 private WriteOperation[] pooled;
42 // Ids issued by an allocator this class does not own, such as the MsgHdrMemoryArray index the datagram
43 // sendmsg path submits directly. A separate array lets that namespace overlap the pooled one above.
44 private WriteOperation[] foreign;
45 private short[] freeIds;
46 private int freeIdCount;
47 private int issuedIds;
48
49 // Only ever holds values above MAX_POOLED_ID. Such a value does not survive a round-trip through short,
50 // so IoUringIoHandler.canUseFastPath(...) rejects it and the slow path preserves the full user_data. The
51 // counter only grows and never recycles, so a fallback id can never collide with a live one.
52 private long nextOverflowId = ((long) MAX_POOLED_ID) + 1;
53 // Never allocated before the short pool runs dry, so the common path never touches a map.
54 private LongObjectHashMap<WriteOperation> overflow;
55
56 // The one non-zero-copy write a stream channel can have in flight. Reused by direct field access -- no id,
57 // no array, no opcode match -- because WRITE_SCHEDULED caps a stream channel to one outstanding write.
58 // Exposed through recordStream/completeStream/abandonStream/isStreamActive below, and touched directly by
59 // retainAll()/releaseAll() the same way those touch the pooled/foreign/overflow namespaces; the field itself
60 // never leaves this class.
61 private final WriteOperation single = new WriteOperation();
62
63 /**
64 * Returns a write-operation id that no in-flight write owns, or {@code 0} when every id is held by a write
65 * that has not seen its terminal CQE yet. Released ids come back through a free list, so this does not
66 * search.
67 */
68 short nextId() {
69 if (freeIdCount > 0) {
70 return freeIds[--freeIdCount];
71 }
72 if (issuedIds == MAX_POOLED_ID) {
73 return 0;
74 }
75 return (short) ++issuedIds;
76 }
77
78 /**
79 * Returns a write-operation id for a zero-copy write, falling back to a long id outside the short range once
80 * every short id is held by an in-flight write, so the write still gets submitted instead of being deferred.
81 * Never returns 0.
82 */
83 long nextZeroCopyId() {
84 short id = nextId();
85 if (id != 0) {
86 return id;
87 }
88 return nextOverflowId++;
89 }
90
91 /**
92 * Registers a write whose id came from {@link #nextId()} or {@link #nextZeroCopyId()}. A pooled id goes back
93 * to the free list once the terminal CQE arrives; an overflow id is never reused, its map entry is simply
94 * dropped.
95 */
96 void record(long id, byte opCode, ReferenceCounted reference) {
97 if (id > MAX_POOLED_ID) {
98 recordOverflow(id, opCode, reference);
99 return;
100 }
101 short slot = (short) id;
102 WriteOperation[] grown = ensureCapacity(pooled, slot);
103 if (grown != pooled) {
104 pooled = grown;
105 }
106 slot(pooled, slot).record(opCode, reference);
107 }
108
109 // Split out of record(...) above so the overflow path -- taken only once every pooled id is in flight --
110 // does not add to the bytecode size of the hot method and keep it eligible for inlining.
111 private void recordOverflow(long id, byte opCode, ReferenceCounted reference) {
112 overflowSlot(id).record(opCode, reference);
113 }
114
115 /**
116 * Registers a write of multiple references whose id came from {@link #nextId()} or {@link #nextZeroCopyId()}.
117 * Copies {@code references}, so the caller may reuse the array.
118 */
119 void record(long id, byte opCode, ReferenceCounted[] references, int count) {
120 if (id > MAX_POOLED_ID) {
121 recordOverflow(id, opCode, references, count);
122 return;
123 }
124 short slot = (short) id;
125 WriteOperation[] grown = ensureCapacity(pooled, slot);
126 if (grown != pooled) {
127 pooled = grown;
128 }
129 slot(pooled, slot).record(opCode, references, count);
130 }
131
132 // Split out for the same reason as the single-reference overflow above: keep it out of the hot method.
133 private void recordOverflow(long id, byte opCode, ReferenceCounted[] references, int count) {
134 overflowSlot(id).record(opCode, references, count);
135 }
136
137 /**
138 * Registers a write whose id is owned by another allocator, such as the {@link MsgHdrMemoryArray} index used
139 * by the datagram sendmsg path. That id lives in its own slot array and never enters the free list.
140 */
141 void recordForeign(short id, byte opCode, ReferenceCounted reference) {
142 WriteOperation[] grown = ensureCapacity(foreign, id);
143 if (grown != foreign) {
144 foreign = grown;
145 }
146 slot(foreign, id).record(opCode, reference);
147 }
148
149 /**
150 * Ends the slot identified by {@code id}/{@code opCode} without it seeing a completion CQE: the submission
151 * itself failed, so the kernel never saw the SQE and no CQE will ever arrive for it. A pooled id goes back to
152 * the free list, an overflow entry is dropped. Deregistration ends its slots through {@link #releaseAll()}
153 * instead.
154 */
155 void abandon(long id, byte opCode) {
156 if (id > MAX_POOLED_ID) {
157 abandonOverflow(id, opCode);
158 return;
159 }
160 short slot = (short) id;
161 WriteOperation op = matching(pooled, slot, opCode);
162 if (op != null) {
163 op.abandon();
164 recycleId(slot);
165 return;
166 }
167 op = matching(foreign, slot, opCode);
168 if (op != null) {
169 op.abandon();
170 }
171 }
172
173 // Split out of abandon(...) above to keep the overflow path -- the rare case where every pooled id is
174 // in flight -- out of the hot method's bytecode.
175 private void abandonOverflow(long id, byte opCode) {
176 WriteOperation op = matchingOverflow(id, opCode);
177 if (op != null) {
178 op.abandon();
179 // Overflow ids are never recycled, so the entry has to go or the map grows without bound.
180 overflow.remove(id);
181 }
182 }
183
184 /**
185 * Applies a completion CQE to the slot identified by {@code id}/{@code opCode}. A terminated pooled slot's id
186 * is recycled back to the free list; a terminated overflow slot is removed from the map.
187 */
188 void complete(long id, byte opCode, int flags) {
189 if (id > MAX_POOLED_ID) {
190 completeOverflow(id, opCode, flags);
191 return;
192 }
193 short slot = (short) id;
194 WriteOperation op = matching(pooled, slot, opCode);
195 if (op != null) {
196 op.complete(flags);
197 if (!op.isActive()) {
198 recycleId(slot);
199 }
200 return;
201 }
202 op = matching(foreign, slot, opCode);
203 if (op != null) {
204 op.complete(flags);
205 }
206 }
207
208 // Split out of complete(...) above for the same reason as abandonOverflow(...): keep the rare overflow
209 // path out of the hot method's bytecode.
210 private void completeOverflow(long id, byte opCode, int flags) {
211 WriteOperation op = matchingOverflow(id, opCode);
212 if (op != null) {
213 op.complete(flags);
214 if (!op.isActive()) {
215 overflow.remove(id);
216 }
217 }
218 }
219
220 /**
221 * Retains the references held by the active slot identified by {@code id}/{@code opCode}, if any. Called from
222 * the zero-copy completion path, where {@code IORING_CQE_F_MORE} says the kernel still owns the memory until
223 * the follow-up {@code IORING_CQE_F_NOTIF}. The shutdown path uses {@link #retainAll()} instead.
224 */
225 void retainReferences(long id, byte opCode) {
226 if (id > MAX_POOLED_ID) {
227 retainOverflowReference(id, opCode);
228 return;
229 }
230 short slot = (short) id;
231 WriteOperation op = matching(pooled, slot, opCode);
232 if (op != null) {
233 op.retainReferences();
234 return;
235 }
236 op = matching(foreign, slot, opCode);
237 if (op != null) {
238 op.retainReferences();
239 }
240 }
241
242 // Split out of retainReferences(...) above for the same reason as abandonOverflow(...): keep the rare
243 // overflow path out of the hot method's bytecode. Named distinctly from retainOverflow() below, which
244 // retains every overflow slot instead of matching a single id/opCode pair.
245 private void retainOverflowReference(long id, byte opCode) {
246 WriteOperation op = matchingOverflow(id, opCode);
247 if (op != null) {
248 op.retainReferences();
249 }
250 }
251
252 /**
253 * The number of write operations parked in the overflow map. Test-only: no production caller, used to assert
254 * completions and abandons drop their entry.
255 */
256 int overflowCount() {
257 return overflow == null ? 0 : overflow.size();
258 }
259
260 /**
261 * Records a single reference on the non-zero-copy stream write slot. No id, no array, no opcode match.
262 */
263 void recordStream(byte opCode, ReferenceCounted reference) {
264 single.record(opCode, reference);
265 }
266
267 /**
268 * Records multiple references (e.g. writev) on the non-zero-copy stream write slot. Copies {@code references}.
269 */
270 void recordStream(byte opCode, ReferenceCounted[] references, int count) {
271 single.record(opCode, references, count);
272 }
273
274 /**
275 * Ends the stream slot without it ever seeing a completion CQE, for the same reason as
276 * {@link #abandon(long, byte)}: the submission itself failed. A no-op if the slot is inactive. There is no id,
277 * so there is no opcode match either -- the slot simply finishes. Deregistration ends this slot through
278 * {@link #releaseAll()} instead.
279 */
280 void abandonStream() {
281 if (single.isActive()) {
282 single.abandon();
283 }
284 }
285
286 /**
287 * Applies a completion CQE to the stream slot.
288 */
289 void completeStream(int flags) {
290 single.complete(flags);
291 }
292
293 /**
294 * Whether the stream slot is active. Test-only: no production caller, used to assert completions and abandons
295 * leave it inactive, same as {@link #overflowCount()}.
296 */
297 boolean isStreamActive() {
298 return single.isActive();
299 }
300
301 /**
302 * Retains every active slot's references across all four members (pooled, foreign, overflow, single) right
303 * before a shutdown, so a write completion that races the shutdown still finds a live reference to release
304 * instead of one the outbound buffer already dropped.
305 */
306 void retainAll() {
307 retainArray(pooled);
308 retainArray(foreign);
309 retainOverflow();
310 retainSingle();
311 }
312
313 /**
314 * Abandons every active slot across all four members and empties them, via {@link WriteOperation#abandon()}
315 * on each: a release for a slot {@link #retainAll()} retained, a plain discard for one it never reached.
316 * Named for the former, more consequential case -- the one this method exists to guard against -- rather
317 * than the latter, more common one. No further completion arrives once a channel is deregistered, so
318 * references a shutdown retained on a slot would otherwise leak forever.
319 */
320 void releaseAll() {
321 releaseArray(pooled);
322 releaseArray(foreign);
323 releaseOverflow();
324 if (single.isActive()) {
325 single.abandon();
326 }
327 }
328
329 private static WriteOperation[] ensureCapacity(WriteOperation[] operations, short id) {
330 if (operations == null) {
331 return new WriteOperation[Math.max(MathUtil.safeFindNextPositivePowerOfTwo(id + 1), 4)];
332 }
333 if (id >= operations.length) {
334 return Arrays.copyOf(operations, MathUtil.safeFindNextPositivePowerOfTwo(id + 1));
335 }
336 return operations;
337 }
338
339 private static WriteOperation slot(WriteOperation[] operations, short id) {
340 WriteOperation operation = operations[id];
341 if (operation == null) {
342 operation = new WriteOperation();
343 operations[id] = operation;
344 }
345 return operation;
346 }
347
348 private WriteOperation overflowSlot(long id) {
349 if (overflow == null) {
350 overflow = new LongObjectHashMap<WriteOperation>(2);
351 }
352 WriteOperation operation = overflow.get(id);
353 if (operation == null) {
354 operation = new WriteOperation();
355 overflow.put(id, operation);
356 }
357 return operation;
358 }
359
360 private WriteOperation matchingOverflow(long id, byte opCode) {
361 if (overflow == null) {
362 return null;
363 }
364 WriteOperation operation = overflow.get(id);
365 return operation != null && operation.isActive() && operation.opCode() == opCode ? operation : null;
366 }
367
368 private static WriteOperation matching(WriteOperation[] operations, short id, byte opCode) {
369 if (operations == null || id < 0 || id >= operations.length) {
370 return null;
371 }
372 WriteOperation operation = operations[id];
373 // Write user_data is not allocated from a single namespace. Every non-zero-copy stream write takes its id
374 // from AbstractIoUringChannel.nextOpsId(), and a splice picks a fixed id of its own to tell its two stages
375 // apart; both register in the single stream slot (see recordStream), not in this array, so their
376 // completions can land on a slot an unrelated zero-copy write occupies. Matching the opcode keeps such a
377 // completion from terminating what it finds.
378 return operation != null && operation.isActive() && operation.opCode() == opCode ? operation : null;
379 }
380
381 private void recycleId(short id) {
382 if (freeIds == null) {
383 freeIds = new short[8];
384 } else if (freeIdCount == freeIds.length) {
385 freeIds = Arrays.copyOf(freeIds, freeIdCount << 1);
386 }
387 freeIds[freeIdCount++] = id;
388 }
389
390 private static void retainArray(WriteOperation[] operations) {
391 if (operations == null) {
392 return;
393 }
394 for (WriteOperation operation : operations) {
395 if (operation == null) {
396 continue;
397 }
398 // One slot failing to retain must not stop the remaining slots from being retained.
399 try {
400 operation.retainReferences();
401 } catch (Throwable cause) {
402 logger.warn("Failed to retain in-flight write operation before shutdown", cause);
403 }
404 }
405 }
406
407 private void retainOverflow() {
408 if (overflow == null) {
409 return;
410 }
411 for (WriteOperation operation : overflow.values()) {
412 try {
413 operation.retainReferences();
414 } catch (Throwable cause) {
415 logger.warn("Failed to retain in-flight write operation before shutdown", cause);
416 }
417 }
418 }
419
420 // A failure here must not stop retainAll() from returning, the same guarantee retainArray(...) and
421 // retainOverflow() above give the array/map namespaces: doShutdownOutput() still has to reach
422 // doShutdownOutput0() -- the actual shutdown(2) -- even when this slot fails to retain.
423 private void retainSingle() {
424 try {
425 single.retainReferences();
426 } catch (Throwable cause) {
427 logger.warn("Failed to retain in-flight write operation before shutdown", cause);
428 }
429 }
430
431 private static void releaseArray(WriteOperation[] operations) {
432 if (operations == null) {
433 return;
434 }
435 for (int i = 0; i < operations.length; i++) {
436 WriteOperation operation = operations[i];
437 if (operation == null) {
438 continue;
439 }
440 if (operation.isActive()) {
441 operation.abandon();
442 }
443 operations[i] = null;
444 }
445 }
446
447 private void releaseOverflow() {
448 if (overflow == null) {
449 return;
450 }
451 for (WriteOperation operation : overflow.values()) {
452 if (operation.isActive()) {
453 operation.abandon();
454 }
455 }
456 overflow.clear();
457 }
458 }