In this article we will look at very simple basic example of Resilience4j bulkhead feature & look at runtime behavior of bulkhead. Here is the maven dependency for resilience4j-bulkhead required for this example.
Bulkhead Concept
Do not burden service with calls more than it can consume at a time. If call load is greater than service’s consumption at that time, then keep calls waiting for reasonable time until existing concurrent calls can finish & they can get turn. Otherwise just timeout the calls & go for alternate recovery path. This will avoid overloading of service & also give a graceful way to provide alternate recovery under heavy load.
Example in this article
Bulkhead Configurations: Maximum 5 concurrent calls at any given time. Keep other calls waiting for until one of the in-process 5 concurrent finishes or until maximum of 2 seconds.
- Create mock external service which takes 2 seconds to finish its processing.
- Create a service client which calls external service using bulkhead with above configurations.
- Mimic 20 parallel users/executions by calling service client in 20 threads.
- We will observe how bulkhead behaves for all threads.
Mock Service & client
Here is a mock service which takes 2 seconds to finish processing. Also a service client which decorates calls to external service using bulkhead. Bulkhead is configured to allow only 5 concurrent calls at a time. In case already 5 concurrent calls are in process, then it is configured to wait for maximum of 5 seconds.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
class ServiceCallerClient { private Bulkhead bulkhead; private ExternalConcurrentService externalConcurrentService = new ExternalConcurrentService(); public ServiceCallerClient() { /* * Create bulk head of 5 max concurrent calls with 2 seconds wait time for * entering bulkhead */ BulkheadConfig config = BulkheadConfig.custom().maxConcurrentCalls(5).maxWaitDuration(Duration.ofMillis(5000)) .build(); BulkheadRegistry registry = BulkheadRegistry.of(config); bulkhead = registry.bulkhead("externalConcurrentService"); } public void callService() { // Wrap service call in bulkhead & call service. Runnable runnable = () -> externalConcurrentService.callService(); bulkhead.executeRunnable(runnable); } } class ExternalConcurrentService { public void callService() { try { // Mock processing time of 2 seconds. Thread.sleep(2000); System.out.println(LocalTime.now() + " Call processing finished = " + Thread.currentThread().getName()); } catch (Exception e) { e.printStackTrace(); } } } |
Bulkhead in action
Here we will call service client in 20 parallel threads which might mimic 20 parallel users or 20 parallel executions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class BulkheadBasics { public static void main(String[] args) throws InterruptedException { ServiceCallerClient callerClient = new ServiceCallerClient(); // Make 20 calls using service client mimicking 20 parallel users. for (int i = 0; i < 20; i++) { System.out.println(LocalTime.now() + " Starting service call = " + i); new Thread(() -> callerClient.callService(), "service-call-" + (i + 1)).start(); Thread.sleep(50); } } } |
Output
Key behavior to look for in output
- Service calls start:
- At first you will see “Starting service call” for all 20 threads.
- 5 concurrent calls & other waiting:
- Then you will see that service-call-1 till service-call-5 (5 concurrent threads) processing finished together around 17:01:27 while other threads were still waiting.
- Next 5 concurrent calls:
- Then you will see that service-call-6 till service-call-10 (5 concurrent threads) processing finished together around 17:01:29
- Wait time over for last 5 calls:
- Then you will see that service-call-16 till service-call-20 ended up with io.github.resilience4j.bulkhead.BulkheadFullException because 5 second wait time for them was over.
- Processing finished for remaining 5 calls:
- Then you will see that service-call-11 till service-call-15 (5 concurrent threads) processing finished together around 17:01:31. These logs show after above exception which means these might be under process while above calls failed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
17:01:25.108 Starting service call = 0 17:01:25.163 Starting service call = 1 17:01:25.213 Starting service call = 2 17:01:25.263 Starting service call = 3 17:01:25.314 Starting service call = 4 17:01:25.365 Starting service call = 5 17:01:25.415 Starting service call = 6 17:01:25.466 Starting service call = 7 17:01:25.517 Starting service call = 8 17:01:25.567 Starting service call = 9 17:01:25.617 Starting service call = 10 17:01:25.668 Starting service call = 11 17:01:25.718 Starting service call = 12 17:01:25.769 Starting service call = 13 17:01:25.819 Starting service call = 14 17:01:25.869 Starting service call = 15 17:01:25.920 Starting service call = 16 17:01:25.971 Starting service call = 17 17:01:26.021 Starting service call = 18 17:01:26.072 Starting service call = 19 17:01:27.117 Call processing finished = service-call-1 17:01:27.163 Call processing finished = service-call-2 17:01:27.213 Call processing finished = service-call-3 17:01:27.264 Call processing finished = service-call-4 17:01:27.315 Call processing finished = service-call-5 17:01:29.117 Call processing finished = service-call-6 17:01:29.164 Call processing finished = service-call-7 17:01:29.214 Call processing finished = service-call-8 17:01:29.265 Call processing finished = service-call-9 17:01:29.316 Call processing finished = service-call-10 Exception in thread "service-call-16" io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'externalConcurrentService' is full and does not permit further calls at io.github.resilience4j.bulkhead.BulkheadFullException.createBulkheadFullException(BulkheadFullException.java:42) at io.github.resilience4j.bulkhead.internal.SemaphoreBulkhead.acquirePermission(SemaphoreBulkhead.java:134) at io.github.resilience4j.bulkhead.Bulkhead.lambda$decorateRunnable$9(Bulkhead.java:430) at io.github.resilience4j.bulkhead.Bulkhead.executeRunnable(Bulkhead.java:181) at com.itsallbinary.resilience4j.tutorial.ServiceCallerClient.callService(BulkheadBasics.java:46) at com.itsallbinary.resilience4j.tutorial.BulkheadBasics.lambda$0(BulkheadBasics.java:19) at java.lang.Thread.run(Thread.java:745) Exception in thread "service-call-17" io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'externalConcurrentService' is full and does not permit further calls at io.github.resilience4j.bulkhead.BulkheadFullException.createBulkheadFullException(BulkheadFullException.java:42) at io.github.resilience4j.bulkhead.internal.SemaphoreBulkhead.acquirePermission(SemaphoreBulkhead.java:134) at io.github.resilience4j.bulkhead.Bulkhead.lambda$decorateRunnable$9(Bulkhead.java:430) at io.github.resilience4j.bulkhead.Bulkhead.executeRunnable(Bulkhead.java:181) at com.itsallbinary.resilience4j.tutorial.ServiceCallerClient.callService(BulkheadBasics.java:46) at com.itsallbinary.resilience4j.tutorial.BulkheadBasics.lambda$0(BulkheadBasics.java:19) at java.lang.Thread.run(Thread.java:745) Exception in thread "service-call-18" io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'externalConcurrentService' is full and does not permit further calls at io.github.resilience4j.bulkhead.BulkheadFullException.createBulkheadFullException(BulkheadFullException.java:42) at io.github.resilience4j.bulkhead.internal.SemaphoreBulkhead.acquirePermission(SemaphoreBulkhead.java:134) at io.github.resilience4j.bulkhead.Bulkhead.lambda$decorateRunnable$9(Bulkhead.java:430) at io.github.resilience4j.bulkhead.Bulkhead.executeRunnable(Bulkhead.java:181) at com.itsallbinary.resilience4j.tutorial.ServiceCallerClient.callService(BulkheadBasics.java:46) at com.itsallbinary.resilience4j.tutorial.BulkheadBasics.lambda$0(BulkheadBasics.java:19) at java.lang.Thread.run(Thread.java:745) Exception in thread "service-call-19" io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'externalConcurrentService' is full and does not permit further calls at io.github.resilience4j.bulkhead.BulkheadFullException.createBulkheadFullException(BulkheadFullException.java:42) at io.github.resilience4j.bulkhead.internal.SemaphoreBulkhead.acquirePermission(SemaphoreBulkhead.java:134) at io.github.resilience4j.bulkhead.Bulkhead.lambda$decorateRunnable$9(Bulkhead.java:430) at io.github.resilience4j.bulkhead.Bulkhead.executeRunnable(Bulkhead.java:181) at com.itsallbinary.resilience4j.tutorial.ServiceCallerClient.callService(BulkheadBasics.java:46) at com.itsallbinary.resilience4j.tutorial.BulkheadBasics.lambda$0(BulkheadBasics.java:19) at java.lang.Thread.run(Thread.java:745) Exception in thread "service-call-20" io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'externalConcurrentService' is full and does not permit further calls at io.github.resilience4j.bulkhead.BulkheadFullException.createBulkheadFullException(BulkheadFullException.java:42) at io.github.resilience4j.bulkhead.internal.SemaphoreBulkhead.acquirePermission(SemaphoreBulkhead.java:134) at io.github.resilience4j.bulkhead.Bulkhead.lambda$decorateRunnable$9(Bulkhead.java:430) at io.github.resilience4j.bulkhead.Bulkhead.executeRunnable(Bulkhead.java:181) at com.itsallbinary.resilience4j.tutorial.ServiceCallerClient.callService(BulkheadBasics.java:46) at com.itsallbinary.resilience4j.tutorial.BulkheadBasics.lambda$0(BulkheadBasics.java:19) at java.lang.Thread.run(Thread.java:745) 17:01:31.117 Call processing finished = service-call-11 17:01:31.165 Call processing finished = service-call-12 17:01:31.214 Call processing finished = service-call-13 17:01:31.265 Call processing finished = service-call-14 17:01:31.317 Call processing finished = service-call-15 |
See Also
Resilience4j Tutorial | Basics with runtime behavior | Simple examples for beginners