├── .gitignore
├── LICENSE
├── README.md
├── docs
├── comparison-plots.png
├── sequence-async.png
├── sequence-threads.png
├── sequence-vthreads.png
└── web-service-example.png
├── pom.xml
└── src
└── main
└── java
└── loomtest
├── AsyncHandler.java
├── Authentication.java
├── Authorization.java
├── Backend.java
├── Frontend.java
├── Json.java
├── Meeting.java
├── Meetings.java
├── NoOpLogger.java
└── SyncHandler.java
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | target
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Elliot Barlas
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Project Loom Comparison
2 |
3 | This project compares different methods for achieving scalable concurrency in Java
4 | in the context of a representative web service architecture.
5 |
6 | * Platform OS threads
7 | * Virtual threads
8 | * Asynchronous programming
9 |
10 | The following model is used. A browser talks to a frontend web service,
11 | which talks to three separate backend web services for authentication,
12 | authorization, and database access.
13 |
14 | The frontend service simply calls each backend service in succession.
15 | Each backend call requires context from the prior calls, as show in the
16 | dependency diagram.
17 |
18 | Each backend call introduces 1/3 second of latency. The target latency
19 | from browser to frontend web service is therefore 1 second. This arrangement
20 | is ideal for experimenting with concurrency. The server latency is overwhelmingly
21 | due to wait time.
22 |
23 | 
24 |
25 | # Components
26 |
27 | In the following experiments, we'll see how throughput and latency respond
28 | to increase in concurrency.
29 |
30 | Three separate handler implementations in the frontend web service are compared:
31 |
32 | * Platform OS threads
33 | * Virtual threads
34 | * Asynchronous programming
35 |
36 | ## Platform Threads
37 |
38 | The following sequence diagram outlines to interaction between components
39 | in the frontend web server with platform threads.
40 |
41 | 
42 |
43 | ## Asynchronous Programming
44 |
45 | The following sequence diagram outlines to interaction between components
46 | in the frontend web server with asynchronous programming.
47 |
48 | 
49 |
50 | ## Virtual Threads
51 |
52 | The following sequence diagram outlines to interaction between components
53 | in the frontend web server with asynchronous programming.
54 |
55 | 
56 |
57 | # Experiment
58 |
59 | Experiments were conducted on EC2 instances:
60 |
61 | * c5.2xlarge
62 | * 16GB RAM
63 | * 8 vCPU
64 | * Amazon Linux 2 with Linux Kernel 5.10, AMI ami-00f7e5c52c0f43726
65 | * Project Loom Early Access Build 19-loom+5-429 (2022/4/4) from https://jdk.java.net/loom/
66 |
67 | Three EC2 instances were used as follows:
68 |
69 | * EC2[1] - ApacheBench HTTP benchmark acting as browser
70 | * EC2[2] - Frontend server the makes 3 calls to backend in succession
71 | * EC2[3] - Backend server that provides authentication, authorization, and database
72 |
73 | ## ApacheBench
74 |
75 | ApacheBench is an HTTP benchmarking tool. It sends the indicated number of requests
76 | continuously using a specified number of persistent connections.
77 |
78 | The following concurrency levels and workloads were tested:
79 |
80 | * `-c 1000 -n 120000`
81 | * `-c 5000 -n 600000`
82 | * `-c 10000 -n 1200000`
83 | * `-c 15000 -n 1800000`
84 | * `-c 20000 -n 2400000`
85 |
86 | ```
87 | $ ab -k -c 1000 -n 120000 -H "Authorization: token" http://10.39.197.143:9000/meetings
88 | ```
89 |
90 | ## Frontend
91 |
92 | The frontend web server receives connections and requests from ApacheBench.
93 | For each request received, it makes three calls in succession to the backend
94 | web server. Each backend call has a configured latency of 1/3 seconds, so the
95 | target latency at the frontend web server is 1 second.
96 |
97 | ```
98 | $ ./jdk-19/bin/java --enable-preview -cp project-loom-example-1.0.0-SNAPSHOT-jar-with-dependencies.jar loomtest.Frontend 0.0.0.0 9000 thread 1000 10.39.196.215:9000 8192 false
99 | Args[host=0.0.0.0, port=9000, type=thread, threads=1000, backend=10.39.196.215:9000, acceptLength=8192, debug=false]
100 | ```
101 |
102 | ## Backend
103 |
104 | The backend web server receives connections and requests from the frontend
105 | web server. It responds to each request after a configured delay of 1/3 seconds.
106 |
107 | ```
108 | ./jdk-19/bin/java -cp project-loom-example-1.0.0-SNAPSHOT-jar-with-dependencies.jar loomtest.Backend 0.0.0.0 9000 333 8192 false
109 | ```
110 |
111 | # Results
112 |
113 | The handler using virtual threads outperforms the others.
114 | It achieves lower latency and higher throughput while using less
115 | overall CPU time for the same workload.
116 |
117 | * All frontend configurations achieve the target latency up to concurrent of 5,000
118 | * All frontend configurations are eventually CPU bound
119 | * Beyond concurrency 5,000, platform threads degrade quickly
120 | * Platform thread virtual memory increasing linearly with concurrency, due to stack memory
121 | * Platform thread consistency spends more CPU for the same amount of work
122 |
123 | Results were gathered from `ab` and `ps` output. The following `ps` command was
124 | used to gather metrics on the frontend web server EC2 instance:
125 |
126 | ```
127 | ps -C java -o args:100,pcpu,cputime,pid,pmem,rss,vsz
128 | ```
129 |
130 | 
131 |
132 | # Raw Data
133 |
134 | The following Python snippets include the raw data and the matplot
135 | lib code to generate plots.
136 |
137 | ```python
138 | x = [1000, 5000, 10000, 15000, 20000]
139 |
140 | type_thread = {
141 | 'label': 'Platform Threads',
142 | 'duration': [121.864, 125.004, 176.969, 318.481, 484.875],
143 | 'latency': [1015.532, 1041.697, 1474.743, 2654.009, 4040.624],
144 | 'throughput': [984.71, 4799.86, 6780.84, 5651.83, 4949.73],
145 | 'cputime': ['00:01:21', '00:05:50', '00:11:42', '00:18:50', '00:28:28'],
146 | 'rss': [397276, 1934504, 2036636, 3588076, 3102536],
147 | 'vsz': [11062040, 15458340, 21293252, 27327492, 32585860]
148 | }
149 |
150 | type_virtual = {
151 | 'label': 'Virtual Threads',
152 | 'duration': [121.775, 123.578, 129.480, 154.657, 207.505],
153 | 'latency': [1014.791, 1029.815, 1079.002, 1288.804, 1729.205],
154 | 'throughput': [985.42, 4855.24, 9267.83, 11638.70, 11566.01],
155 | 'cputime': ['00:01:06', '00:04:41', '00:07:58', '00:15:37', '00:23:54'],
156 | 'rss': [842032, 2337268, 4265148, 4265612, 4301388],
157 | 'vsz': [8300160, 8366728, 8433296, 8699568, 8633000]
158 | }
159 |
160 | type_async = {
161 | 'label': 'Asynchronous',
162 | 'duration': [121.651, 123.146, 127.690, 184.894, 248.448],
163 | 'latency': [1013.756, 1026.217, 1064.080, 1540.781, 2070.396],
164 | 'throughput': [986.43, 4872.26, 9397.79, 9735.32, 9659.99],
165 | 'cputime': ['00:01:15', '00:05:11', '00:09:32', '00:14:41', '00:19:53'],
166 | 'rss': [328928, 1082940, 2423148, 3508304, 4263280],
167 | 'vsz': [10085308, 10542920, 12049948, 13110868, 10042428]
168 | }
169 | ```
170 |
171 | ```python
172 | def to_seconds(txt):
173 | parts = txt.split(':')
174 | return int(parts[0]) * 60 * 60 + int(parts[1]) * 60 + int(parts[2])
175 | ```
176 |
177 | ```python
178 | import matplotlib.pyplot as plt
179 |
180 | fig, axes = plt.subplots(3, 2)
181 |
182 | ax11, ax21, ax12, ax22, ax13, ax23 = axes.ravel()
183 |
184 | for d in [type_thread, type_virtual, type_async]:
185 | ax11.plot(x, d['throughput'], label=d['label'])
186 | ax11.set_xlabel('Connections')
187 | ax11.set_ylabel('Requests per second')
188 | ax11.set_title('Throughput')
189 | ax11.grid()
190 | ax11.legend()
191 |
192 | for d in [type_thread, type_virtual, type_async]:
193 | ax12.plot(x, [to_seconds(c) for c in d['cputime']], label=d['label'])
194 | ax12.set_xlabel('Connections')
195 | ax12.set_ylabel('Seconds')
196 | ax12.set_title('Cumulative CPU Time')
197 | ax12.grid()
198 | ax12.legend()
199 |
200 | for d in [type_thread, type_virtual, type_async]:
201 | ax21.plot(x, d['rss'], label=d['label'])
202 | ax21.set_xlabel('Connections')
203 | ax21.set_ylabel('Kilobytes')
204 | ax21.set_title('Physical Memory Used')
205 | ax21.grid()
206 | ax21.legend()
207 |
208 | for d in [type_thread, type_virtual, type_async]:
209 | ax22.plot(x, d['vsz'], label=d['label'])
210 | ax22.set_xlabel('Connections')
211 | ax22.set_ylabel('Kilobytes')
212 | ax22.set_title('Virtual Memory Size')
213 | ax22.grid()
214 | ax22.legend()
215 |
216 | for d in [type_thread, type_virtual, type_async]:
217 | ax13.plot(x, d['latency'], label=d['label'])
218 | ax13.set_xlabel('Connections')
219 | ax13.set_ylabel('Seconds')
220 | ax13.set_title('Latency')
221 | ax13.grid()
222 | ax13.legend()
223 |
224 | for d in [type_thread, type_virtual, type_async]:
225 | cpufac = [(to_seconds(d['cputime'][n]) / d['duration'][n]) * 100 for n in range(len(d['cputime']))]
226 | ax23.plot(x, cpufac, label=d['label'])
227 | ax23.set_xlabel('Connections')
228 | ax23.set_ylabel('CPU%')
229 | ax23.set_title('CPU%')
230 | ax23.grid()
231 | ax23.legend()
232 |
233 | fig.set_size_inches(24, 18)
234 | plt.savefig('project-loom-comparison.png')
235 | plt.show()
236 | ```
--------------------------------------------------------------------------------
/docs/comparison-plots.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebarlas/project-loom-comparison/1869f2084060e8757e470e8bb1ad84d4ccd9ad3e/docs/comparison-plots.png
--------------------------------------------------------------------------------
/docs/sequence-async.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebarlas/project-loom-comparison/1869f2084060e8757e470e8bb1ad84d4ccd9ad3e/docs/sequence-async.png
--------------------------------------------------------------------------------
/docs/sequence-threads.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebarlas/project-loom-comparison/1869f2084060e8757e470e8bb1ad84d4ccd9ad3e/docs/sequence-threads.png
--------------------------------------------------------------------------------
/docs/sequence-vthreads.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebarlas/project-loom-comparison/1869f2084060e8757e470e8bb1ad84d4ccd9ad3e/docs/sequence-vthreads.png
--------------------------------------------------------------------------------
/docs/web-service-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebarlas/project-loom-comparison/1869f2084060e8757e470e8bb1ad84d4ccd9ad3e/docs/web-service-example.png
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 | 4.0.0
5 | loomtest
6 | project-loom-example
7 | jar
8 | Project Loom Example
9 | 1.0.0-SNAPSHOT
10 |
11 |
12 |
13 | org.microhttp
14 | microhttp
15 | 0.7
16 |
17 |
18 | com.fasterxml.jackson.core
19 | jackson-core
20 | 2.13.2
21 |
22 |
23 | com.fasterxml.jackson.core
24 | jackson-databind
25 | 2.13.2.2
26 |
27 |
28 |
29 |
30 |
31 |
32 | org.apache.maven.plugins
33 | maven-compiler-plugin
34 | 3.10.1
35 |
36 | 19
37 | 19
38 | --enable-preview
39 |
40 |
41 |
42 | maven-assembly-plugin
43 |
44 |
45 | jar-with-dependencies
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/main/java/loomtest/AsyncHandler.java:
--------------------------------------------------------------------------------
1 | package loomtest;
2 |
3 | import org.microhttp.Handler;
4 | import org.microhttp.Header;
5 | import org.microhttp.Request;
6 | import org.microhttp.Response;
7 |
8 | import java.net.URI;
9 | import java.net.http.HttpClient;
10 | import java.net.http.HttpRequest;
11 | import java.net.http.HttpResponse;
12 | import java.time.Duration;
13 | import java.util.List;
14 | import java.util.concurrent.CompletableFuture;
15 | import java.util.function.Consumer;
16 |
17 | public class AsyncHandler implements Handler {
18 |
19 | final HttpClient httpClient;
20 | final String backend;
21 |
22 | AsyncHandler(HttpClient httpClient, String backend) {
23 | this.httpClient = httpClient;
24 | this.backend = backend;
25 | }
26 |
27 | public void handle(Request request, Consumer callback) {
28 | String token = request.header("Authorization");
29 | CompletableFuture tokenFtr = token != null
30 | ? CompletableFuture.completedFuture(token)
31 | : CompletableFuture.failedFuture(new RuntimeException("token header required"));
32 | CompletableFuture authentication = tokenFtr
33 | .thenCompose(t -> sendRequestFor("/authenticate?token=" + t, Authentication.class));
34 | CompletableFuture authorization = authentication
35 | .thenCompose(a -> sendRequestFor("/authorize?id=" + a.userId(), Authorization.class))
36 | .thenApply(a -> {
37 | if (!a.authorities().contains("get-meetings")) {
38 | throw new RuntimeException("not authorized to get meetings");
39 | }
40 | return a;
41 | });
42 | CompletableFuture meetings = authorization
43 | .thenCompose(a -> sendRequestFor("/meetings?id=" + authentication.join().userId(), Meetings.class));
44 | List headers = List.of(new Header("Content-Type", "application/json"));
45 | CompletableFuture response = meetings
46 | .thenApply(meets -> new Response(200, "OK", headers, Json.toJson(meets)));
47 | response.whenComplete((res, exception) -> {
48 | if (exception != null) {
49 | exception.printStackTrace();
50 | callback.accept(new Response(500, "Internal Server Error", List.of(), new byte[0]));
51 | } else {
52 | callback.accept(res);
53 | }
54 | });
55 | }
56 |
57 | CompletableFuture sendRequestFor(String endpoint, Class type) {
58 | URI uri = URI.create("http://%s%s".formatted(backend, endpoint));
59 | HttpRequest request = HttpRequest.newBuilder()
60 | .timeout(Duration.ofSeconds(15))
61 | .uri(uri)
62 | .GET()
63 | .build();
64 | CompletableFuture> response = httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString());
65 | return response.thenApply(res -> Json.fromJson(res.body(), type));
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/loomtest/Authentication.java:
--------------------------------------------------------------------------------
1 | package loomtest;
2 |
3 | record Authentication(String userId) {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/loomtest/Authorization.java:
--------------------------------------------------------------------------------
1 | package loomtest;
2 |
3 | import java.util.List;
4 |
5 | record Authorization(List authorities) {
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/java/loomtest/Backend.java:
--------------------------------------------------------------------------------
1 | package loomtest;
2 |
3 | import org.microhttp.DebugLogger;
4 | import org.microhttp.EventLoop;
5 | import org.microhttp.Handler;
6 | import org.microhttp.Header;
7 | import org.microhttp.Logger;
8 | import org.microhttp.Options;
9 | import org.microhttp.Request;
10 | import org.microhttp.Response;
11 |
12 | import java.io.IOException;
13 | import java.util.List;
14 | import java.util.concurrent.Executors;
15 | import java.util.concurrent.ScheduledExecutorService;
16 | import java.util.concurrent.TimeUnit;
17 |
18 | public class Backend {
19 |
20 | public static void main(String[] args) throws IOException {
21 | Args a = Args.parse(args);
22 | System.out.println(a);
23 | ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
24 | Handler handler = (req, callback) -> scheduler.schedule(() -> callback.accept(handle(req)), a.delay, TimeUnit.MILLISECONDS);
25 | Options options = new Options()
26 | .withHost(a.host)
27 | .withPort(a.port)
28 | .withAcceptLength(a.acceptLength)
29 | .withReuseAddr(true)
30 | .withReusePort(true);
31 | Logger logger = a.debug ? new DebugLogger() : new NoOpLogger();
32 | EventLoop eventLoop = new EventLoop(options, logger, handler);
33 | eventLoop.start();
34 | }
35 |
36 | static Response handle(Request req) {
37 | if (req.uri().startsWith("/authenticate")) {
38 | return new Response(
39 | 200,
40 | "OK",
41 | List.of(new Header("Content-Type", "application/json")),
42 | "{\"userId\":\"abc123\"}".getBytes());
43 | } else if (req.uri().startsWith("/authorize")) {
44 | return new Response(
45 | 200,
46 | "OK",
47 | List.of(new Header("Content-Type", "application/json")),
48 | "{\"authorities\":[\"get-meetings\"]}".getBytes());
49 | } else if (req.uri().startsWith("/meetings")) {
50 | return new Response(
51 | 200,
52 | "OK",
53 | List.of(new Header("Content-Type", "application/json")),
54 | "{\"meetings\":[{\"time\":\"now\",\"subject\":\"tech-talk\"}]}".getBytes());
55 | } else {
56 | return new Response(404, "Not Found", List.of(), new byte[0]);
57 | }
58 | }
59 |
60 | record Args(String host, int port, int delay, int acceptLength, boolean debug) {
61 | static Args parse(String[] args) {
62 | return new Args(
63 | args.length >= 1 ? args[0] : "localhost",
64 | args.length >= 2 ? Integer.parseInt(args[1]) : 8080,
65 | args.length >= 3 ? Integer.parseInt(args[2]) : 100,
66 | args.length >= 4 ? Integer.parseInt(args[3]) : 0,
67 | args.length >= 5 ? Boolean.parseBoolean(args[4]) : true);
68 | }
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/loomtest/Frontend.java:
--------------------------------------------------------------------------------
1 | package loomtest;
2 |
3 | import org.microhttp.DebugLogger;
4 | import org.microhttp.EventLoop;
5 | import org.microhttp.Handler;
6 | import org.microhttp.Logger;
7 | import org.microhttp.Options;
8 |
9 | import java.io.IOException;
10 | import java.net.http.HttpClient;
11 | import java.time.Duration;
12 | import java.util.concurrent.ExecutorService;
13 | import java.util.concurrent.Executors;
14 |
15 | public class Frontend {
16 |
17 | public static void main(String[] args) throws IOException {
18 | Args a = Args.parse(args);
19 | System.out.println(a);
20 | Options options = new Options()
21 | .withHost(a.host)
22 | .withPort(a.port)
23 | .withAcceptLength(a.acceptLength)
24 | .withReuseAddr(true)
25 | .withReusePort(true);
26 | Logger logger = a.debug ? new DebugLogger() : new NoOpLogger();
27 | EventLoop eventLoop = new EventLoop(options, logger, handler(a));
28 | eventLoop.start();
29 | }
30 |
31 | static Handler handler(Args args) {
32 | return args.type.equals("async")
33 | ? new AsyncHandler(httpClient(args.type), args.backend)
34 | : new SyncHandler(executorService(args), httpClient(args.type), args.backend);
35 | }
36 |
37 | static ExecutorService executorService(Args args) {
38 | return args.type.contains("vthread")
39 | ? Executors.newVirtualThreadPerTaskExecutor()
40 | : Executors.newFixedThreadPool(args.threads);
41 | }
42 |
43 | static HttpClient httpClient(String type) {
44 | if (type.equals("vthread")) {
45 | return HttpClient.newBuilder()
46 | .executor(Executors.newVirtualThreadPerTaskExecutor())
47 | .connectTimeout(Duration.ofSeconds(5))
48 | .build();
49 | }
50 | return HttpClient.newBuilder()
51 | .connectTimeout(Duration.ofSeconds(5))
52 | .build();
53 | }
54 |
55 | record Args(String host, int port, String type, int threads, String backend, int acceptLength, boolean debug) {
56 | static Args parse(String[] args) {
57 | return new Args(
58 | args.length >= 1 ? args[0] : "localhost",
59 | args.length >= 2 ? Integer.parseInt(args[1]) : 8081,
60 | args.length >= 3 ? args[2] : "vthread",
61 | args.length >= 4 ? Integer.parseInt(args[3]) : 10,
62 | args.length >= 5 ? args[4] : "localhost:8080",
63 | args.length >= 6 ? Integer.parseInt(args[5]) : 0,
64 | args.length >= 7 ? Boolean.parseBoolean(args[6]) : true);
65 | }
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/loomtest/Json.java:
--------------------------------------------------------------------------------
1 | package loomtest;
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 |
6 | import java.io.IOException;
7 |
8 | public class Json {
9 |
10 | static final ObjectMapper objectMapper = new ObjectMapper();
11 |
12 | static byte[] toJson(Object object) {
13 | try {
14 | return objectMapper.writeValueAsBytes(object);
15 | } catch (JsonProcessingException e) {
16 | throw new RuntimeException(e);
17 | }
18 | }
19 |
20 | static T fromJson(String json, Class type) {
21 | try {
22 | return objectMapper.readValue(json, type);
23 | } catch (IOException e) {
24 | throw new RuntimeException(e);
25 | }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/loomtest/Meeting.java:
--------------------------------------------------------------------------------
1 | package loomtest;
2 |
3 | record Meeting(String time, String subject) {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/loomtest/Meetings.java:
--------------------------------------------------------------------------------
1 | package loomtest;
2 |
3 | import java.util.List;
4 |
5 | record Meetings(List meetings) {
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/java/loomtest/NoOpLogger.java:
--------------------------------------------------------------------------------
1 | package loomtest;
2 |
3 | import org.microhttp.LogEntry;
4 | import org.microhttp.Logger;
5 |
6 | public class NoOpLogger implements Logger {
7 |
8 | @Override
9 | public boolean enabled() {
10 | return false;
11 | }
12 |
13 | @Override
14 | public void log(LogEntry... entries) {
15 |
16 | }
17 |
18 | @Override
19 | public void log(Exception e, LogEntry... entries) {
20 |
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/loomtest/SyncHandler.java:
--------------------------------------------------------------------------------
1 | package loomtest;
2 |
3 | import org.microhttp.Handler;
4 | import org.microhttp.Header;
5 | import org.microhttp.Request;
6 | import org.microhttp.Response;
7 |
8 | import java.io.IOException;
9 | import java.net.URI;
10 | import java.net.http.HttpClient;
11 | import java.net.http.HttpRequest;
12 | import java.net.http.HttpResponse;
13 | import java.time.Duration;
14 | import java.util.List;
15 | import java.util.concurrent.ExecutorService;
16 | import java.util.function.Consumer;
17 |
18 | public class SyncHandler implements Handler {
19 |
20 | final ExecutorService executorService;
21 | final HttpClient httpClient;
22 | final String backend;
23 |
24 | SyncHandler(ExecutorService executorService, HttpClient httpClient, String backend) {
25 | this.executorService = executorService;
26 | this.httpClient = httpClient;
27 | this.backend = backend;
28 | }
29 |
30 | @Override
31 | public void handle(Request request, Consumer callback) {
32 | executorService.execute(() -> callback.accept(doHandle(request)));
33 | }
34 |
35 | Response doHandle(Request request) {
36 | try {
37 | String token = request.header("Authorization");
38 | if (token == null) {
39 | throw new RuntimeException("token header required");
40 | }
41 | Authentication authentication = sendRequestFor("/authenticate?token=" + token, Authentication.class);
42 | Authorization authorization = sendRequestFor("/authorize?id=" + authentication.userId(), Authorization.class);
43 | if (!authorization.authorities().contains("get-meetings")) {
44 | throw new RuntimeException("not authorized to get meetings");
45 | }
46 | Meetings meetings = sendRequestFor("/meetings?id=" + authentication.userId(), Meetings.class);
47 | List headers = List.of(new Header("Content-Type", "application/json"));
48 | return new Response(200, "OK", headers, Json.toJson(meetings));
49 | } catch (Exception e) {
50 | e.printStackTrace();
51 | return new Response(500, "Internal Server Error", List.of(), new byte[0]);
52 | }
53 | }
54 |
55 | T sendRequestFor(String endpoint, Class type) throws IOException, InterruptedException {
56 | URI uri = URI.create("http://%s%s".formatted(backend, endpoint));
57 | HttpRequest request = HttpRequest.newBuilder()
58 | .timeout(Duration.ofSeconds(10))
59 | .uri(uri)
60 | .GET()
61 | .build();
62 | HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
63 | if (response.statusCode() != 200) {
64 | throw new RuntimeException("error occurred contacting %s".formatted(endpoint));
65 | }
66 | return Json.fromJson(response.body(), type);
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------