View Javadoc
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 }