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.Closeable;
21  import java.io.FileDescriptor;
22  import java.io.IOException;
23  import java.io.InterruptedIOException;
24  import java.net.SocketTimeoutException;
25  import java.util.LinkedList;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.concurrent.ConcurrentHashMap;
29  import java.util.concurrent.ExecutionException;
30  import java.util.concurrent.locks.LockSupport;
31  
32  import org.eclipse.jdt.annotation.Nullable;
33  import org.newsclub.net.unix.AFSelector.PollFd;
34  
35  /**
36   * "Naive" implementation of {@link VirtualThreadPoller}, using
37   * {@link NativeUnixSocket#poll(PollFd, int)} on non-virtual threads.
38   *
39   * @author Christian Kohlschütter
40   */
41  final class VirtualThreadPollerNaive implements VirtualThreadPoller {
42    private static final int POLL_INTERVAL_MILLIS = 1_000; // should remain at 1 second to simplify
43                                                           // socket timeout handling
44  
45    private static final Map<FileDescriptor, PollJob> POLL_JOBS = new ConcurrentHashMap<>();
46  
47    private static final InterruptedIOException POLL_INTERRUPTED_SENTINEL =
48        new InterruptedIOException();
49  
50    private static final String PROP_POLLJOB_EXECUTOR =
51        "org.newsclub.net.unix.VirtualThreadPoller.use-common-pool";
52  
53    // With Java 25 and above, using the commonPool may cause a deadlock
54    // see https://github.com/kohlschutter/junixsocket/issues/172
55    private static final java.util.concurrent.ExecutorService POLLJOB_EXECUTOR = //
56        Boolean.parseBoolean(System.getProperty(PROP_POLLJOB_EXECUTOR, "false")) ? //
57            ThreadUtil.commonPool() : ThreadUtil.newWorkStealingPool();
58  
59    private static final class PollJob {
60      private final List<Thread> waitingThreads = new LinkedList<>();
61      private final FileDescriptor fd;
62      private final int mode;
63      private final long now;
64      private final AFSupplier<Integer> timeout;
65  
66      PollJob(FileDescriptor fd, int mode, long now, AFSupplier<Integer> timeout) {
67        this.fd = fd;
68        this.mode = mode;
69        this.now = now;
70        this.timeout = timeout;
71      }
72  
73      @SuppressWarnings("PMD.CognitiveComplexity")
74      AFFuture<@Nullable IOException> trigger(Thread waitingThread) {
75        synchronized (fd) {
76          waitingThreads.add(waitingThread);
77        }
78        return AFFuture.supplyAsync(() -> {
79          try {
80            Thread thread = Thread.currentThread();
81            PollFd pfd = new PollFd(new FileDescriptor[] {fd}, new int[] {mode});
82            do {
83              if (thread.isInterrupted() || !fd.valid()) {
84                return POLL_INTERRUPTED_SENTINEL;
85              }
86              try {
87                NativeUnixSocket.poll(pfd, POLL_INTERVAL_MILLIS);
88              } catch (IOException e) {
89                return e;
90              }
91              if (thread.isInterrupted() || !fd.valid()) {
92                return POLL_INTERRUPTED_SENTINEL;
93              }
94              if (pfd.rops[0] != 0) {
95                break;
96              }
97  
98              int timeoutMillis = timeout.get();
99              if (timeoutMillis > 0) {
100               if ((System.currentTimeMillis() - now) >= timeoutMillis) {
101                 // handle in calling thread
102                 break;
103               }
104             }
105           } while (true); // NOPMD.WhileLoopWithLiteralBoolean
106         } finally {
107           Thread threadToWake = null;
108           try {
109             synchronized (fd) {
110               threadToWake = waitingThreads.remove(0);
111               if (waitingThreads.isEmpty()) {
112                 POLL_JOBS.remove(fd);
113               }
114             }
115           } finally {
116             if (threadToWake != null) {
117               LockSupport.unpark(threadToWake);
118             }
119           }
120         }
121 
122         return null;
123       }, POLLJOB_EXECUTOR)::get;
124     }
125   }
126 
127   @Override
128   public void parkThreadUntilReady(FileDescriptor fd, int mode, long now,
129       AFSupplier<Integer> timeout, Closeable closeOnInterrupt) throws IOException {
130     Thread virtualThread = Thread.currentThread();
131 
132     PollJob job = Java7Util.computeIfAbsent(POLL_JOBS, fd, (k) -> new PollJob(fd, mode, now,
133         timeout));
134     AFFuture<@Nullable IOException> future = job.trigger(virtualThread);
135 
136     LockSupport.park();
137     if (virtualThread.isInterrupted()) {
138       throw SocketClosedByInterruptException.newInstanceAndClose(closeOnInterrupt);
139     }
140 
141     try {
142       IOException ex = future.get();
143       if (ex != null) {
144         if (ex == POLL_INTERRUPTED_SENTINEL) {
145           throw SocketClosedByInterruptException.newInstanceAndClose(closeOnInterrupt);
146         }
147         throw ex;
148       }
149     } catch (InterruptedException | ExecutionException e) {
150       throw SocketClosedByInterruptException.newInstanceAndClose(closeOnInterrupt); // NOPMD.PreserveStackTrace
151     }
152 
153     int timeoutMillis = timeout.get();
154     if (timeoutMillis > 0) {
155       if ((System.currentTimeMillis() - now) >= timeoutMillis) {
156         throw new SocketTimeoutException();
157       }
158     }
159   }
160 }