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