Skip to content

Commit d6103ba

Browse files
authored
Add customizable graceful shutdown options for Client's EventLoopGroups (#5813) (#6588)
**Motivation:** Netty's default 2-second graceful shutdown delay can be excessive for applications requiring frequent restarts. Customization allows users to reduce teardown time. **Modifications:** - Added APIs in `ClientFactoryBuilder` to set custom shutdown quiet period and timeout. - Introduced related configuration options in `ClientFactoryOptions`. - Adapted shutdown behavior to respect custom defaults or explicit user configurations. - Introduced `EventLoopGroupBuilder` for creating Netty `EventLoopGroup`s with custom graceful shutdown parameters (quiet period and timeout). - Added `EventLoopGroups.builder()` to provide a fluent API for `EventLoopGroup` configuration. - Added `ShutdownConfigurableEventLoopGroup` and `DelegatingEventLoopGroup` to apply pre-configured shutdown delays when `shutdownGracefully()` is called. - Added `ServerBuilder.bossGroupFactory()` to allow customization of boss `EventLoopGroup`s and their shutdown behavior. **Result:** - Clients can now configure graceful shutdown delays, enhancing flexibility for applications with frequent lifecycle changes. - Users can now minimize `Server` and `ClientFactory` shutdown time by customizing the graceful shutdown quiet period and timeout for `EventLoopGroup`s. - Improved flexibility and responsiveness for applications with frequent start/stop cycles. This pull request combines two previously discussed and verified pull requests: - #6528 - #6586 Multiple separated commits from previous PRs were squashed to two clean commits and conflicts with `main` branch has been resolved. Changes were pre-approved by @ikhoon and @jrhee17
1 parent d34863b commit d6103ba

File tree

12 files changed

+832
-414
lines changed

12 files changed

