1+ /*
2+ *
3+ * Copyright [ 2020 - 2024 ] Matthew Buckton
4+ * Copyright [ 2024 - 2025 ] MapsMessaging B.V.
5+ *
6+ * Licensed under the Apache License, Version 2.0 with the Commons Clause
7+ * (the "License"); 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+ * https://commonsclause.com/
12+ *
13+ * Unless required by applicable law or agreed to in writing, software
14+ * distributed under the License is distributed on an "AS IS" BASIS,
15+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+ * See the License for the specific language governing permissions and
17+ * limitations under the License.
18+ */
19+
20+ package io .mapsmessaging .network .io .impl .serial .threads ;
21+
22+
23+ import java .util .Objects ;
24+ import java .util .concurrent .ConcurrentHashMap ;
25+ import java .util .concurrent .ConcurrentMap ;
26+ import java .util .concurrent .ExecutorService ;
27+ import java .util .concurrent .Executors ;
28+ import java .util .concurrent .ThreadFactory ;
29+ import java .util .concurrent .TimeUnit ;
30+ import java .util .concurrent .atomic .AtomicInteger ;
31+
32+ public final class SerialIoExecutors implements AutoCloseable {
33+
34+ private static final int SHUTDOWN_WAIT_SECONDS = 2 ;
35+
36+ private static final SerialIoExecutors INSTANCE = new SerialIoExecutors ();
37+
38+ private final ConcurrentMap <String , SerialIoPoolHandle > poolsByPortName ;
39+
40+ private SerialIoExecutors () {
41+ poolsByPortName = new ConcurrentHashMap <>();
42+ }
43+
44+ public static SerialIoExecutors getInstance () {
45+ return INSTANCE ;
46+ }
47+
48+ public SerialIoPoolHandle acquire (String portName ) {
49+ String canonicalPortName = canonicalizePortName (portName );
50+
51+ return poolsByPortName .computeIfAbsent (canonicalPortName , key -> {
52+ ThreadFactory readThreadFactory = new SerialThreadFactory ("SerialRead" , key );
53+ ThreadFactory writeThreadFactory = new SerialThreadFactory ("SerialWrite" , key );
54+
55+ ExecutorService readExecutor = Executors .newSingleThreadExecutor (readThreadFactory );
56+ ExecutorService writeExecutor = Executors .newSingleThreadExecutor (writeThreadFactory );
57+
58+ return new SerialIoPoolHandle (key , readExecutor , writeExecutor );
59+ });
60+ }
61+
62+ /**
63+ * In this simplified design, close is a no-op for individual handles.
64+ * We keep pools around for the lifetime of the process.
65+ */
66+ public void release (String portName ) {
67+ // Intentionally empty by design.
68+ }
69+
70+ @ Override
71+ public void close () {
72+ for (SerialIoPoolHandle handle : poolsByPortName .values ()) {
73+ shutdownExecutor (handle .getReadExecutor ());
74+ shutdownExecutor (handle .getWriteExecutor ());
75+ }
76+ poolsByPortName .clear ();
77+ }
78+
79+ private void shutdownExecutor (ExecutorService executorService ) {
80+ executorService .shutdown ();
81+ try {
82+ boolean terminated = executorService .awaitTermination (SHUTDOWN_WAIT_SECONDS , TimeUnit .SECONDS );
83+ if (!terminated ) {
84+ executorService .shutdownNow ();
85+ }
86+ } catch (InterruptedException interruptedException ) {
87+ executorService .shutdownNow ();
88+ Thread .currentThread ().interrupt ();
89+ }
90+ }
91+
92+ private String canonicalizePortName (String portName ) {
93+ Objects .requireNonNull (portName , "portName must not be null" );
94+ String trimmed = portName .trim ();
95+ if (trimmed .isEmpty ()) {
96+ throw new IllegalArgumentException ("portName must not be empty" );
97+ }
98+ return trimmed ;
99+ }
100+
101+ private static final class SerialThreadFactory implements ThreadFactory {
102+
103+ private final String prefix ;
104+ private final String portName ;
105+ private final AtomicInteger threadNumber ;
106+
107+ private SerialThreadFactory (String prefix , String portName ) {
108+ this .prefix = prefix ;
109+ this .portName = portName ;
110+ this .threadNumber = new AtomicInteger (1 );
111+ }
112+
113+ @ Override
114+ public Thread newThread (Runnable runnable ) {
115+ String name = prefix + "-" + portName + "-" + threadNumber .getAndIncrement ();
116+ Thread thread = new Thread (runnable , name );
117+ thread .setDaemon (true );
118+ return thread ;
119+ }
120+ }
121+ }
0 commit comments