1
1
/*
2
- * Copyright 2022 the original author or authors.
2
+ * Copyright 2022-2023 the original author or authors.
3
3
*
4
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
5
* you may not use this file except in compliance with the License.
26
26
import java .util .concurrent .ExecutorService ;
27
27
import java .util .concurrent .Executors ;
28
28
import java .util .concurrent .Future ;
29
+ import java .util .concurrent .RejectedExecutionException ;
29
30
import java .util .concurrent .TimeUnit ;
30
31
31
32
import org .postgresql .PGNotification ;
59
60
*
60
61
* @author Rafael Winterhalter
61
62
* @author Artem Bilan
63
+ * @author Igor Lovich
62
64
*
63
65
* @since 6.0
64
66
*/
@@ -75,15 +77,17 @@ public final class PostgresChannelMessageTableSubscriber implements SmartLifecyc
75
77
@ Nullable
76
78
private ExecutorService executor ;
77
79
78
- private CountDownLatch latch = new CountDownLatch ( 0 ) ;
80
+ private boolean userProvidedExecutor = false ;
79
81
80
- private Future <?> future = CompletableFuture . completedFuture ( null );
82
+ private CountDownLatch latch = new CountDownLatch ( 0 );
81
83
82
84
@ Nullable
83
85
private volatile PgConnection connection ;
86
+ private Future <?> future = CompletableFuture .completedFuture (null );
84
87
85
88
/**
86
89
* Create a new subscriber using the {@link JdbcChannelMessageStore#DEFAULT_TABLE_PREFIX}.
90
+ *
87
91
* @param connectionSupplier The connection supplier for the targeted Postgres database.
88
92
*/
89
93
public PostgresChannelMessageTableSubscriber (PgConnectionSupplier connectionSupplier ) {
@@ -92,7 +96,8 @@ public PostgresChannelMessageTableSubscriber(PgConnectionSupplier connectionSupp
92
96
93
97
/**
94
98
* Create a new subscriber.
95
- * @param tablePrefix The table prefix of the {@link JdbcChannelMessageStore} to subscribe to.
99
+ *
100
+ * @param tablePrefix The table prefix of the {@link JdbcChannelMessageStore} to subscribe to.
96
101
* @param connectionSupplier The connection supplier for the targeted Postgres database.
97
102
*/
98
103
public PostgresChannelMessageTableSubscriber (PgConnectionSupplier connectionSupplier , String tablePrefix ) {
@@ -106,14 +111,16 @@ public PostgresChannelMessageTableSubscriber(PgConnectionSupplier connectionSupp
106
111
* Define an executor to use for listening for new messages. Note that the Postgres SQL driver implements
107
112
* listening for notifications as a blocking operation which will permanently block a thread of this executor
108
113
* while running.
114
+ *
109
115
* @param executor The executor to use or {@code null} if an executor should be created by this class.
110
116
*/
111
117
public synchronized void setExecutor (@ Nullable ExecutorService executor ) {
112
118
this .executor = executor ;
113
119
}
114
120
115
121
/**
116
- * Add a new subscription to this subscriber.
122
+ * Add a new subscription to this subscriber.;
123
+ *
117
124
* @param subscription The subscription to register.
118
125
* @return {@code true} if the subscription was not already added.
119
126
*/
@@ -126,6 +133,7 @@ public boolean subscribe(Subscription subscription) {
126
133
127
134
/**
128
135
* Remove a previous subscription from this subscriber.
136
+ *
129
137
* @param subscription The subscription to remove.
130
138
* @return {@code true} if the subscription was previously registered and is now removed.
131
139
*/
@@ -143,12 +151,17 @@ public synchronized void start() {
143
151
ExecutorService executorToUse = this .executor ;
144
152
if (executorToUse == null ) {
145
153
CustomizableThreadFactory threadFactory =
146
- new CustomizableThreadFactory ("postgres-channel-message-table-subscriber -" );
154
+ new CustomizableThreadFactory ("postgres-channel-notifications -" );
147
155
threadFactory .setDaemon (true );
148
- executorToUse = Executors .newSingleThreadExecutor ( threadFactory );
156
+ executorToUse = Executors .newFixedThreadPool ( 2 , threadFactory );
149
157
this .executor = executorToUse ;
150
158
}
159
+ else {
160
+ this .userProvidedExecutor = true ;
161
+ }
151
162
this .latch = new CountDownLatch (1 );
163
+
164
+ CountDownLatch startingLatch = new CountDownLatch (1 );
152
165
this .future = executorToUse .submit (() -> {
153
166
try {
154
167
while (isActive ()) {
@@ -166,11 +179,13 @@ public synchronized void start() {
166
179
}
167
180
throw ex ;
168
181
}
169
- this .subscriptionsMap .values ()
170
- . forEach ( subscriptions -> subscriptions . forEach ( Subscription :: notifyUpdate ));
182
+ this .subscriptionsMap .values (). forEach ( this :: notifyAll );
183
+
171
184
try {
172
185
this .connection = conn ;
173
186
while (isActive ()) {
187
+ startingLatch .countDown ();
188
+
174
189
PGNotification [] notifications = conn .getNotifications (0 );
175
190
// Unfortunately, there is no good way of interrupting a notification
176
191
// poll but by closing its connection.
@@ -184,9 +199,7 @@ public synchronized void start() {
184
199
if (subscriptions == null ) {
185
200
continue ;
186
201
}
187
- for (Subscription subscription : subscriptions ) {
188
- subscription .notifyUpdate ();
189
- }
202
+ notifyAll (subscriptions );
190
203
}
191
204
}
192
205
}
@@ -208,6 +221,29 @@ public synchronized void start() {
208
221
this .latch .countDown ();
209
222
}
210
223
});
224
+
225
+ try {
226
+ if (!startingLatch .await (5 , TimeUnit .SECONDS )) {
227
+ throw new IllegalStateException ("Failed to start "
228
+ + PostgresChannelMessageTableSubscriber .class .getName ());
229
+ }
230
+ }
231
+ catch (InterruptedException e ) {
232
+ Thread .currentThread ().interrupt ();
233
+ throw new IllegalStateException ("Failed to start "
234
+ + PostgresChannelMessageTableSubscriber .class .getName (), e );
235
+ }
236
+ }
237
+
238
+ private void notifyAll (Set <Subscription > subscriptions ) {
239
+ subscriptions .forEach (it -> {
240
+ try {
241
+ this .executor .submit (it ::notifyUpdate );
242
+ }
243
+ catch (RejectedExecutionException e ) {
244
+ LOGGER .warn (e , "Executor rejected submission of notification task" );
245
+ }
246
+ });
211
247
}
212
248
213
249
private boolean isActive () {
@@ -232,6 +268,11 @@ public synchronized void stop() {
232
268
catch (SQLException ignored ) {
233
269
}
234
270
}
271
+
272
+ if (!this .userProvidedExecutor ) {
273
+ shutdownAndAwaitTermination (this .executor );
274
+ }
275
+
235
276
try {
236
277
if (!this .latch .await (5 , TimeUnit .SECONDS )) {
237
278
throw new IllegalStateException ("Failed to stop "
@@ -242,6 +283,35 @@ public synchronized void stop() {
242
283
}
243
284
}
244
285
286
+
287
+ /**
288
+ * Gracefully shutdown an executor service. Taken from @see ExecutorService javadoc
289
+ *
290
+ * @param pool The pool to shut down
291
+ */
292
+ private void shutdownAndAwaitTermination (@ Nullable ExecutorService pool ) {
293
+ if (pool == null ) {
294
+ return ;
295
+ }
296
+ pool .shutdown (); // Disable new tasks from being submitted
297
+ try {
298
+ // Wait a while for existing tasks to terminate
299
+ if (!pool .awaitTermination (2 , TimeUnit .SECONDS )) {
300
+ pool .shutdownNow (); // Cancel currently executing tasks
301
+ // Wait a while for tasks to respond to being cancelled
302
+ if (!pool .awaitTermination (2 , TimeUnit .SECONDS )) {
303
+ LOGGER .warn ("Unable to shutdown the executor service" );
304
+ }
305
+ }
306
+ }
307
+ catch (InterruptedException ie ) {
308
+ // (Re-)Cancel if current thread also interrupted
309
+ pool .shutdownNow ();
310
+ // Preserve interrupt status
311
+ Thread .currentThread ().interrupt ();
312
+ }
313
+ }
314
+
245
315
@ Override
246
316
public boolean isRunning () {
247
317
return this .latch .getCount () > 0 ;
@@ -269,12 +339,14 @@ public interface Subscription {
269
339
270
340
/**
271
341
* Return the region for which this subscription receives notifications.
342
+ *
272
343
* @return The relevant region of the {@link JdbcChannelMessageStore}.
273
344
*/
274
345
String getRegion ();
275
346
276
347
/**
277
348
* Return the group id for which this subscription receives notifications.
349
+ *
278
350
* @return The group id of the {@link PostgresSubscribableChannel}.
279
351
*/
280
352
Object getGroupId ();
0 commit comments