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.ReferenceCountUtil;
19 import io.netty.util.ReferenceCounted;
20
21 /**
22 * A slot tracking the references a submitted SQE handed to the kernel. Recording does not retain: the outbound buffer
23 * owns them until the write completes or {@link #retainReferences()} takes over. Slots are reused and the backing
24 * array is kept across reuses, so a slot allocates only on its first use and when a write hands it more references
25 * than any earlier use did. {@link #abandon()} is how a slot is ended outside of a completion CQE -- see its
26 * javadoc for the three situations that call it.
27 */
28 final class WriteOperation {
29 private static final ReferenceCounted[] EMPTY_REFERENCES = new ReferenceCounted[0];
30
31 private byte opCode;
32 private ReferenceCounted[] references = EMPTY_REFERENCES;
33 private int count;
34 private boolean active;
35 private int retainedCount;
36
37 void record(byte opCode, ReferenceCounted reference) {
38 if (isActive()) {
39 throw new IllegalStateException("slot still owned by an operation that has not completed");
40 }
41 if (references.length < 1) {
42 references = new ReferenceCounted[1];
43 }
44 references[0] = reference;
45 count = 1;
46 this.opCode = opCode;
47 active = true;
48 retainedCount = 0;
49 }
50
51 /**
52 * Copies {@code references} so the caller may reuse the array: a zero-copy slot stays alive from its primary CQE
53 * until the follow-up {@code IORING_CQE_F_NOTIF}, during which a reused collector array would be overwritten.
54 */
55 void record(byte opCode, ReferenceCounted[] references, int count) {
56 if (isActive()) {
57 throw new IllegalStateException("slot still owned by an operation that has not completed");
58 }
59 if (this.references.length < count) {
60 this.references = new ReferenceCounted[count];
61 }
62 System.arraycopy(references, 0, this.references, 0, count);
63 this.count = count;
64 this.opCode = opCode;
65 active = true;
66 retainedCount = 0;
67 }
68
69 /**
70 * NOOP once every reference is retained or while the slot is inactive. {@code retainedCount} only advances
71 * past an index once its {@code retain()} call actually returns, so a {@code retain()} that throws partway
72 * leaves {@link #finish()} to release exactly the references that were retained, instead of over-releasing
73 * the ones that never were.
74 */
75 void retainReferences() {
76 if (!isActive() || retainedCount == count) {
77 return;
78 }
79 for (int i = retainedCount; i < count; i++) {
80 references[i].retain();
81 retainedCount = i + 1;
82 }
83 }
84
85 /**
86 * Whether this slot is occupied. A submitted SQE that ended up with zero references still occupies it.
87 */
88 boolean isActive() {
89 return active;
90 }
91
92 byte opCode() {
93 return opCode;
94 }
95
96 /**
97 * Releases the slot on the terminal CQE: a notification, or any completion without {@code IORING_CQE_F_MORE}.
98 */
99 void complete(int cqeFlags) {
100 if ((cqeFlags & Native.IORING_CQE_F_NOTIF) != 0 || (cqeFlags & Native.IORING_CQE_F_MORE) == 0) {
101 finish();
102 }
103 }
104
105 /**
106 * Ends this slot without it ever seeing a completion CQE. Called in three situations: (1) submission itself
107 * failed, so the kernel never saw the SQE and no CQE will ever arrive for it; (2) deregistration discards a
108 * slot whose completion this channel can no longer observe, and {@link #retainReferences()} never ran on it,
109 * making this call a plain discard; (3) deregistration discards a slot that {@link #retainReferences()} did
110 * retain before a shutdown, in which case this call is what actually releases those references.
111 */
112 void abandon() {
113 finish();
114 }
115
116 private void finish() {
117 if (!active) {
118 return;
119 }
120 active = false;
121 int releaseCount = retainedCount;
122 retainedCount = 0;
123 int finishedCount = count;
124 count = 0;
125 for (int i = 0; i < finishedCount; i++) {
126 if (i < releaseCount) {
127 // A completion, a failed submission and a deregistration all end up in this loop, so a reference
128 // that fails to release must not strand the ones after it or leave them reachable through the array.
129 ReferenceCountUtil.safeRelease(references[i]);
130 }
131 references[i] = null;
132 }
133 }
134 }