001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2019 microBean™. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 014 * implied. See the License for the specific language governing 015 * permissions and limitations under the License. 016 */ 017package org.microbean.jersey.netty.cdi; 018 019import java.lang.annotation.Annotation; 020 021import java.net.URI; 022import java.net.URISyntaxException; 023 024import java.nio.channels.spi.SelectorProvider; 025 026import java.util.ArrayList; 027import java.util.Collection; 028import java.util.HashMap; 029import java.util.Map; 030import java.util.Objects; 031import java.util.Set; 032 033import java.util.concurrent.CountDownLatch; 034import java.util.concurrent.Executor; 035 036import java.util.function.BiFunction; 037 038import javax.enterprise.context.ApplicationScoped; 039import javax.enterprise.context.BeforeDestroyed; 040import javax.enterprise.context.Initialized; 041 042import javax.enterprise.event.Observes; 043 044import javax.enterprise.inject.Any; 045import javax.enterprise.inject.Default; 046import javax.enterprise.inject.Instance; 047 048import javax.enterprise.inject.spi.Bean; 049import javax.enterprise.inject.spi.BeanManager; 050import javax.enterprise.inject.spi.DeploymentException; 051import javax.enterprise.inject.spi.Extension; 052 053import javax.enterprise.util.TypeLiteral; 054 055import javax.inject.Named; 056 057import javax.ws.rs.ApplicationPath; 058 059import javax.ws.rs.core.Application; 060import javax.ws.rs.core.SecurityContext; 061 062import io.netty.bootstrap.ServerBootstrap; 063import io.netty.bootstrap.ServerBootstrapConfig; 064 065import io.netty.channel.ChannelFactory; 066import io.netty.channel.ChannelHandlerContext; 067import io.netty.channel.DefaultSelectStrategyFactory; 068import io.netty.channel.EventLoopGroup; 069import io.netty.channel.SelectStrategyFactory; 070import io.netty.channel.ServerChannel; 071 072import io.netty.channel.nio.NioEventLoopGroup; 073 074import io.netty.channel.socket.nio.NioServerSocketChannel; 075 076import io.netty.handler.codec.http.HttpRequest; 077 078import io.netty.handler.ssl.SslContext; 079 080import io.netty.util.concurrent.EventExecutorGroup; 081import io.netty.util.concurrent.Future; 082import io.netty.util.concurrent.RejectedExecutionHandler; 083import io.netty.util.concurrent.RejectedExecutionHandlers; 084import io.netty.util.concurrent.EventExecutorChooserFactory; 085import io.netty.util.concurrent.DefaultEventExecutorChooserFactory; 086 087import org.glassfish.jersey.server.ApplicationHandler; 088 089import org.microbean.configuration.api.Configurations; 090 091import org.microbean.jaxrs.cdi.JaxRsExtension; 092 093import org.microbean.jersey.netty.JerseyChannelInitializer; 094 095public class JerseyNettyExtension implements Extension { 096 097 098 /* 099 * Static fields. 100 */ 101 102 103 private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; 104 105 106 /* 107 * Instance fields. 108 */ 109 110 111 private final Collection<Throwable> shutdownProblems; 112 113 private volatile Collection<EventExecutorGroup> eventExecutorGroups; 114 115 private volatile CountDownLatch bindLatch; 116 117 private volatile CountDownLatch runLatch; 118 119 private volatile CountDownLatch shutdownLatch; 120 121 122 /* 123 * Constructors. 124 */ 125 126 127 /** 128 * Creates a new {@link JerseyNettyExtension}. 129 */ 130 public JerseyNettyExtension() { 131 super(); 132 this.shutdownProblems = new ArrayList<>(); 133 final Thread containerThread = Thread.currentThread(); 134 Runtime.getRuntime().addShutdownHook(new Thread(() -> { 135 zeroOut(this.runLatch); 136 zeroOut(this.bindLatch); 137 containerThread.interrupt(); 138 try { 139 containerThread.join(); 140 } catch (final InterruptedException interruptedException) { 141 Thread.currentThread().interrupt(); 142 } 143 })); 144 } 145 146 147 /* 148 * Instance methods. 149 */ 150 151 152 // TODO: prioritize 153 private final void onStartup(@Observes @Initialized(ApplicationScoped.class) 154 final Object event, 155 final BeanManager beanManager) 156 throws URISyntaxException { 157 if (beanManager != null) { 158 final JaxRsExtension extension = beanManager.getExtension(JaxRsExtension.class); 159 if (extension != null) { 160 final Set<Set<Annotation>> applicationQualifierSets = extension.getAllApplicationQualifiers(); 161 if (applicationQualifierSets != null && !applicationQualifierSets.isEmpty()) { 162 163 final Instance<Object> instance = beanManager.createInstance(); 164 assert instance != null; 165 166 final Configurations configurations = instance.select(Configurations.class).get(); 167 168 final Map<String, String> baseConfigurationCoordinates = configurations.getConfigurationCoordinates(); 169 170 final int size = applicationQualifierSets.size(); 171 assert size > 0; 172 this.shutdownLatch = new CountDownLatch(size); 173 this.bindLatch = new CountDownLatch(size); 174 final Collection<Throwable> bindProblems = new ArrayList<>(); 175 176 for (final Set<Annotation> applicationQualifiers : applicationQualifierSets) { 177 178 // Quick check to bail out if someone CTRL-Ced and caused 179 // Weld's shutdown hook to fire. That hook fires 180 // a @BeforeDestroyed event on a Thread that is not this 181 // Thread, so it's possible to be processing 182 // an @Initialized(ApplicationScoped.class) event on the 183 // container thread while also processing 184 // a @BeforeDestroyed(ApplicationScoped.class) event. We 185 // basically want to skip bootstrapping a bunch of things 186 // if we're going down anyway. 187 // 188 // Be particularly mindful later on of the state of the 189 // latches. 190 if (Thread.currentThread().isInterrupted()) { 191 zeroOut(this.bindLatch); 192 this.bindLatch = null; 193 break; 194 } 195 196 final Annotation[] applicationQualifiersArray; 197 if (applicationQualifiers == null) { 198 applicationQualifiersArray = null; 199 } else if (applicationQualifiers.isEmpty()) { 200 applicationQualifiersArray = EMPTY_ANNOTATION_ARRAY; 201 } else { 202 applicationQualifiersArray = applicationQualifiers.toArray(new Annotation[applicationQualifiers.size()]); 203 } 204 205 final Set<Bean<?>> applicationBeans = beanManager.getBeans(Application.class, applicationQualifiersArray); 206 assert applicationBeans != null; 207 assert !applicationBeans.isEmpty(); 208 209 try { 210 211 @SuppressWarnings("unchecked") 212 final Bean<Application> applicationBean = (Bean<Application>)beanManager.resolve(applicationBeans); 213 assert applicationBean != null; 214 215 // TODO: need to somehow squirrel away creationalContext 216 // and release it after the application goes out of 217 // scope; kind of doesn't really matter because 218 // Applications are long-lived but still. 219 final Application application = 220 (Application)beanManager.getReference(applicationBean, 221 Application.class, 222 beanManager.createCreationalContext(applicationBean)); 223 assert application != null; 224 225 final ApplicationPath applicationPathAnnotation = application.getClass().getAnnotation(ApplicationPath.class); 226 final String applicationPath; 227 if (applicationPathAnnotation == null) { 228 applicationPath = "/"; 229 } else { 230 applicationPath = applicationPathAnnotation.value(); 231 } 232 assert applicationPath != null; 233 234 final ServerBootstrap serverBootstrap = getServerBootstrap(beanManager, instance, applicationQualifiersArray, true); 235 assert serverBootstrap != null; 236 237 final SslContext sslContext = getSslContext(beanManager, instance, applicationQualifiersArray, true); 238 239 final Map<String, String> qualifierCoordinates = toConfigurationCoordinates(applicationQualifiers); 240 final Map<String, String> configurationCoordinates; 241 if (baseConfigurationCoordinates == null || baseConfigurationCoordinates.isEmpty()) { 242 if (qualifierCoordinates == null || qualifierCoordinates.isEmpty()) { 243 configurationCoordinates = baseConfigurationCoordinates; 244 } else { 245 configurationCoordinates = qualifierCoordinates; 246 } 247 } else if (qualifierCoordinates == null || qualifierCoordinates.isEmpty()) { 248 configurationCoordinates = baseConfigurationCoordinates; 249 } else { 250 configurationCoordinates = new HashMap<>(baseConfigurationCoordinates); 251 configurationCoordinates.putAll(qualifierCoordinates); 252 } 253 254 final URI baseUri; 255 if (sslContext == null) { 256 baseUri = new URI("http", 257 null /* no userInfo */, 258 configurations.getValue(configurationCoordinates, "host", "0.0.0.0"), 259 configurations.getValue(configurationCoordinates, "port", Integer.TYPE, "8080"), 260 applicationPath, 261 null /* no query */, 262 null /* no fragment */); 263 } else { 264 baseUri = new URI("https", 265 null /* no userInfo */, 266 configurations.getValue(configurationCoordinates, "host", "0.0.0.0"), 267 configurations.getValue(configurationCoordinates, "port", Integer.TYPE, "443"), 268 applicationPath, 269 null /* no query */, 270 null /* no fragment */); 271 } 272 assert baseUri != null; 273 274 final BiFunction<? super ChannelHandlerContext, ? super HttpRequest, ? extends SecurityContext> securityContextBiFunction = null; // TODO 275 276 serverBootstrap.childHandler(new JerseyChannelInitializer(baseUri, 277 sslContext, 278 new ApplicationHandler(application), 279 securityContextBiFunction)); 280 serverBootstrap.validate(); 281 282 final ServerBootstrapConfig config = serverBootstrap.config(); 283 assert config != null; 284 285 final EventLoopGroup group = config.group(); 286 assert group != null; // see validate() above 287 group.terminationFuture() 288 .addListener(f -> { 289 try { 290 if (!f.isSuccess()) { 291 final Throwable throwable = f.cause(); 292 if (throwable != null) { 293 synchronized (this.shutdownProblems) { 294 this.shutdownProblems.add(throwable); 295 } 296 } 297 } 298 } finally { 299 this.shutdownLatch.countDown(); 300 } 301 }); 302 303 Collection<EventExecutorGroup> eventExecutorGroups = this.eventExecutorGroups; 304 if (eventExecutorGroups == null) { 305 eventExecutorGroups = new ArrayList<>(); 306 this.eventExecutorGroups = eventExecutorGroups; 307 } 308 synchronized (eventExecutorGroups) { 309 eventExecutorGroups.add(group); 310 } 311 312 final Future<?> bindFuture; 313 if (config.localAddress() == null) { 314 bindFuture = serverBootstrap.bind(baseUri.getHost(), baseUri.getPort()); 315 } else { 316 bindFuture = serverBootstrap.bind(); 317 } 318 bindFuture.addListener(f -> { 319 try { 320 if (!f.isSuccess()) { 321 final Throwable throwable = f.cause(); 322 if (throwable != null) { 323 synchronized (bindProblems) { 324 bindProblems.add(throwable); 325 } 326 } 327 } 328 } finally { 329 final CountDownLatch latch = this.bindLatch; 330 if (latch != null) { 331 latch.countDown(); 332 } 333 } 334 }); 335 336 } catch (final RuntimeException | URISyntaxException throwMe) { 337 zeroOut(this.bindLatch); 338 zeroOut(this.shutdownLatch); 339 synchronized (bindProblems) { 340 for (final Throwable bindProblem : bindProblems) { 341 throwMe.addSuppressed(bindProblem); 342 } 343 } 344 throw throwMe; 345 } 346 347 } 348 349 final CountDownLatch bindLatch = this.bindLatch; 350 if (bindLatch != null) { 351 try { 352 bindLatch.await(); 353 } catch (final InterruptedException interruptedException) { 354 Thread.currentThread().interrupt(); 355 } 356 assert bindLatch.getCount() <= 0; 357 this.bindLatch = null; 358 } 359 360 DeploymentException throwMe = null; 361 synchronized (bindProblems) { 362 for (final Throwable bindProblem : bindProblems) { 363 if (throwMe == null) { 364 throwMe = new DeploymentException(bindProblem); 365 } else { 366 throwMe.addSuppressed(bindProblem); 367 } 368 } 369 bindProblems.clear(); 370 } 371 if (throwMe != null) { 372 zeroOut(this.shutdownLatch); 373 throw throwMe; 374 } 375 376 this.runLatch = new CountDownLatch(1); 377 378 } 379 } 380 } 381 assert this.bindLatch == null; 382 } 383 384 private final void waitForAllServersToStop(@Observes @BeforeDestroyed(ApplicationScoped.class) 385 final Object event) { 386 387 // Note: somewhat interestingly, Weld can fire a @BeforeDestroyed 388 // event on a thread that is not the container thread: a shutdown 389 // hook that it installs. So we have to take care to be thread 390 // safe. 391 392 final CountDownLatch runLatch = this.runLatch; 393 if (runLatch != null) { 394 try { 395 runLatch.await(); 396 } catch (final InterruptedException interruptedException) { 397 Thread.currentThread().interrupt(); 398 } 399 assert runLatch.getCount() <= 0; 400 this.runLatch = null; 401 } 402 403 final Collection<EventExecutorGroup> eventExecutorGroups = this.eventExecutorGroups; 404 if (eventExecutorGroups != null) { 405 synchronized (eventExecutorGroups) { 406 for (final EventExecutorGroup group : eventExecutorGroups) { 407 // idempotent 408 group.shutdownGracefully(); 409 } 410 eventExecutorGroups.clear(); 411 } 412 this.eventExecutorGroups = null; 413 } 414 415 final CountDownLatch shutdownLatch = this.shutdownLatch; 416 if (shutdownLatch != null) { 417 try { 418 shutdownLatch.await(); 419 } catch (final InterruptedException interruptedException) { 420 Thread.currentThread().interrupt(); 421 } 422 assert shutdownLatch.getCount() <= 0; 423 this.shutdownLatch = null; 424 } 425 426 DeploymentException throwMe = null; 427 synchronized (this.shutdownProblems) { 428 for (final Throwable shutdownProblem : this.shutdownProblems) { 429 if (throwMe == null) { 430 throwMe = new DeploymentException(shutdownProblem); 431 } else { 432 throwMe.addSuppressed(shutdownProblem); 433 } 434 } 435 this.shutdownProblems.clear(); 436 } 437 if (throwMe != null) { 438 throw throwMe; 439 } 440 441 } 442 443 444 /* 445 * Production and lookup methods. 446 */ 447 448 449 private static final SslContext getSslContext(final BeanManager beanManager, 450 final Instance<Object> instance, 451 final Annotation[] qualifiersArray, 452 final boolean lookup) { 453 return acquire(beanManager, 454 instance, 455 SslContext.class, 456 qualifiersArray, 457 lookup, 458 (bm, i, qa) -> null); 459 } 460 461 private static final ServerBootstrap getServerBootstrap(final BeanManager beanManager, 462 final Instance<Object> instance, 463 final Annotation[] qualifiersArray, 464 final boolean lookup) { 465 return acquire(beanManager, 466 instance, 467 ServerBootstrap.class, 468 qualifiersArray, 469 lookup, 470 (bm, i, qa) -> { 471 final ServerBootstrap returnValue = new ServerBootstrap(); 472 // See https://stackoverflow.com/a/28342821/208288 473 returnValue.group(getEventLoopGroup(bm, i, qa, true)); 474 returnValue.channelFactory(getChannelFactory(bm, i, qa, true)); 475 476 // Permit arbitrary customization 477 beanManager.getEvent().select(ServerBootstrap.class, qualifiersArray).fire(returnValue); 478 return returnValue; 479 }); 480 } 481 482 private static final ChannelFactory<? extends ServerChannel> getChannelFactory(final BeanManager beanManager, 483 final Instance<Object> instance, 484 final Annotation[] qualifiersArray, 485 final boolean lookup) { 486 return acquire(beanManager, 487 instance, 488 new TypeLiteral<ChannelFactory<? extends ServerChannel>>() { 489 private static final long serialVersionUID = 1L; 490 }, 491 qualifiersArray, 492 lookup, 493 (bm, i, qa) -> { 494 final SelectorProvider selectorProvider = getSelectorProvider(bm, i, qa, true); 495 assert selectorProvider != null; 496 return () -> new NioServerSocketChannel(selectorProvider); 497 }); 498 } 499 500 private static final EventLoopGroup getEventLoopGroup(final BeanManager beanManager, 501 final Instance<Object> instance, 502 final Annotation[] qualifiersArray, 503 final boolean lookup) { 504 return acquire(beanManager, 505 instance, 506 EventLoopGroup.class, 507 qualifiersArray, 508 lookup, 509 (bm, i, qa) -> { 510 final EventLoopGroup returnValue = 511 new NioEventLoopGroup(0 /* 0 == default number of threads */, 512 getExecutor(bm, i, qa, true), // null is OK 513 getEventExecutorChooserFactory(bm, i, qa, true), 514 getSelectorProvider(bm, i, qa, true), 515 getSelectStrategyFactory(bm, i, qa, true), 516 getRejectedExecutionHandler(bm, i, qa, true)); 517 // Permit arbitrary customization. (Not much you can do here 518 // except call setIoRatio(int).) 519 beanManager.getEvent().select(EventLoopGroup.class, qa).fire(returnValue); 520 return returnValue; 521 }); 522 } 523 524 private static final Executor getExecutor(final BeanManager beanManager, 525 final Instance<Object> instance, 526 final Annotation[] qualifiersArray, 527 final boolean lookup) { 528 return acquire(beanManager, 529 instance, 530 Executor.class, 531 qualifiersArray, 532 lookup, 533 (bm, i, qa) -> null); 534 } 535 536 private static final RejectedExecutionHandler getRejectedExecutionHandler(final BeanManager beanManager, 537 final Instance<Object> instance, 538 final Annotation[] qualifiersArray, 539 final boolean lookup) { 540 return acquire(beanManager, 541 instance, 542 RejectedExecutionHandler.class, 543 qualifiersArray, 544 lookup, 545 (bm, i, qa) -> RejectedExecutionHandlers.reject()); 546 } 547 548 private static final SelectorProvider getSelectorProvider(final BeanManager beanManager, 549 final Instance<Object> instance, 550 final Annotation[] qualifiersArray, 551 final boolean lookup) { 552 return acquire(beanManager, 553 instance, 554 SelectorProvider.class, 555 qualifiersArray, 556 lookup, 557 (bm, i, qa) -> SelectorProvider.provider()); 558 } 559 560 private static final SelectStrategyFactory getSelectStrategyFactory(final BeanManager beanManager, 561 final Instance<Object> instance, 562 final Annotation[] qualifiersArray, 563 final boolean lookup) { 564 return acquire(beanManager, 565 instance, 566 SelectStrategyFactory.class, 567 qualifiersArray, 568 lookup, 569 (bm, i, qa) -> DefaultSelectStrategyFactory.INSTANCE); 570 } 571 572 private static final EventExecutorChooserFactory getEventExecutorChooserFactory(final BeanManager beanManager, 573 final Instance<Object> instance, 574 final Annotation[] qualifiersArray, 575 final boolean lookup) { 576 return acquire(beanManager, 577 instance, 578 EventExecutorChooserFactory.class, 579 qualifiersArray, 580 lookup, 581 (bm, i, qa) -> DefaultEventExecutorChooserFactory.INSTANCE); 582 } 583 584 585 /* 586 * Static utility methods. 587 */ 588 589 590 private static final <T> T acquire(final BeanManager beanManager, 591 final Instance<Object> instance, 592 final TypeLiteral<T> typeLiteral, 593 final Annotation[] qualifiersArray, 594 final boolean lookup, 595 final DefaultValueFunction<? extends T> defaultValueFunction) { 596 Objects.requireNonNull(beanManager); 597 Objects.requireNonNull(instance); 598 Objects.requireNonNull(typeLiteral); 599 Objects.requireNonNull(defaultValueFunction); 600 601 final T returnValue; 602 final Instance<? extends T> tInstance; 603 if (lookup) { 604 if (qualifiersArray == null || qualifiersArray.length <= 0) { 605 tInstance = instance.select(typeLiteral); 606 } else { 607 tInstance = instance.select(typeLiteral, qualifiersArray); 608 } 609 } else { 610 tInstance = null; 611 } 612 if (tInstance == null || tInstance.isUnsatisfied()) { 613 returnValue = defaultValueFunction.getDefaultValue(beanManager, instance, qualifiersArray); 614 } else { 615 returnValue = tInstance.get(); 616 } 617 return returnValue; 618 } 619 620 private static final <T> T acquire(final BeanManager beanManager, 621 final Instance<Object> instance, 622 final Class<T> cls, 623 final Annotation[] qualifiersArray, 624 final boolean lookup, 625 final DefaultValueFunction<? extends T> defaultValueFunction) { 626 Objects.requireNonNull(beanManager); 627 Objects.requireNonNull(instance); 628 Objects.requireNonNull(cls); 629 Objects.requireNonNull(defaultValueFunction); 630 631 final T returnValue; 632 final Instance<? extends T> tInstance; 633 if (lookup) { 634 if (qualifiersArray == null || qualifiersArray.length <= 0) { 635 tInstance = instance.select(cls); 636 } else { 637 tInstance = instance.select(cls, qualifiersArray); 638 } 639 } else { 640 tInstance = null; 641 } 642 if (tInstance == null || tInstance.isUnsatisfied()) { 643 returnValue = defaultValueFunction.getDefaultValue(beanManager, instance, qualifiersArray); 644 } else { 645 returnValue = tInstance.get(); 646 } 647 return returnValue; 648 } 649 650 private static final void zeroOut(final CountDownLatch latch) { 651 if (latch != null) { 652 while (latch.getCount() > 0L) { 653 latch.countDown(); 654 } 655 assert latch.getCount() == 0L; 656 } 657 } 658 659 private static final Map<String, String> toConfigurationCoordinates(final Set<? extends Annotation> qualifiers) { 660 final Map<String, String> returnValue = new HashMap<>(); 661 if (qualifiers != null && !qualifiers.isEmpty()) { 662 for (final Annotation qualifier : qualifiers) { 663 if (qualifier instanceof Named) { 664 returnValue.put("name", ((Named)qualifier).value()); 665 } else if (!(qualifier instanceof Default) && !(qualifier instanceof Any)) { 666 returnValue.put(qualifier.toString(), ""); 667 } 668 } 669 } 670 return returnValue; 671 } 672 673 674 /* 675 * Inner and nested classes. 676 */ 677 678 679 @FunctionalInterface 680 private static interface DefaultValueFunction<T> { 681 682 T getDefaultValue(final BeanManager beanManager, 683 final Instance<Object> instance, 684 final Annotation[] qualifiersArray); 685 686 } 687 688}