+832
-414
lines changed
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
* Copyright 2026 LINE Corporation
3+
*
4+
* LINE Corporation 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+
17+
package com.linecorp.armeria.common.util;
18+
19+
import static java.util.Objects.requireNonNull;
20+
21+
import java.util.Collection;
22+
import java.util.Iterator;
23+
import java.util.List;
24+
import java.util.concurrent.Callable;
25+
import java.util.concurrent.ExecutionException;
26+
import java.util.concurrent.TimeUnit;
27+
import java.util.concurrent.TimeoutException;
28+
29+
import io.netty.channel.Channel;
30+
import io.netty.channel.ChannelFuture;
31+
import io.netty.channel.ChannelPromise;
32+
import io.netty.channel.EventLoop;
33+
import io.netty.channel.EventLoopGroup;
34+
import io.netty.util.concurrent.EventExecutor;
35+
import io.netty.util.concurrent.Future;
36+
import io.netty.util.concurrent.ScheduledFuture;
37+
38+
/**
39+
* A delegating {@link EventLoopGroup} that forwards all calls to the underlying delegate.
40+
*/
41+
class DelegatingEventLoopGroup implements EventLoopGroup {
42+
43+
private final EventLoopGroup delegate;
44+
45+
/**
46+
* Creates a new instance.
47+
*
48+
* @param delegate the {@link EventLoopGroup} to delegate to
49+
*/
50+
DelegatingEventLoopGroup(EventLoopGroup delegate) {
51+
this.delegate = requireNonNull(delegate, "delegate");
52+
}
53+
54+
/**
55+
* Returns the underlying {@link EventLoopGroup}.
56+
*/
57+
protected EventLoopGroup delegate() {
58+
return delegate;
59+
}
60+
61+
@Override
62+
public EventLoop next() {
63+
return delegate.next();
64+
}
65+
66+
@Override
67+
public Iterator<EventExecutor> iterator() {
68+
return delegate.iterator();
69+
}
70+
71+
@Override
72+
public ChannelFuture register(Channel channel) {
73+
return delegate.register(channel);
74+
}
75+
76+
@Override
77+
public ChannelFuture register(ChannelPromise promise) {
78+
return delegate.register(promise);
79+
}
80+
81+
@Deprecated
82+
@Override
83+
public ChannelFuture register(Channel channel, ChannelPromise promise) {
84+
return delegate.register(channel, promise);
85+
}
86+
87+
@Override
88+
public boolean isShuttingDown() {
89+
return delegate.isShuttingDown();
90+
}
91+
92+
@Override
93+
public Future<?> shutdownGracefully() {
94+
return delegate.shutdownGracefully();
95+
}
96+
97+
@Override
98+
public Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
99+
return delegate.shutdownGracefully(quietPeriod, timeout, unit);
100+
}
101+
102+
@Override
103+
public Future<?> terminationFuture() {
104+
return delegate.terminationFuture();
105+
}
106+
107+
@Deprecated
108+
@Override
109+
public void shutdown() {
110+
delegate.shutdown();
111+
}
112+
113+
@Deprecated
114+
@Override
115+
public List<Runnable> shutdownNow() {
116+
return delegate.shutdownNow();
117+
}
118+
119+
@Override
120+
public boolean isShutdown() {
121+
return delegate.isShutdown();
122+
}
123+
124+
@Override
125+
public boolean isTerminated() {
126+
return delegate.isTerminated();
127+
}
128+
129+
@Override
130+
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
131+
return delegate.awaitTermination(timeout, unit);
132+
}
133+
134+
@Override
135+
public <T> Future<T> submit(Callable<T> task) {
136+
return delegate.submit(task);
137+
}
138+
139+
@Override
140+
public <T> Future<T> submit(Runnable task, T result) {
141+
return delegate.submit(task, result);
142+
}
143+
144+
@Override
145+
public Future<?> submit(Runnable task) {
146+
return delegate.submit(task);
147+
}
148+
149+
@Override
150+
public <T> List<java.util.concurrent.Future<T>> invokeAll(
151+
Collection<? extends Callable<T>> tasks) throws InterruptedException {
152+
return delegate.invokeAll(tasks);
153+
}
154+
155+
@Override
156+
public <T> List<java.util.concurrent.Future<T>> invokeAll(
157+
Collection<? extends Callable<T>> tasks, long timeout,
158+
TimeUnit unit) throws InterruptedException {
159+
return delegate.invokeAll(tasks, timeout, unit);
160+
}
161+
162+
@Override
163+
public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
164+
throws InterruptedException, ExecutionException {
165+
return delegate.invokeAny(tasks);
166+
}
167+
168+
@Override
169+
public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
170+
throws InterruptedException, ExecutionException, TimeoutException {
171+
return delegate.invokeAny(tasks, timeout, unit);
172+
}
173+
174+
@Override
175+
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
176+
return delegate.schedule(command, delay, unit);
177+
}
178+
179+
@Override
180+
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
181+
return delegate.schedule(callable, delay, unit);
182+
}
183+
184+
@Override
185+
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay,
186+
long period, TimeUnit unit) {
187+
return delegate.scheduleAtFixedRate(command, initialDelay, period, unit);
188+
}
189+
190+
@Override
191+
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay,
192+
long delay, TimeUnit unit) {
193+
return delegate.scheduleWithFixedDelay(command, initialDelay, delay, unit);
194+
}
195+
196+
@Override
197+
public void execute(Runnable command) {
198+
delegate.execute(command);
199+
}
200+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright 2026 LINE Corporation
3+
*
4+
* LINE Corporation 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+
17+
package com.linecorp.armeria.common.util;
18+
19+
import static com.google.common.base.Preconditions.checkArgument;
20+
import static java.util.Objects.requireNonNull;
21+
22+
import java.time.Duration;
23+
import java.util.concurrent.ThreadFactory;
24+
25+
import com.linecorp.armeria.common.Flags;
26+
import com.linecorp.armeria.common.annotation.Nullable;
27+
import com.linecorp.armeria.common.annotation.UnstableApi;
28+
29+
import io.netty.channel.EventLoopGroup;
30+
31+
/**
32+
* A builder for creating an {@link EventLoopGroup} with custom configuration.
33+
*
34+
* <h2>Example</h2>
35+
* <pre>{@code
36+
* EventLoopGroup eventLoopGroup = EventLoopGroups
37+
* .builder()
38+
* .numThreads(4)
39+
* .threadFactory(ThreadFactories.newThreadFactory("my-eventloop", true))
40+
* .gracefulShutdown(Duration.ofSeconds(2), Duration.ofSeconds(15))
41+
* .build();
42+
* }</pre>
43+
*
44+
* @see EventLoopGroups#builder()
45+
*/
46+
@UnstableApi
47+
public final class EventLoopGroupBuilder {
48+
49+
public static final String DEFAULT_ARMERIA_THREAD_NAME_PREFIX = "armeria-eventloop";
50+
51+
// Netty defaults from AbstractEventExecutorGroup.shutdownGracefully()
52+
private static final long DEFAULT_SHUTDOWN_QUIET_PERIOD_MILLIS = 2000;
53+
private static final long DEFAULT_SHUTDOWN_TIMEOUT_MILLIS = 15000;
54+
55+
private int numThreads = Flags.numCommonWorkers();
56+
@Nullable
57+
private ThreadFactory threadFactory;
58+
private long shutdownQuietPeriodMillis = DEFAULT_SHUTDOWN_QUIET_PERIOD_MILLIS;
59+
private long shutdownTimeoutMillis = DEFAULT_SHUTDOWN_TIMEOUT_MILLIS;
60+
61+
EventLoopGroupBuilder() {}
62+
63+
/**
64+
* Sets the number of event loop threads. The default is {@link Flags#numCommonWorkers()},
65+
* which is typically the number of available processors multiplied by 2.
66+
*
67+
* @param numThreads the number of threads (must be greater than 0)
68+
*/
69+
public EventLoopGroupBuilder numThreads(int numThreads) {
70+
checkArgument(numThreads > 0, "numThreads: %s (expected: > 0)", numThreads);
71+
this.numThreads = numThreads;
72+
return this;
73+
}
74+
75+
/**
76+
* Sets a custom {@link ThreadFactory} for creating event loop threads.
77+
*
78+
* @param threadFactory the thread factory to use
79+
* @see ThreadFactories for convenient factory methods to create a {@link ThreadFactory}
80+
*/
81+
public EventLoopGroupBuilder threadFactory(ThreadFactory threadFactory) {
82+
this.threadFactory = requireNonNull(threadFactory, "threadFactory");
83+
return this;
84+
}
85+
86+
/**
87+
* Sets the graceful shutdown quiet period and timeout.
88+
* The quiet period is the amount of time the executor will wait for new tasks before
89+
* starting to shut down. The timeout is the maximum amount of time to wait for the
90+
* executor to terminate.
91+
*
92+
* <p>The default values are 2 seconds for quiet period and 15 seconds for timeout,
93+
* which are Netty's default values.
94+
*
95+
* @param quietPeriod the quiet period during which the executor will wait for new tasks
96+
* @param timeout the maximum time to wait for the executor to terminate
97+
*/
98+
public EventLoopGroupBuilder gracefulShutdown(Duration quietPeriod, Duration timeout) {
99+
requireNonNull(quietPeriod, "quietPeriod");
100+
requireNonNull(timeout, "timeout");
101+
return gracefulShutdownMillis(quietPeriod.toMillis(), timeout.toMillis());
102+
}
103+
104+
/**
105+
* Sets the graceful shutdown quiet period and timeout in milliseconds.
106+
* The quiet period is the amount of time the executor will wait for new tasks before
107+
* starting to shut down. The timeout is the maximum amount of time to wait for the
108+
* executor to terminate.
109+
*
110+
* <p>The default values are 2000ms for quiet period and 15000ms for timeout,
111+
* which are Netty's default values.
112+
*
113+
* @param quietPeriodMillis the quiet period in milliseconds (must be &gt;= 0)
114+
* @param timeoutMillis the timeout in milliseconds (must be &gt;= quietPeriodMillis)
115+
*/
116+
public EventLoopGroupBuilder gracefulShutdownMillis(long quietPeriodMillis, long timeoutMillis) {
117+
checkArgument(quietPeriodMillis >= 0,
118+
"quietPeriodMillis: %s (expected: >= 0)", quietPeriodMillis);
119+
checkArgument(timeoutMillis >= quietPeriodMillis,
120+
"timeoutMillis: %s (expected: >= quietPeriodMillis)", timeoutMillis);
121+
shutdownQuietPeriodMillis = quietPeriodMillis;
122+
shutdownTimeoutMillis = timeoutMillis;
123+
return this;
124+
}
125+
126+
/**
127+
* Returns a newly-created {@link EventLoopGroup} with the properties set on this builder.
128+
* If graceful shutdown parameters have been configured, the returned {@link EventLoopGroup}
129+
* will use those parameters when {@link EventLoopGroup#shutdownGracefully()} is called.
130+
*/
131+
public EventLoopGroup build() {
132+
final TransportType type = Flags.transportType();
133+
final ThreadFactory factory;
134+
if (threadFactory != null) {
135+
factory = threadFactory;
136+
} else {
137+
final String prefix = DEFAULT_ARMERIA_THREAD_NAME_PREFIX + '-' + type.lowerCasedName();
138+
factory = ThreadFactories.newEventLoopThreadFactory(prefix, false);
139+
}
140+
141+
final EventLoopGroup eventLoopGroup = type.newEventLoopGroup(numThreads, unused -> factory);
142+
143+
// Wrap with shutdown configuration if non-default values are used
144+
if (shutdownQuietPeriodMillis != DEFAULT_SHUTDOWN_QUIET_PERIOD_MILLIS ||
145+
shutdownTimeoutMillis != DEFAULT_SHUTDOWN_TIMEOUT_MILLIS) {
146+
return new ShutdownConfigurableEventLoopGroup(
147+
eventLoopGroup, shutdownQuietPeriodMillis, shutdownTimeoutMillis);
148+
}
149+
return eventLoopGroup;
150+
}
151+
}

0 commit comments

Comments
 (0)