View Javadoc
1   /*
2    * junixsocket
3    *
4    * Copyright 2009-2026 Christian Kohlschütter
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.newsclub.net.unix;
19  
20  import java.io.FileDescriptor;
21  import java.io.IOException;
22  import java.net.SocketAddress;
23  import java.net.SocketException;
24  import java.net.SocketTimeoutException;
25  import java.nio.ByteBuffer;
26  import java.nio.channels.AsynchronousCloseException;
27  import java.nio.channels.ClosedByInterruptException;
28  import java.nio.channels.ClosedChannelException;
29  import java.nio.channels.SelectionKey;
30  import java.util.Objects;
31  import java.util.concurrent.atomic.AtomicBoolean;
32  import java.util.concurrent.atomic.AtomicInteger;
33  
34  import org.eclipse.jdt.annotation.NonNull;
35  import org.newsclub.net.unix.pool.MutableHolder;
36  import org.newsclub.net.unix.pool.ObjectPool;
37  import org.newsclub.net.unix.pool.ObjectPool.Lease;
38  
39  /**
40   * The core functionality of file descriptor based I/O.
41   *
42   * @author Christian Kohlschütter
43   */
44  class AFCore extends CleanableState {
45    private static final ObjectPool<MutableHolder<ByteBuffer>> TL_BUFFER = ObjectPool
46        .newThreadLocalPool(() -> {
47          return new MutableHolder<>(null);
48        }, (o) -> {
49          ByteBuffer bb = o.get();
50          if (bb != null) {
51            bb.clear();
52          }
53          return true;
54        });
55  
56    private static final String PROP_TL_BUFFER_MAX_CAPACITY =
57        "org.newsclub.net.unix.thread-local-buffer.max-capacity"; // 0 means "no limit" (discouraged)
58  
59    private static final int TL_BUFFER_MIN_CAPACITY = 8192; // 8 kb per thread
60    private static final int TL_BUFFER_MAX_CAPACITY = Integer.parseInt(System.getProperty(
61        PROP_TL_BUFFER_MAX_CAPACITY, Integer.toString(1 * 1024 * 1024))); // 1 MB per thread
62  
63    private final AtomicBoolean closed = new AtomicBoolean(false);
64  
65    final FileDescriptor fd;
66    final AncillaryDataSupport ancillaryDataSupport;
67  
68    private final boolean datagramMode;
69  
70    private final AtomicInteger virtualBlockingLeases = new AtomicInteger(0);
71    private volatile boolean blocking = true;
72    private final AtomicBoolean cleanFd = new AtomicBoolean(true);
73  
74    AFCore(Object observed, FileDescriptor fd, AncillaryDataSupport ancillaryDataSupport,
75        boolean datagramMode) {
76      super(observed);
77      this.datagramMode = datagramMode;
78      this.ancillaryDataSupport = ancillaryDataSupport;
79  
80      this.fd = fd == null ? new FileDescriptor() : fd;
81    }
82  
83    AFCore(Object observed, FileDescriptor fd) {
84      this(observed, fd, null, false);
85    }
86  
87    @Override
88    protected final void doClean() {
89      if (fd != null && fd.valid() && cleanFd.get()) {
90        try {
91          doClose();
92        } catch (IOException e) {
93          // ignore
94        }
95      }
96      if (ancillaryDataSupport != null) {
97        ancillaryDataSupport.close();
98      }
99    }
100 
101   void disableCleanFd() {
102     this.cleanFd.set(false);
103   }
104 
105   boolean isClosed() {
106     return closed.get();
107   }
108 
109   void doClose() throws IOException {
110     if (closed.compareAndSet(false, true)) {
111       NativeUnixSocket.close(fd);
112     }
113   }
114 
115   FileDescriptor validFdOrException() throws SocketException {
116     FileDescriptor fdesc = validFd();
117     if (fdesc == null) {
118       closed.set(true);
119       throw new SocketClosedException("Not open");
120     }
121     return fdesc;
122   }
123 
124   synchronized FileDescriptor validFd() {
125     if (isClosed()) {
126       return null;
127     }
128     FileDescriptor descriptor = this.fd;
129     if (descriptor != null) {
130       if (descriptor.valid()) {
131         return descriptor;
132       }
133     }
134     return null;
135   }
136 
137   int read(ByteBuffer dst, AFSupplier<Integer> timeout) throws IOException {
138     return read(dst, timeout, null, 0);
139   }
140 
141   @SuppressWarnings({
142       "PMD.NcssCount", "PMD.CognitiveComplexity", "PMD.CyclomaticComplexity",
143       "PMD.VariableDeclarationUsageDistance"})
144   int read(ByteBuffer dst, AFSupplier<Integer> timeout, ByteBuffer socketAddressBuffer, int options)
145       throws IOException {
146     int remaining = dst.remaining();
147     if (remaining == 0) {
148       return 0;
149     }
150     FileDescriptor fdesc = validFdOrException();
151 
152     int dstPos = dst.position();
153 
154     ByteBuffer buf;
155     int pos;
156 
157     boolean direct = dst.isDirect();
158 
159     final boolean virtualBlocking = (ThreadUtil.isVirtualThread() && isBlocking())
160         || isVirtualBlocking();
161     final long now;
162     if (virtualBlocking) {
163       now = System.currentTimeMillis();
164     } else {
165       now = 0;
166     }
167     if (virtualBlocking || !blocking) {
168       options |= NativeUnixSocket.OPT_NON_BLOCKING;
169     }
170 
171     boolean park = false;
172 
173     int count;
174     virtualThreadLoop : do {
175       if (virtualBlocking) {
176         if (park) {
177           VirtualThreadPoller.INSTANCE.parkThreadUntilReady(fdesc, SelectionKey.OP_WRITE, now,
178               timeout, this::close);
179         }
180         configureVirtualBlocking(true);
181       }
182 
183       try (Lease<MutableHolder<ByteBuffer>> lease = direct ? null : getPrivateDirectByteBuffer(
184           remaining)) {
185         if (direct) {
186           buf = dst;
187           pos = dstPos;
188         } else {
189           buf = Objects.requireNonNull(Objects.requireNonNull(lease).get().get());
190           remaining = Math.min(remaining, buf.remaining());
191           pos = buf.position();
192           buf.limit(pos + remaining);
193         }
194 
195         try {
196           count = NativeUnixSocket.receive(fdesc, buf, pos, remaining, socketAddressBuffer, options,
197               ancillaryDataSupport, 0);
198           if (count == 0 && virtualBlocking) {
199             // try again
200             park = true;
201             continue virtualThreadLoop;
202           }
203         } catch (AsynchronousCloseException e) {
204           throw e;
205         } catch (ClosedChannelException e) {
206           if (isClosed()) {
207             throw e;
208           } else if (Thread.currentThread().isInterrupted()) {
209             throw (ClosedByInterruptException) new ClosedByInterruptException().initCause(e);
210           } else {
211             throw (AsynchronousCloseException) new AsynchronousCloseException().initCause(e);
212           }
213         } catch (SocketTimeoutException e) {
214           if (virtualBlocking) {
215             // try again
216             park = true;
217             continue virtualThreadLoop;
218           } else {
219             throw e;
220           }
221         }
222 
223         if (count == -1 || buf == null) {
224           return -1;
225         }
226 
227         if (direct) {
228           if (count < 0) {
229             throw new IllegalStateException();
230           }
231           dst.position(pos + count);
232         } else {
233           int oldLimit = buf.limit();
234           if (count < oldLimit) {
235             buf.limit(count);
236           }
237           try {
238             while (buf.hasRemaining()) {
239               dst.put(buf);
240             }
241           } finally {
242             if (count < oldLimit) {
243               buf.limit(oldLimit);
244             }
245           }
246         }
247       } finally {
248         if (virtualBlocking) {
249           configureVirtualBlocking(false);
250         }
251       }
252       break; // NOPMD.AvoidBranchingStatementAsLastInLoop virtualThreadLoop
253     } while (true); // NOPMD.WhileLoopWithLiteralBoolean
254 
255     return count;
256   }
257 
258   int write(ByteBuffer src, AFSupplier<Integer> timeout) throws IOException {
259     return write(src, timeout, null, 0);
260   }
261 
262   @SuppressWarnings({
263       "PMD.NcssCount", "PMD.CognitiveComplexity", "PMD.CyclomaticComplexity", "PMD.NPathComplexity",
264       "PMD.VariableDeclarationUsageDistance"})
265   int write(ByteBuffer src, AFSupplier<Integer> timeout, SocketAddress target, int options)
266       throws IOException {
267     int remaining = src.remaining();
268 
269     if (remaining == 0) {
270       return 0;
271     }
272 
273     FileDescriptor fdesc = validFdOrException();
274     final ByteBuffer addressTo;
275     final int addressToLen;
276     try (Lease<ByteBuffer> addressToLease = target == null ? null
277         : AFSocketAddress.SOCKETADDRESS_BUFFER_TL.take()) {
278       if (addressToLease == null) {
279         addressTo = null;
280         addressToLen = 0;
281       } else {
282         addressTo = addressToLease.get();
283         addressToLen = AFSocketAddress.unwrapAddressDirectBufferInternal(addressTo, target);
284       }
285 
286       // accept "send buffer overflow" as packet loss
287       // and don't retry (which may slow things down quite a bit)
288 
289       int pos = src.position();
290       boolean isDirect = src.isDirect();
291       ByteBuffer buf;
292       int bufPos;
293 
294       final boolean virtualBlocking = (ThreadUtil.isVirtualThread() && isBlocking())
295           || isVirtualBlocking();
296       final long now;
297       if (virtualBlocking) {
298         now = System.currentTimeMillis();
299       } else {
300         now = 0;
301       }
302       if (virtualBlocking || !blocking) {
303         options |= NativeUnixSocket.OPT_NON_BLOCKING;
304       }
305       if (datagramMode) {
306         options |= NativeUnixSocket.OPT_DGRAM_MODE;
307       }
308 
309       int written;
310 
311       boolean park = false;
312       virtualThreadLoop : do {
313         if (virtualBlocking) {
314           if (park) {
315             VirtualThreadPoller.INSTANCE.parkThreadUntilReady(fdesc, SelectionKey.OP_WRITE, now,
316                 timeout, this::close);
317           }
318           configureVirtualBlocking(true);
319         }
320 
321         try (Lease<MutableHolder<ByteBuffer>> lease = isDirect ? null : getPrivateDirectByteBuffer(
322             remaining)) {
323           if (isDirect) {
324             buf = src;
325             bufPos = pos;
326           } else {
327             buf = Objects.requireNonNull(Objects.requireNonNull(lease).get().get());
328             remaining = Math.min(remaining, buf.remaining());
329 
330             bufPos = buf.position();
331 
332             while (src.hasRemaining() && buf.hasRemaining()) {
333               buf.put(src);
334             }
335 
336             buf.position(bufPos);
337           }
338 
339           written = NativeUnixSocket.send(fdesc, buf, bufPos, remaining, addressTo, addressToLen,
340               options, ancillaryDataSupport);
341           if (written == 0 && virtualBlocking) {
342             // try again
343             park = true;
344             continue virtualThreadLoop;
345           }
346         } catch (SocketTimeoutException e) {
347           if (virtualBlocking) {
348             // try again
349             park = true;
350             continue virtualThreadLoop;
351           } else {
352             throw e;
353           }
354         } finally {
355           if (virtualBlocking) {
356             configureVirtualBlocking(false);
357           }
358         }
359         break; // NOPMD.AvoidBranchingStatementAsLastInLoop virtualThreadLoop
360       } while (true); // NOPMD.WhileLoopWithLiteralBoolean
361       src.position(pos + written);
362       return written;
363     }
364   }
365 
366   /**
367    * Returns a per-thread reusable byte buffer for a given capacity.
368    *
369    * If a thread-local buffer currently uses a smaller capacity, the buffer is replaced by a larger
370    * one. If the capacity exceeds a configurable maximum, a new direct buffer is allocated but not
371    * cached (i.e., the previously cached one is kept but not immediately returned to the caller).
372    *
373    * @param capacity The desired capacity.
374    * @return A byte buffer satisfying the requested capacity.
375    */
376   @SuppressWarnings("null")
377   Lease<MutableHolder<@NonNull ByteBuffer>> getPrivateDirectByteBuffer(int capacity) {
378     if (capacity > TL_BUFFER_MAX_CAPACITY && TL_BUFFER_MAX_CAPACITY > 0) {
379       // Capacity exceeds configurable maximum limit;
380       // allocate but do not cache direct buffer.
381       // This may incur a performance penalty at the cost of correctness when using such capacities.
382       return ObjectPool.unpooledLease(new MutableHolder<>(ByteBuffer.allocateDirect(capacity)));
383     }
384     if (capacity < TL_BUFFER_MIN_CAPACITY) {
385       capacity = TL_BUFFER_MIN_CAPACITY;
386     }
387     Lease<MutableHolder<ByteBuffer>> lease = TL_BUFFER.take();
388     MutableHolder<ByteBuffer> holder = lease.get();
389     ByteBuffer buffer = holder.get();
390     if (buffer == null || capacity > buffer.capacity()) {
391       buffer = ByteBuffer.allocateDirect(capacity);
392       holder.set(buffer);
393     }
394     buffer.clear();
395     return lease;
396   }
397 
398   void implConfigureBlocking(boolean block) throws IOException {
399     this.blocking = block;
400     if (block && isVirtualBlocking()) {
401       // do not actually change it here, defer it to when the virtual blocking counter goes to 0
402     } else {
403       NativeUnixSocket.configureBlocking(validFdOrException(), block);
404     }
405   }
406 
407   /**
408    * Increments/decrements the "virtual blocking" counter (calls must be in pairs/balanced using
409    * try-finally blocks).
410    *
411    * @param enabled {@code true} if increment, {@code false} if decrement.
412    * @throws SocketException on error.
413    * @throws IOException on error, including count overflow/underflow.
414    */
415   void configureVirtualBlocking(boolean enabled) throws SocketException, IOException {
416     int v;
417     if (enabled) {
418       if ((v = this.virtualBlockingLeases.incrementAndGet()) >= 1 && blocking) {
419         NativeUnixSocket.configureBlocking(validFdOrException(), false);
420       }
421       if (v >= Integer.MAX_VALUE) {
422         throw new IOException("blocking overflow");
423       }
424     } else {
425       if ((v = this.virtualBlockingLeases.decrementAndGet()) == 0 && blocking) {
426         NativeUnixSocket.configureBlocking(validFdOrException(), true);
427       }
428       if (v < 0) {
429         throw new IOException("blocking underflow");
430       }
431     }
432   }
433 
434   boolean isVirtualBlocking() {
435     return virtualBlockingLeases.get() > 0;
436   }
437 
438   boolean isBlocking() {
439     return blocking;
440   }
441 }