├── .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 | ![Web Service Example](docs/web-service-example.png) 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 | ![Platform Threads](docs/sequence-threads.png) 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 | ![Platform Threads](docs/sequence-async.png) 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 | ![Platform Threads](docs/sequence-vthreads.png) 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 | ![Comparison Plots](docs/comparison-plots.png) 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 | --------------------------------------------------------------------------------