diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 000000000..2c1f1a6b4 --- /dev/null +++ b/docs/404.html @@ -0,0 +1,185 @@ + + + + + + + + Mercury + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • +
  • +
  • +
+
+
+
+
+ + +

404

+ +

Page not found

+ + +
+
+ +
+
+ +
+ +
+ +
+ + + + + +
+ + + + + + + + + diff --git a/docs/CHANGELOG/index.html b/docs/CHANGELOG/index.html new file mode 100644 index 000000000..f7fb6e7e1 --- /dev/null +++ b/docs/CHANGELOG/index.html @@ -0,0 +1,1912 @@ + + + + + + + + Release notes - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Changelog

+

Release notes

+

All notable changes to this project will be documented in this file.

+

The format is based on Keep a Changelog, +and this project adheres to Semantic Versioning.

+
+

Version 3.0.16, 8/31/2024

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+
    +
  1. Updated documentation
  2. +
  3. AsyncHttpRequest - remove the class type variable in the data model
  4. +
  5. MultiLevelMap - update the removeElement method
  6. +
  7. OSS update - GSON 2.11.0, classpath 4.8.174, guava 33.3.0-jre, vertx 2.5.9, spring boot 3.3.3, spring framework 5.3.39
  8. +
+
+

Version 3.0.15, 5/1/2024

+

This version supercedes 3.0.13 and 3.0.14 due to updated data structure +for static content handling.

+

Added

+
    +
  1. Added optional static-content.no-cache-pages in rest.yaml
  2. +
  3. AsyncHttpClientLoader
  4. +
+

Removed

+

N/A

+

Changed

+
    +
  1. Updated data structure for static-content section in rest.yaml
  2. +
  3. Fixed bug for setting multiple HTTP cookies
  4. +
  5. Unified configuration file prefix "yaml."
  6. +
+
+

Version 3.0.14, 4/28/2024

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+

Updated syntax for static-content-filter section in rest.yaml

+
+

Version 3.0.13, 4/28/2024

+

Added

+

Added optional static content HTTP-GET request filter in rest.yaml

+

Removed

+

N/A

+

Changed

+

Updated guava to version 33.1.0-jre

+
+

Version 3.0.12, 4/24/2024

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+

Enhanced OptionalService annotation

+
+

Version 3.0.11, 4/23/2024

+

Backport KernelThreadRunner from Mercury Composable 3.1

+

Added

+

KernelThreadRunner annotation

+

Removed

+

CoroutineRunner annotation

+

Changed

+
    +
  1. +

    Set default execution strategy to "coroutine". To tell the system to run a function + using kernel thread pool, add the KernelThreadRunner annotation.

    +
  2. +
  3. +

    Update PersistentWsClient to use vertx WebSocketClient.

    +
  4. +
  5. +

    Upgrade netty to version 4.1.109.Final.

    +
  6. +
+
+

Version 3.0.10, 3/19/2024

+

Added

+

Added "app-config-reader.yml" file in the resources folder so that you can override +the default application configuration files.

+

Removed

+

N/A

+

Changed

+
    +
  1. Open sources library update (Spring Boot 3.2.4, Vertx 4.5.5)
  2. +
  3. Improve AppConfigReader and ConfigReader to use the app-config-reader.yml file.
  4. +
+
+

Version 3.0.9, 2/9/2024

+

Added

+
    +
  1. AutoStart to run application as Spring Boot if the rest-spring-3 library is packaged in app
  2. +
+

Removed

+
    +
  1. Bugfix: removed websocket client connection timeout that causes the first connection to drop after one minute
  2. +
+

Changed

+
    +
  1. Open sources library update (Spring Boot 3.2.2, Vertx 4.5.3 and MsgPack 0.9.8)
  2. +
  3. Rename application parameter "event.worker.pool" to "kernel.thread.pool"
  4. +
  5. Support user defined serializer with PreLoad annotation and platform API
  6. +
+
+

Version 3.0.8, 1/27/2024

+

Added

+

N/A

+

Removed

+

ActiveMQ and Tibco cloud connectors

+

Changed

+

Updated AsyncHttpRequest and CryptoApi

+
+

Version 3.0.7, 12/23/2023

+

Added

+

Print out basic JVM information before startup for verification of base container image.

+

Removed

+

Removed Maven Shade packager

+

Changed

+

Updated open sources libraries to address security vulnerabilities

+
    +
  1. Spring Boot 2/3 to version 2.7.18 and 3.2.1 respectively
  2. +
  3. Tomcat 9.0.84
  4. +
  5. Vertx 4.5.1
  6. +
  7. Classgraph 4.8.165
  8. +
  9. Netty 4.1.104.Final
  10. +
  11. slf4j API 2.0.9
  12. +
  13. log4j2 2.22.0
  14. +
  15. Kotlin 1.9.22
  16. +
  17. Artemis 2.31.2
  18. +
  19. Hazelcast 5.3.6
  20. +
  21. Guava 33.0.0-jre
  22. +
+
+

Version 3.0.6, 10/26/2023

+

Added

+

Enhanced Benchmark tool to support "Event over HTTP" protocol to evaluate performance +efficiency for commmunication between application containers using HTTP.

+

Removed

+

N/A

+

Changed

+

Updated open sources libraries

+
    +
  1. Spring Boot 2/3 to version 2.7.17 and 3.1.5 respectively
  2. +
  3. Kafka-client 3.6.0
  4. +
+
+

Version 3.0.5, 10/21/2023

+

Added

+

Support two executable JAR packaging system: +1. Maven Shade packager +2. Spring Boot packager

+

Starting from version 3.0.5, we have replaced Spring Boot packager with Maven Shade. +This avoids a classpath edge case for Spring Boot packager when running kafka-client +under Java 11 or higher.

+

Maven Shade also results in smaller executable JAR size.

+

Removed

+

N/A

+

Changed

+

Updated open sources libraries

+
    +
  1. Spring-Boot 2.7.16 / 3.1.4
  2. +
  3. classgraph 4.8.163
  4. +
  5. snakeyaml 2.2
  6. +
  7. kotlin 1.9.10
  8. +
  9. vertx 4.4.6
  10. +
  11. guava 32.1.3-jre
  12. +
  13. msgpack 0.9.6
  14. +
  15. slj4j 2.0.9
  16. +
  17. zookeeper 3.7.2
  18. +
+

The "/info/lib" admin endpoint has been enhanced to list library dependencies for executable JAR +generated by either Maven Shade or Spring Boot Packager.

+

Improved ConfigReader to recognize both ".yml" and ".yaml" extensions and their uses are interchangeable.

+
+

Version 3.0.4, 8/6/2023

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+

Updated open sources libraries

+
    +
  1. Spring-Boot 2.7.14 / 3.1.2
  2. +
  3. Kafka-client 3.5.1
  4. +
  5. classgraph 4.8.161
  6. +
  7. guava 32.1.2-jre
  8. +
  9. msgpack 0.9.5
  10. +
+
+

Version 3.0.3, 6/27/2023

+

Added

+
    +
  1. File extension to MIME type mapping for static HTML file handling
  2. +
+

Removed

+

N/A

+

Changed

+
    +
  1. Open sources library update - Kotlin version 1.9.0
  2. +
+
+

Version 3.0.2, 6/9/2023

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+
    +
  1. Consistent exception handling for Event API endpoint
  2. +
  3. Open sources lib update - Vertx 4.4.4, Spring Boot 2.7.13, Spring Boot 3.1.1, classgraph 4.8.160, guava 32.0.1-jre
  4. +
+
+

Version 3.0.1, 6/5/2023

+

In this release, we have replace Google HTTP Client with vertx non-blocking WebClient. +We also tested compatibility up to OpenJDK version 20 and maven 3.9.2.

+

Added

+

When "x-raw-xml" HTTP request header is set to "true", the AsyncHttpClient will skip the built-in +XML serialization so that your application can retrieve the original XML text.

+

Removed

+

Retire Google HTTP client

+

Changed

+

Upgrade maven plugin versions.

+
+

Version 3.0.0, 4/18/2023

+

This is a major release with some breaking changes. Please refer to Chapter-10 (Migration guide) for details. +This version brings the best of preemptive and cooperating multitasking to Java (version 1.8 to 19) before +Java 19 virtual thread feature becomes officially available.

+

Added

+
    +
  1. Function execution engine supporting kernel thread pool, Kotlin coroutine and suspend function
  2. +
  3. "Event over HTTP" service for inter-container communication
  4. +
  5. Support for Spring Boot version 3 and WebFlux
  6. +
  7. Sample code for a pre-configured Spring Boot 3 application
  8. +
+

Removed

+
    +
  1. Remove blocking APIs from platform-core
  2. +
  3. Retire PM2 process manager sample script due to compatibility issue
  4. +
+

Changed

+
    +
  1. Refactor "async.http.request" to use vertx web client for non-blocking operation
  2. +
  3. Update log4j2 version 2.20.0 and slf4j version 2.0.7 in platform-core
  4. +
  5. Update JBoss RestEasy JAX_RS to version 3.15.6.Final in rest-spring
  6. +
  7. Update vertx to 4.4.2
  8. +
  9. Update Spring Boot parent pom to 2.7.12 and 3.1.0 for spring boot 2 and 3 respectively
  10. +
  11. Remove com.fasterxml.classmate dependency from rest-spring
  12. +
+
+

Version 2.8.0, 3/20/2023

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+
    +
  1. Improved load balancing in cloud-connector
  2. +
  3. Filter URI to avoid XSS attack
  4. +
  5. Upgrade to SnakeYaml 2.0 and patch Spring Boot 2.6.8 for compatibility with it
  6. +
  7. Upgrade to Vertx 4.4.0, classgraph 4.8.157, tomcat 9.0.73
  8. +
+
+

Version 2.7.1, 12/22/2022

+

Added

+
    +
  1. standalone benchmark report app
  2. +
  3. client and server benchmark apps
  4. +
  5. add timeout tag to RPC events
  6. +
+

Removed

+

N/A

+

Changed

+
    +
  1. Updated open sources dependencies
  2. +
  3. Netty 4.1.86.Final
  4. +
  5. Tomcat 9.0.69
  6. +
  7. Vertx 4.3.6
  8. +
  9. classgraph 4.8.152
  10. +
  11. +

    google-http-client 1.42.3

    +
  12. +
  13. +

    Improved unit tests to use assertThrows to evaluate exception

    +
  14. +
  15. Enhanced AsyncHttpRequest serialization
  16. +
+
+

Version 2.7.0, 11/11/2022

+

In this version, REST automation code is moved to platform-core such that REST and Websocket +service can share the same port.

+

Added

+
    +
  1. AsyncObjectStreamReader is added for non-blocking read operation from an object stream.
  2. +
  3. Support of LocalDateTime in SimpleMapper
  4. +
  5. Add "removeElement" method to MultiLevelMap
  6. +
  7. Automatically convert a map to a PoJo when the sender does not specify class in event body
  8. +
+

Removed

+

N/A

+

Changed

+
    +
  1. REST automation becomes part of platform-core and it can co-exist with Spring Web in the rest-spring module
  2. +
  3. Enforce Spring Boot lifecycle management such that user apps will start after Spring Boot has loaded all components
  4. +
  5. Update netty to version 4.1.84.Final
  6. +
+
+

Version 2.6.0, 10/13/2022

+

In this version, websocket notification example code has been removed from the REST automation system. +If your application uses this feature, please recover the code from version 2.5.0 and refactor it as a +separate library.

+

Added

+

N/A

+

Removed

+

Simplify REST automation system by removing websocket notification example in REST automation.

+

Changed

+
    +
  1. Replace Tomcat websocket server with Vertx non-blocking websocket server library
  2. +
  3. Update netty to version 4.1.79.Final
  4. +
  5. Update kafka client to version 2.8.2
  6. +
  7. Update snake yaml to version 1.33
  8. +
  9. Update gson to version 2.9.1
  10. +
+
+

Version 2.5.0, 9/10/2022

+

Added

+

New Preload annotation class to automate pre-registration of LambdaFunction.

+

Removed

+

Removed Spring framework and Tomcat dependencies from platform-core so that the core library can be applied +to legacy J2EE application without library conflict.

+

Changed

+
    +
  1. Bugfix for proper housekeeping of future events.
  2. +
  3. Make Gson and MsgPack handling of integer/long consistent
  4. +
+

Updated open sources libraries.

+
    +
  1. Eclipse vertx-core version 4.3.4
  2. +
  3. MsgPack version 0.9.3
  4. +
  5. Google httpclient version 1.42.2
  6. +
  7. SnakeYaml version 1.31
  8. +
+
+

Version 2.3.6, 6/21/2022

+

Added

+

Support more than one event stream cluster. User application can share the same event stream cluster +for pub/sub or connect to an alternative cluster for pub/sub use cases.

+

Removed

+

N/A

+

Changed

+

Cloud connector libraries update to Hazelcast 5.1.2

+
+

Version 2.3.5, 5/30/2022

+

Added

+

Add tagging feature to handle language connector's routing and exception handling

+

Removed

+

Remove language pack's pub/sub broadcast feature

+

Changed

+
    +
  1. Update Spring Boot parent to version 2.6.8 to fetch Netty 4.1.77 and Spring Framework 5.3.20
  2. +
  3. Streamlined language connector transport protocol for compatibility with both Python and Node.js
  4. +
+
+

Version 2.3.4, 5/14/2022

+

Added

+

N/A

+

Removed

+
    +
  1. Remove swagger-ui distribution from api-playground such that developer can clone the latest version
  2. +
+

Changed

+
    +
  1. Update application.properties (from spring.resources.static-locations to spring.web.resources.static-locations)
  2. +
  3. Update log4j, Tomcat and netty library version using Spring parent 2.6.6
  4. +
+
+

Version 2.3.3, 3/30/2022

+

Added

+

Enhanced AsyncRequest to handle non-blocking fork-n-join

+

Removed

+

N/A

+

Changed

+

Upgrade Spring Boot from 2.6.3 to 2.6.6

+
+

Version 2.3.2, 2/21/2022

+

Added

+

Add support of queue API in native pub/sub module for improved ESB compatibility

+

Removed

+

N/A

+

Changed

+

N/A

+
+

Version 2.3.1, 2/19/2022

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+
    +
  1. Update Vertx to version 4.2.4
  2. +
  3. Update Tomcat to version 5.0.58
  4. +
  5. Use Tomcat websocket server for presence monitors
  6. +
  7. Bugfix - Simple Scheduler's leader election searches peers correctly
  8. +
+
+

Version 2.3.0, 1/28/2022

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+
    +
  1. Update copyright notice
  2. +
  3. Update Vertx to version 4.2.3
  4. +
  5. Bugfix - RSA key generator supporting key length from 1024 to 4096 bits
  6. +
  7. CryptoAPI - support different AES algorithms and custom IV
  8. +
  9. Update Spring Boot to version 2.6.3
  10. +
+
+

Version 2.2.3, 12/29/2021

+

Added

+
    +
  1. Transaction journaling
  2. +
  3. Add parameter distributed.trace.aggregation in application.properties such that trace aggregation + may be disabled.
  4. +
+

Removed

+

N/A

+

Changed

+
    +
  1. Update JBoss RestEasy library to 3.15.3.Final
  2. +
  3. Improved po.search(route) to scan local and remote service registries. Added "remoteOnly" selection.
  4. +
  5. Fix bug in releasing presence monitor topic for specific closed user group
  6. +
  7. Update Apache log4j to version 2.17.1
  8. +
  9. Update Spring Boot parent to version 2.6.1
  10. +
  11. Update Netty to version 4.1.72.Final
  12. +
  13. Update Vertx to version 4.2.2
  14. +
  15. Convenient class "UserNotification" for backend service to publish events to the UI when REST automation is deployed
  16. +
+
+

Version 2.2.2, 11/12/2021

+

Added

+
    +
  1. User defined API authentication functions can be selected using custom HTTP request header
  2. +
  3. "Exception chaining" feature in EventEnvelope
  4. +
  5. New "deferred.commit.log" parameter for backward compatibility with older PowerMock in unit tests
  6. +
+

Removed

+

N/A

+

Changed

+
    +
  1. Improved and streamlined SimpleXmlParser to handle arrays
  2. +
  3. Bugfix for file upload in Service Gateway (REST automation library)
  4. +
  5. Update Tomcat library from 9.0.50 to 9.0.54
  6. +
  7. Update Spring Boot library to 2.5.6
  8. +
  9. Update GSON library to 2.8.9
  10. +
+
+

Version 2.2.1, 10/1/2021

+

Added

+

Callback function can implement ServiceExceptionHandler to catch exception. It adds the onError() method.

+

Removed

+

N/A

+

Changed

+

Open sources library update - Vert.x 4.1.3, Netty 4.1.68-Final

+
+

Version 2.1.1, 9/10/2021

+

Added

+
    +
  1. User defined PoJo and Generics mapping
  2. +
  3. Standardized serializers for default case, snake_case and camelCase
  4. +
  5. Support of EventEnvelope as input parameter in TypedLambdaFunction so application function can inspect event's + metadata
  6. +
  7. Application can subscribe to life cycle events of other application instances
  8. +
+

Removed

+

N/A

+

Changed

+
    +
  1. Replace Tomcat websocket server engine with Vertx in presence monitor for higher performance
  2. +
  3. Bugfix for MsgPack transport of integer, long, BigInteger and BigDecimal
  4. +
+
+

Version 2.1.0, 7/25/2021

+

Added

+
    +
  1. Multicast - application can define a multicast.yaml config to relay events to more than one target service.
  2. +
  3. StreamFunction - function that allows the application to control back-pressure
  4. +
+

Removed

+

"object.streams.io" route is removed from platform-core

+

Changed

+
    +
  1. Elastic Queue - Refactored using Oracle Berkeley DB
  2. +
  3. Object stream I/O - simplified design using the new StreamFunction feature
  4. +
  5. Open sources library update - Spring Boot 2.5.2, Tomcat 9.0.50, Vert.x 4.1.1, Netty 4.1.66-Final
  6. +
+
+

Version 2.0.0, 5/5/2021

+

Vert.x is introduced as the in-memory event bus

+

Added

+
    +
  1. ActiveMQ and Tibco connectors
  2. +
  3. Admin endpoints to stop, suspend and resume an application instance
  4. +
  5. Handle edge case to detect stalled application instances
  6. +
  7. Add "isStreamingPubSub" method to the PubSub interface
  8. +
+

Removed

+
    +
  1. Event Node event stream emulator has been retired. You may use standalone Kafka server as a replacement for + development and testing in your laptop.
  2. +
  3. Multi-tenancy namespace configuration has been retired. It is replaced by the "closed user group" feature.
  4. +
+

Changed

+
    +
  1. Refactored Kafka and Hazelcast connectors to support virtual topics and closed user groups.
  2. +
  3. Updated ConfigReader to be consistent with Spring value substitution logic for application properties
  4. +
  5. Replace Akka actor system with Vert.x event bus
  6. +
  7. Common code for various cloud connectors consolidated into cloud core libraries
  8. +
+
+

Version 1.13.0, 1/15/2021

+

Version 1.13.0 is the last version that uses Akka as the in-memory event system.

+
+

Version 1.12.66, 1/15/2021

+

Added

+
    +
  1. A simple websocket notification service is integrated into the REST automation system
  2. +
  3. Seamless migration feature is added to the REST automation system
  4. +
+

Removed

+

Legacy websocket notification example application

+

Changed

+

N/A

+
+

Version 1.12.65, 12/9/2020

+

Added

+
    +
  1. "kafka.pubsub" is added as a cloud service
  2. +
  3. File download example in the lambda-example project
  4. +
  5. "trace.log.header" added to application.properties - when tracing is enabled, this inserts the trace-ID of the + transaction in the log context. For more details, please refer to the Developer Guide
  6. +
  7. Add API to pub/sub engine to support creation of topic with partitions
  8. +
  9. TypedLambdaFunction is added so that developer can predefine input and output classes in a service without casting
  10. +
+

Removed

+

N/A

+

Changed

+
    +
  1. Decouple Kafka pub/sub from kafka connector so that native pub/sub can be used when application is running in + standalone mode
  2. +
  3. Rename "relay" to "targetHost" in AsyncHttpRequest data model
  4. +
  5. Enhanced routing table distribution by sending a complete list of route tables, thus reducing network admin traffic.
  6. +
+
+

Version 1.12.64, 9/28/2020

+

Added

+

If predictable topic is set, application instances will report their predictable topics as "instance ID" +to the presence monitor. This improves visibility when a developer tests their application in "hybrid" mode. +i.e. running the app locally and connect to the cloud remotely for event streams and cloud resources.

+

Removed

+

N/A

+

Changed

+

N/A

+
+

Version 1.12.63, 8/27/2020

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+

Improved Kafka producer and consumer pairing

+
+

Version 1.12.62, 8/12/2020

+

Added

+

New presence monitor's admin endpoint for the operator to force routing table synchronization ("/api/ping/now")

+

Removed

+

N/A

+

Changed

+

Improved routing table integrity check

+
+

Version 1.12.61, 8/8/2020

+

Added

+

Event stream systems like Kafka assume topic to be used long term. +This version adds support to reuse the same topic when an application instance restarts.

+

You can create a predictable topic using unique application name and instance ID. +For example, with Kubernetes, you can use the POD name as the unique application instance topic.

+

Removed

+

N/A

+

Changed

+

N/A

+
+

Version 1.12.56, 8/4/2020

+

Added

+

Automate trace for fork-n-join use case

+

Removed

+

N/A

+

Changed

+

N/A

+
+

Version 1.12.55, 7/19/2020

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+

Improved distributed trace - set the "from" address in EventEnvelope automatically.

+
+

Version 1.12.54, 7/10/2020

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+

Application life-cycle management - User provided main application(s) will be started after Spring Boot declares web +application ready. This ensures correct Spring autowiring or dependencies are available.

+

Bugfix for locale - String.format(float) returns comma as decimal point that breaks number parser. +Replace with BigDecimal decimal point scaling.

+

Bugfix for Tomcat 9.0.35 - Change Async servlet default timeout from 30 seconds to -1 so the system can handle the +whole life-cycle directly.

+
+

Version 1.12.52, 6/11/2020

+

Added

+
    +
  1. new "search" method in Post Office to return a list of application instances for a service
  2. +
  3. simple "cron" job scheduler as an extension project
  4. +
  5. add "sequence" to MainApplication annotation for orderly execution when more than one MainApplication is available
  6. +
  7. support "Optional" object in EventEnvelope so a LambdaFunction can read and return Optional
  8. +
+

Removed

+

N/A

+

Changed

+
    +
  1. The rest-spring library has been updated to support both JAR and WAR deployment
  2. +
  3. All pom.xml files updated accordingly
  4. +
  5. PersistentWsClient will back off for 10 seconds when disconnected by remote host
  6. +
+
+

Version 1.12.50, 5/20/2020

+

Added

+
    +
  1. Payload segmentation
  2. +
+

For large payload in an event, the payload is automatically segmented into 64 KB segments. + When there are more than one target application instances, the system ensures that the segments of the same event + is delivered to exactly the same target.

+
    +
  1. PersistentWsClient added - generalized persistent websocket client for Event Node, Kafka reporter and Hazelcast + reporter.
  2. +
+

Removed

+

N/A

+

Changed

+
    +
  1. Code cleaning to improve consistency
  2. +
  3. Upgraded to hibernate-validator to v6.1.5.Final and Hazelcast version 4.0.1
  4. +
  5. REST automation is provided as a library and an application to handle different use cases
  6. +
+
+

Version 1.12.40, 5/4/2020

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+

For security reason, upgrade log4j to version 2.13.2

+
+

Version 1.12.39, 5/3/2020

+

Added

+

Use RestEasy JAX-RS library

+

Removed

+

For security reason, removed Jersey JAX-RS library

+

Changed

+
    +
  1. Updated RestLoader to initialize RestEasy servlet dispatcher
  2. +
  3. Support nested arrays in MultiLevelMap
  4. +
+
+

Version 1.12.36, 4/16/2020

+

Added

+

N/A

+

Removed

+

For simplicity, retire route-substitution admin endpoint. Route substitution uses a simple static table in +route-substitution.yaml.

+

Changed

+

N/A

+
+

Version 1.12.35, 4/12/2020

+

Added

+

N/A

+

Removed

+

SimpleRBAC class is retired

+

Changed

+
    +
  1. Improved ConfigReader and AppConfigReader with automatic key-value normalization for YAML and JSON files
  2. +
  3. Improved pub/sub module in kafka-connector
  4. +
+
+

Version 1.12.34, 3/28/2020

+

Added

+

N/A

+

Removed

+

Retired proprietary config manager since we can use the "BeforeApplication" approach to load config from Kubernetes +configMap or other systems of config record.

+

Changed

+
    +
  1. Added "isZero" method to the SimpleMapper class
  2. +
  3. Convert BigDecimal to string without scientific notation (i.e. toPlainString instead of toString)
  4. +
  5. Corresponding unit tests added to verify behavior
  6. +
+
+

Version 1.12.32, 3/14/2020

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+

Kafka-connector will shutdown application instance when the EventProducer cannot send event to Kafka. +This would allow the infrastructure to restart application instance automatically.

+
+

Version 1.12.31, 2/26/2020

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+
    +
  1. Kafka-connector now supports external service provider for Kafka properties and credentials. + If your application implements a function with route name "kafka.properties.provider" before connecting to cloud, + the kafka-connector will retrieve kafka credentials on demand. This addresses case when kafka credentials change + after application start-up.
  2. +
  3. Interceptors are designed to forward requests and thus they do not generate replies. However, if you implement a + function as an EventInterceptor, your function can throw exception just like a regular function and the exception + will be returned to the calling function. This makes it easier to write interceptors.
  4. +
+
+

Version 1.12.30, 2/6/2020

+

Added

+
    +
  1. Expose "async.http.request" as a PUBLIC function ("HttpClient as a service")
  2. +
+

Removed

+

N/A

+

Changed

+
    +
  1. Improved Hazelcast client connection stability
  2. +
  3. Improved Kafka native pub/sub
  4. +
+
+

Version 1.12.29, 1/10/2020

+

Added

+
    +
  1. Rest-automation will transport X-Trace-Id from/to Http request/response, therefore extending distributed trace + across systems that support the X-Trace-Id HTTP header.
  2. +
  3. Added endpoint and service to shutdown application instance.
  4. +
+

Removed

+

N/A

+

Changed

+
    +
  1. Updated SimpleXmlParser with XML External Entity (XXE) injection prevention.
  2. +
  3. Bug fix for hazelcast recovery logic - when a hazelcast node is down, the app instance will restart the hazelcast + client and reset routing table correctly.
  4. +
  5. HSTS header insertion is optional so that we can disable it to avoid duplicated header when API gateway is doing it.
  6. +
+
+

Version 1.12.26, 1/4/2020

+

Added

+

Feature to disable PoJo deserialization so that caller can decide if the result set should be in PoJo or a Map.

+

Removed

+

N/A

+

Changed

+
    +
  1. Simplified key management for Event Node
  2. +
  3. AsyncHttpRequest case insensitivity for headers, cookies, path parameters and session key-values
  4. +
  5. Make built-in configuration management optional
  6. +
+
+

Version 1.12.19, 12/28/2019

+

Added

+

Added HTTP relay feature in rest-automation project

+

Removed

+

N/A

+

Changed

+
    +
  1. Improved hazelcast retry and peer discovery logic
  2. +
  3. Refactored rest-automation's service gateway module to use AsyncHttpRequest
  4. +
  5. Info endpoint to show routing table of a peer
  6. +
+
+

Version 1.12.17, 12/16/2019

+

Added

+
    +
  1. Simple configuration management is added to event-node, hazelcast-presence and kafka-presence monitors
  2. +
  3. Added BeforeApplication annotation - this allows user application to execute some setup logic before the main + application starts. e.g. modifying parameters in application.properties
  4. +
  5. Added API playground as a convenient standalone application to render OpenAPI 2.0 and 3.0 yaml and json files
  6. +
  7. Added argument parser in rest-automation helper app to use a static HTML folder in the local file system if + arguments -html file_path is given when starting the JAR file.
  8. +
+

Removed

+

N/A

+

Changed

+
    +
  1. Kafka publisher timeout value changed from 10 to 20 seconds
  2. +
  3. Log a warning when Kafka takes more than 5 seconds to send an event
  4. +
+
+

Version 1.12.14, 11/20/2019

+

Added

+
    +
  1. getRoute() method is added to PostOffice to facilitate RBAC
  2. +
  3. The route name of the current service is added to an outgoing event when the "from" field is not present
  4. +
  5. Simple RBAC using YAML configuration instead of code
  6. +
+

Removed

+

N/A

+

Changed

+

Updated Spring Boot to v2.2.1

+
+

Version 1.12.12, 10/26/2019

+

Added

+

Multi-tenancy support for event streams (Hazelcast and Kafka). +This allows the use of a single event stream cluster for multiple non-prod environments. +For production, it must use a separate event stream cluster for security reason.

+

Removed

+

N/A

+

Changed

+
    +
  1. logging framework changed from logback to log4j2 (version 2.12.1)
  2. +
  3. Use JSR-356 websocket annotated ClientEndpoint
  4. +
  5. Improved websocket reconnection logic
  6. +
+
+

Version 1.12.9, 9/14/2019

+

Added

+
    +
  1. Distributed tracing implemented in platform-core and rest-automation
  2. +
  3. Improved HTTP header transformation for rest-automation
  4. +
+

Removed

+

N/A

+

Changed

+

language pack API key obtained from environment variable

+
+

Version 1.12.8, 8/15/2019

+

Added

+

N/A

+

Removed

+

rest-core subproject has been merged with rest-spring

+

Changed

+

N/A

+
+

Version 1.12.7, 7/15/2019

+

Added

+
    +
  1. Periodic routing table integrity check (15 minutes)
  2. +
  3. Set kafka read pointer to the beginning for new application instances except presence monitor
  4. +
  5. REST automation helper application in the "extensions" project
  6. +
  7. Support service discovery of multiple routes in the updated PostOffice's exists() method
  8. +
  9. logback to set log level based on environment variable LOG_LEVEL (default is INFO)
  10. +
+

Removed

+

N/A

+

Changed

+

Minor refactoring of kafka-connector and hazelcast-connector to ensure that they can coexist if you want to include +both of these dependencies in your project.

+

This is for convenience of dev and testing. In production, please select only one cloud connector library to reduce +memory footprint.

+
+

Version 1.12.4, 6/24/2019

+

Added

+

Add inactivity expiry timer to ObjectStreamIO so that house-keeper can clean up resources that are idle

+

Removed

+

N/A

+

Changed

+
    +
  1. Disable HTML encape sequence for GSON serializer
  2. +
  3. Bug fix for GSON serialization optimization
  4. +
  5. Bug fix for Object Stream housekeeper
  6. +
+

By default, GSON serializer converts all numbers to double, resulting in unwanted decimal point for integer and long. +To handle custom map serialization for correct representation of numbers, an unintended side effect was introduced in +earlier releases.

+

List of inner PoJo would be incorrectly serialized as map, resulting in casting exception. +This release resolves this issue.

+
+

Version 1.12.1, 6/10/2019

+

Added

+
    +
  1. Store-n-forward pub/sub API will be automatically enabled if the underlying cloud connector supports it. e.g. kafka
  2. +
  3. ObjectStreamIO, a convenient wrapper class, to provide event stream I/O API.
  4. +
  5. Object stream feature is now a standard feature instead of optional.
  6. +
  7. Deferred delivery added to language connector.
  8. +
+

Removed

+

N/A

+

Changed

+

N/A

+
+

Version 1.11.40, 5/25/2019

+

Added

+
    +
  1. Route substitution for simple versioning use case
  2. +
  3. Add "Strict Transport Security" header if HTTPS (https://tools.ietf.org/html/rfc6797)
  4. +
  5. Event stream connector for Kafka
  6. +
  7. Distributed housekeeper feature for Hazelcast connector
  8. +
+

Removed

+

System log service

+

Changed

+

Refactoring of Hazelcast event stream connector library to sync up with the new Kafka connector.

+
+

Version 1.11.39, 4/30/2019

+

Added

+

Language-support service application for Python, Node.js and Go, etc. +Python language pack project is available at https://github.com/Accenture/mercury-python

+

Removed

+

N/A

+

Changed

+
    +
  1. replace Jackson serialization engine with Gson (platform-core project)
  2. +
  3. replace Apache HttpClient with Google Http Client (rest-spring)
  4. +
  5. remove Jackson dependencies from Spring Boot (rest-spring)
  6. +
  7. interceptor improvement
  8. +
+
+

Version 1.11.33, 3/25/2019

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+
    +
  1. Move safe.data.models validation rules from EventEnvelope to SimpleMapper
  2. +
  3. Apache fluent HTTP client downgraded to version 4.5.6 because the pom file in 4.5.7 is invalid
  4. +
+
+

Version 1.11.30, 3/7/2019

+

Added

+

Added retry logic in persistent queue when OS cannot update local file metadata in real-time for Windows based machine.

+

Removed

+

N/A

+

Changed

+

pom.xml changes - update with latest 3rd party open sources dependencies.

+
+

Version 1.11.29, 1/25/2019

+

Added

+

platform-core

+
    +
  1. Support for long running functions so that any long queries will not block the rest of the system.
  2. +
  3. "safe.data.models" is available as an option in the application.properties. + This is an additional security measure to protect against Jackson deserialization vulnerability. + See example below:
  4. +
+
#
+# additional security to protect against model injection
+# comma separated list of model packages that are considered safe to be used for object deserialization
+#
+#safe.data.models=com.accenture.models
+
+

rest-spring

+

"/env" endpoint is added. See sample application.properties below:

+
#
+# environment and system properties to be exposed to the "/env" admin endpoint
+#
+show.env.variables=USER, TEST
+show.application.properties=server.port, cloud.connector
+
+

Removed

+

N/A

+

Changed

+

platform-core

+

Use Java Future and an elastic cached thread pool for executing user functions.

+

Fixed

+

N/A

+
+

Version 1.11.28, 12/20/2018

+

Added

+

Hazelcast support is added. This includes two projects (hazelcast-connector and hazelcast-presence).

+

Hazelcast-connector is a cloud connector library. Hazelcast-presence is the "Presence Monitor" for monitoring the +presence status of each application instance.

+

Removed

+

platform-core

+

The "fixed resource manager" feature is removed because the same outcome can be achieved at the application level. +e.g. The application can broadcast requests to multiple application instances with the same route name and use a +callback function to receive response asynchronously. The services can provide resource metrics so that the caller +can decide which is the most available instance to contact.

+

For simplicity, resources management is better left to the cloud platform or the application itself.

+

Changed

+

N/A

+

Fixed

+

N/A

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/CODE_OF_CONDUCT/index.html b/docs/CODE_OF_CONDUCT/index.html new file mode 100644 index 000000000..0b7b37feb --- /dev/null +++ b/docs/CODE_OF_CONDUCT/index.html @@ -0,0 +1,268 @@ + + + + + + + + Code of Conduct - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Contributor Covenant Code of Conduct

+

Our Pledge

+

In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation.

+

Our Standards

+

Examples of behavior that contributes to creating a positive environment +include:

+
    +
  • Using welcoming and inclusive language
  • +
  • Being respectful of differing viewpoints and experiences
  • +
  • Gracefully accepting constructive criticism
  • +
  • Focusing on what is best for the community
  • +
  • Showing empathy towards other community members
  • +
+

Examples of unacceptable behavior by participants include:

+
    +
  • The use of sexualized language or imagery and unwelcome sexual attention or + advances
  • +
  • Trolling, insulting/derogatory comments, and personal or political attacks
  • +
  • Public or private harassment
  • +
  • Publishing others' private information, such as a physical or electronic + address, without explicit permission
  • +
  • Other conduct which could reasonably be considered inappropriate in a + professional setting
  • +
+

Our Responsibilities

+

Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior.

+

Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful.

+

Scope

+

This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers.

+

Enforcement

+

Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting Kevin Bader (the current project maintainer). All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately.

+

Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership.

+

Attribution

+

This Code of Conduct is adapted from the Contributor Covenant, version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/CONTRIBUTING/index.html b/docs/CONTRIBUTING/index.html new file mode 100644 index 000000000..66242bb4f --- /dev/null +++ b/docs/CONTRIBUTING/index.html @@ -0,0 +1,238 @@ + + + + + + + + Contribution - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Contributing to the Mercury framework

+

Thanks for taking the time to contribute!

+

The following is a set of guidelines for contributing to Mercury and its packages, which are hosted +in the Accenture Organization on GitHub. These are mostly +guidelines, not rules. Use your best judgment, and feel free to propose changes to this document +in a pull request.

+

Code of Conduct

+

This project and everyone participating in it is governed by our +Code of Conduct. By participating, you are expected to uphold this code. +Please report unacceptable behavior to Kevin Bader, who is the current project maintainer.

+

What should I know before I get started?

+

We follow the standard GitHub workflow. +Before submitting a Pull Request:

+
    +
  • Please write tests.
  • +
  • Make sure you run all tests and check for warnings.
  • +
  • Think about whether it makes sense to document the change in some way. For smaller, internal changes, + inline documentation might be sufficient, while more visible ones might warrant a change to + the developer's guide or the README.
  • +
  • Update CHANGELOG.md file with your current change in form of [Type of change e.g. Config, Kafka, .etc] + with a short description of what it is all about and a link to issue or pull request, + and choose a suitable section (i.e., changed, added, fixed, removed, deprecated).
  • +
+

Design Decisions

+

When we make a significant decision in how to write code, or how to maintain the project and +what we can or cannot support, we will document it using +Architecture Decision Records (ADR). +Take a look at the design notes for existing ADRs. +If you have a question around how we do things, check to see if it is documented +there. If it is not documented there, please ask us - chances are you're not the only one +wondering. Of course, also feel free to challenge the decisions by starting a discussion on the +mailing list.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/INCLUSIVITY/index.html b/docs/INCLUSIVITY/index.html new file mode 100644 index 000000000..9f6339115 --- /dev/null +++ b/docs/INCLUSIVITY/index.html @@ -0,0 +1,276 @@ + + + + + + + + Inclusivity - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

TECHNOLOGY INCLUSIVE
LANGUAGE GUIDEBOOK

+

As an organization, Accenture believes in building an inclusive workplace and contributing to a world where equality thrives. Certain terms or expressions can unintentionally harm, perpetuate damaging stereotypes, and insult people. Inclusive language avoids bias, slang terms, and word choices which express derision of groups of people based on race, gender, sexuality, or socioeconomic status. The Accenture North America Technology team created this guidebook to provide Accenture employees with a view into inclusive language and guidance for working to avoid its use—helping to ensure that we communicate with respect, dignity and fairness.

+

How to use this guide?

+

As of 8/2023, Accenture has over 730,000 employees from diverse backgrounds, who perform consulting and delivery work for an equally diverse set of clients and partners. When communicating with your colleagues and representing Accenture, consider the connotation, however unintended, of certain terms in your written and verbal communication. The guidelines are intended to help you recognize non-inclusive words and understand potential meanings that these words might convey. Our goal with these recommendations is not to require you to use specific words, but to ask you to take a moment to consider how your audience may be affected by the language you choose.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Inclusive CategoriesNon-inclusive termReplacementExplanation
Race, Ethnicity & National Originmasterprimary
client
source
leader
Using the terms “master/slave” in this context inappropriately normalizes and minimizes the very large magnitude that slavery and its effects have had in our history.
slavesecondary
replica
follower
blacklistdeny list
block list
The term “blacklist” was first used in the early 1600s to describe a list of those who were under suspicion and thus not to be trusted, whereas “whitelist” referred to those considered acceptable. Accenture does not want to promote the association of “black” and negative, nor the connotation of “white” being the inverse, or positive.
whitelistallow list
approved list
nativeoriginal
core feature
Referring to “native” vs “non-native” to describe technology platforms carries overtones of minimizing the impact of colonialism on native people, and thus minimizes the negative associations the terminology has in the latter context.
non-nativenon-original
non-core feature
Gender & Sexualityman-hourswork-hours
business-hours
When people read the words ‘man’ or ‘he,’ people often picture males only. Usage of the male terminology subtly suggests that only males can perform certain work or hold certain jobs. Gender-neutral terms include the whole audience, and thus using terms such as “business executive” instead of “businessman,” or informally, “folks” instead of “guys” is preferable because it is inclusive.
man-dayswork-days
business-days
Ability Status & (Dis)abilitiessanity check
insanity check
confidence check
quality check
rationality check
Using the “Human Engagement, People First’ approach, putting people - all people - at the center is + important. Denoting ability status in the context of inferior or problematic work implies that people with mental illnesses are inferior, wrong, or incorrect.
dummy variablesindicator variables
ViolenceSTONITH, kill, hitconclude
cease
discontinue
Using the “Human Engagement, People First’ approach, putting people - all people - at the center is + important. Denoting ability status in the context of inferior or problematic work implies that people with mental illnesses are inferior, wrong, or incorrect.
one throat to chokesingle point of contact
primary contact
+ +

This guidebook is a living document and will be updated as terminology evolves. We encourage our users to provide feedback on the effectiveness of this document and we welcome additional suggestions. Contact us at Technology_ProjectElevate@accenture.com.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/ROADMAP/index.html b/docs/ROADMAP/index.html new file mode 100644 index 000000000..716dc2d7c --- /dev/null +++ b/docs/ROADMAP/index.html @@ -0,0 +1,204 @@ + + + + + + + + Roadmap - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Technology roadmap for 2023

+

Language Packs

+

Upgrade Mercury python and node.js projects to the Mercury 3.0 specifications.

+

Java "virtual thread" feature

+

Integrate with Java virtual thread feature when it becomes officially available in Java 19 around Q4, 2023.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + +
+ + + + + + + + + diff --git a/docs/arch-decisions/DESIGN-NOTES/index.html b/docs/arch-decisions/DESIGN-NOTES/index.html new file mode 100644 index 000000000..01b22e671 --- /dev/null +++ b/docs/arch-decisions/DESIGN-NOTES/index.html @@ -0,0 +1,282 @@ + + + + + + + + Design notes - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Design notes

+

Non-blocking design

+

The foundation library (platform-core) has been integrated with Kotlin coroutine and +suspend function features. We have also removed all blocking APIs from the platform-core library.

+

Applications using Mercury version 2 blocking RPC calls should be refactored to "suspend function". +This would reduce memory footprint and increase throughput. i.e. support more concurrent users and requests.

+

Since many functions in an application may be waiting for responses from a database or from a REST endpoint, +"suspend function" approach releases CPU resources during the "wait" state, thus contributing to +higher application throughput.

+

Low level control of function execution strategies

+

You can precisely control how your functions execute, using kernel thread pool, coroutine or suspend function +to address various use cases to yield the highest performance and throughput.

+

Kernel threads provide the highest performance in terms of operations per second when the number of threads is smaller. +As a rule of thumb, do not set "kernel.thread.pool" higher than 200.

+

Coroutine is ideal for functions that execute very quickly to yield control to other coroutines.

+

Suspend function should be used to support "sequential non-blocking" RPC or logic that requires artificial delay. +You can use the "awaitRequest" and "delay" APIs respectively.

+

Serialization

+

Gson

+

We are using Gson for its minimalist design.

+

We have customized the serialization behavior to be similar to Jackson and other serializers. +i.e. Integer and long values are kept without decimal points.

+

For backward compatibility with Jackson, we have added the writeValueAsString, writeValueAsBytes and readValue methods. +The convertValue method has been consolidated into the readValue method.

+

For simplicity, custom serialization annotations are discouraged.

+

MsgPack

+

For efficient and serialization performance, we use MsgPack as schemaless binary transport for EventEnvelope that +contains event metadata, headers and payload.

+

Custom JSON and XML serializers

+

For consistency, we have customized JAX-RS, Spring Boot and Servlet serialization and exception handlers.

+

Reactive design

+

Mercury uses the temporary local file system (/tmp) as an overflow area for events when the consumer is +slower than the producer. This event buffering design means that user application does not have to handle +back-pressure logic directly.

+

However, it does not restrict you from implementing your flow-control logic.

+

Vertx

+

In Mercury version 1, the Akka actor system is used as the in-memory event bus. +Since Mercury version 2, we have migrated from Akka to Eclipse Vertx.

+

In Mercury version 3, we extend the engine to be fully non-blocking with low-level control of application +performance and throughput.

+

Java versions

+

The platform-core library is backward compatible to Java 1.8 so that it can be used for IT modernization of +legacy Java projects.

+

However, the codebase has been tested with Java 1.8 to 19 for projects, and you can apply the platform-core library +in your projects without JVM constraints.

+

Spring Boot

+

The platform-core includes a non-blocking HTTP and websocket server for standalone operation without Spring Boot +or similar application server.

+

You may also use the platform-core with Spring Boot or other frameworks.

+

Customized Spring Boot

+

The rest-spring-2-example project demonstrates the use of the rest-spring-2 library to build a Spring Boot 2 +executable. The rest-spring-2 is a convenient library with customized Spring Boot serializers and exception handlers.

+

A corresponding library and example application for Spring Boot version 3 is rest-spring-3 and +rest-spring-3-example.

+

Regular Spring Boot

+

You can also use the platform-core library with a regular Spring Boot application if you prefer.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/css/fonts/Roboto-Slab-Bold.woff b/docs/css/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 000000000..6cb600001 Binary files /dev/null and b/docs/css/fonts/Roboto-Slab-Bold.woff differ diff --git a/docs/css/fonts/Roboto-Slab-Bold.woff2 b/docs/css/fonts/Roboto-Slab-Bold.woff2 new file mode 100644 index 000000000..7059e2314 Binary files /dev/null and b/docs/css/fonts/Roboto-Slab-Bold.woff2 differ diff --git a/docs/css/fonts/Roboto-Slab-Regular.woff b/docs/css/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 000000000..f815f63f9 Binary files /dev/null and b/docs/css/fonts/Roboto-Slab-Regular.woff differ diff --git a/docs/css/fonts/Roboto-Slab-Regular.woff2 b/docs/css/fonts/Roboto-Slab-Regular.woff2 new file mode 100644 index 000000000..f2c76e5bd Binary files /dev/null and b/docs/css/fonts/Roboto-Slab-Regular.woff2 differ diff --git a/docs/css/fonts/fontawesome-webfont.eot b/docs/css/fonts/fontawesome-webfont.eot new file mode 100644 index 000000000..e9f60ca95 Binary files /dev/null and b/docs/css/fonts/fontawesome-webfont.eot differ diff --git a/docs/css/fonts/fontawesome-webfont.svg b/docs/css/fonts/fontawesome-webfont.svg new file mode 100644 index 000000000..855c845e5 --- /dev/null +++ b/docs/css/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/css/fonts/fontawesome-webfont.ttf b/docs/css/fonts/fontawesome-webfont.ttf new file mode 100644 index 000000000..35acda2fa Binary files /dev/null and b/docs/css/fonts/fontawesome-webfont.ttf differ diff --git a/docs/css/fonts/fontawesome-webfont.woff b/docs/css/fonts/fontawesome-webfont.woff new file mode 100644 index 000000000..400014a4b Binary files /dev/null and b/docs/css/fonts/fontawesome-webfont.woff differ diff --git a/docs/css/fonts/fontawesome-webfont.woff2 b/docs/css/fonts/fontawesome-webfont.woff2 new file mode 100644 index 000000000..4d13fc604 Binary files /dev/null and b/docs/css/fonts/fontawesome-webfont.woff2 differ diff --git a/docs/css/fonts/lato-bold-italic.woff b/docs/css/fonts/lato-bold-italic.woff new file mode 100644 index 000000000..88ad05b9f Binary files /dev/null and b/docs/css/fonts/lato-bold-italic.woff differ diff --git a/docs/css/fonts/lato-bold-italic.woff2 b/docs/css/fonts/lato-bold-italic.woff2 new file mode 100644 index 000000000..c4e3d804b Binary files /dev/null and b/docs/css/fonts/lato-bold-italic.woff2 differ diff --git a/docs/css/fonts/lato-bold.woff b/docs/css/fonts/lato-bold.woff new file mode 100644 index 000000000..c6dff51f0 Binary files /dev/null and b/docs/css/fonts/lato-bold.woff differ diff --git a/docs/css/fonts/lato-bold.woff2 b/docs/css/fonts/lato-bold.woff2 new file mode 100644 index 000000000..bb195043c Binary files /dev/null and b/docs/css/fonts/lato-bold.woff2 differ diff --git a/docs/css/fonts/lato-normal-italic.woff b/docs/css/fonts/lato-normal-italic.woff new file mode 100644 index 000000000..76114bc03 Binary files /dev/null and b/docs/css/fonts/lato-normal-italic.woff differ diff --git a/docs/css/fonts/lato-normal-italic.woff2 b/docs/css/fonts/lato-normal-italic.woff2 new file mode 100644 index 000000000..3404f37e2 Binary files /dev/null and b/docs/css/fonts/lato-normal-italic.woff2 differ diff --git a/docs/css/fonts/lato-normal.woff b/docs/css/fonts/lato-normal.woff new file mode 100644 index 000000000..ae1307ff5 Binary files /dev/null and b/docs/css/fonts/lato-normal.woff differ diff --git a/docs/css/fonts/lato-normal.woff2 b/docs/css/fonts/lato-normal.woff2 new file mode 100644 index 000000000..3bf984332 Binary files /dev/null and b/docs/css/fonts/lato-normal.woff2 differ diff --git a/docs/css/theme.css b/docs/css/theme.css new file mode 100644 index 000000000..ad773009b --- /dev/null +++ b/docs/css/theme.css @@ -0,0 +1,13 @@ +/* + * This file is copied from the upstream ReadTheDocs Sphinx + * theme. To aid upgradability this file should *not* be edited. + * modifications we need should be included in theme_extra.css. + * + * https://github.com/readthedocs/sphinx_rtd_theme + */ + + /* sphinx_rtd_theme version 1.2.0 | MIT license */ +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} diff --git a/docs/css/theme_extra.css b/docs/css/theme_extra.css new file mode 100644 index 000000000..ab0631a18 --- /dev/null +++ b/docs/css/theme_extra.css @@ -0,0 +1,197 @@ +/* + * Wrap inline code samples otherwise they shoot of the side and + * can't be read at all. + * + * https://github.com/mkdocs/mkdocs/issues/313 + * https://github.com/mkdocs/mkdocs/issues/233 + * https://github.com/mkdocs/mkdocs/issues/834 + */ +.rst-content code { + white-space: pre-wrap; + word-wrap: break-word; + padding: 2px 5px; +} + +/** + * Make code blocks display as blocks and give them the appropriate + * font size and padding. + * + * https://github.com/mkdocs/mkdocs/issues/855 + * https://github.com/mkdocs/mkdocs/issues/834 + * https://github.com/mkdocs/mkdocs/issues/233 + */ +.rst-content pre code { + white-space: pre; + word-wrap: normal; + display: block; + padding: 12px; + font-size: 12px; +} + +/** + * Fix code colors + * + * https://github.com/mkdocs/mkdocs/issues/2027 + */ +.rst-content code { + color: #E74C3C; +} + +.rst-content pre code { + color: #000; + background: #f8f8f8; +} + +/* + * Fix link colors when the link text is inline code. + * + * https://github.com/mkdocs/mkdocs/issues/718 + */ +a code { + color: #2980B9; +} +a:hover code { + color: #3091d1; +} +a:visited code { + color: #9B59B6; +} + +/* + * The CSS classes from highlight.js seem to clash with the + * ReadTheDocs theme causing some code to be incorrectly made + * bold and italic. + * + * https://github.com/mkdocs/mkdocs/issues/411 + */ +pre .cs, pre .c { + font-weight: inherit; + font-style: inherit; +} + +/* + * Fix some issues with the theme and non-highlighted code + * samples. Without and highlighting styles attached the + * formatting is broken. + * + * https://github.com/mkdocs/mkdocs/issues/319 + */ +.rst-content .no-highlight { + display: block; + padding: 0.5em; + color: #333; +} + + +/* + * Additions specific to the search functionality provided by MkDocs + */ + +.search-results { + margin-top: 23px; +} + +.search-results article { + border-top: 1px solid #E1E4E5; + padding-top: 24px; +} + +.search-results article:first-child { + border-top: none; +} + +form .search-query { + width: 100%; + border-radius: 50px; + padding: 6px 12px; + border-color: #D1D4D5; +} + +/* + * Improve inline code blocks within admonitions. + * + * https://github.com/mkdocs/mkdocs/issues/656 + */ + .rst-content .admonition code { + color: #404040; + border: 1px solid #c7c9cb; + border: 1px solid rgba(0, 0, 0, 0.2); + background: #f8fbfd; + background: rgba(255, 255, 255, 0.7); +} + +/* + * Account for wide tables which go off the side. + * Override borders to avoid weirdness on narrow tables. + * + * https://github.com/mkdocs/mkdocs/issues/834 + * https://github.com/mkdocs/mkdocs/pull/1034 + */ +.rst-content .section .docutils { + width: 100%; + overflow: auto; + display: block; + border: none; +} + +td, th { + border: 1px solid #e1e4e5 !important; + border-collapse: collapse; +} + +/* + * Without the following amendments, the navigation in the theme will be + * slightly cut off. This is due to the fact that the .wy-nav-side has a + * padding-bottom of 2em, which must not necessarily align with the font-size of + * 90 % on the .rst-current-version container, combined with the padding of 12px + * above and below. These amendments fix this in two steps: First, make sure the + * .rst-current-version container has a fixed height of 40px, achieved using + * line-height, and then applying a padding-bottom of 40px to this container. In + * a second step, the items within that container are re-aligned using flexbox. + * + * https://github.com/mkdocs/mkdocs/issues/2012 + */ + .wy-nav-side { + padding-bottom: 40px; +} + +/* For section-index only */ +.wy-menu-vertical .current-section p { + background-color: #e3e3e3; + color: #404040; +} + +/* + * The second step of above amendment: Here we make sure the items are aligned + * correctly within the .rst-current-version container. Using flexbox, we + * achieve it in such a way that it will look like the following: + * + * [No repo_name] + * Next >> // On the first page + * << Previous Next >> // On all subsequent pages + * + * [With repo_name] + * Next >> // On the first page + * << Previous Next >> // On all subsequent pages + * + * https://github.com/mkdocs/mkdocs/issues/2012 + */ +.rst-versions .rst-current-version { + padding: 0 12px; + display: flex; + font-size: initial; + justify-content: space-between; + align-items: center; + line-height: 40px; +} + +/* + * Please note that this amendment also involves removing certain inline-styles + * from the file ./mkdocs/themes/readthedocs/versions.html. + * + * https://github.com/mkdocs/mkdocs/issues/2012 + */ +.rst-current-version span { + flex: 1; + text-align: center; +} diff --git a/docs/diagrams/figure-1.png b/docs/diagrams/figure-1.png new file mode 100644 index 000000000..e1255ed4b Binary files /dev/null and b/docs/diagrams/figure-1.png differ diff --git a/docs/diagrams/figure-2.png b/docs/diagrams/figure-2.png new file mode 100644 index 000000000..8ed98a0c8 Binary files /dev/null and b/docs/diagrams/figure-2.png differ diff --git a/docs/diagrams/figure-3.png b/docs/diagrams/figure-3.png new file mode 100644 index 000000000..49a9c1a7f Binary files /dev/null and b/docs/diagrams/figure-3.png differ diff --git a/docs/diagrams/figure-4.png b/docs/diagrams/figure-4.png new file mode 100644 index 000000000..98f6922df Binary files /dev/null and b/docs/diagrams/figure-4.png differ diff --git a/docs/diagrams/figure-5.png b/docs/diagrams/figure-5.png new file mode 100644 index 000000000..e40787812 Binary files /dev/null and b/docs/diagrams/figure-5.png differ diff --git a/docs/diagrams/figure-6.png b/docs/diagrams/figure-6.png new file mode 100644 index 000000000..58bc0b97b Binary files /dev/null and b/docs/diagrams/figure-6.png differ diff --git a/docs/diagrams/figure-7.png b/docs/diagrams/figure-7.png new file mode 100644 index 000000000..82def6cf7 Binary files /dev/null and b/docs/diagrams/figure-7.png differ diff --git a/docs/diagrams/figure-8.png b/docs/diagrams/figure-8.png new file mode 100644 index 000000000..3eac52a52 Binary files /dev/null and b/docs/diagrams/figure-8.png differ diff --git a/docs/guides/APPENDIX-I/index.html b/docs/guides/APPENDIX-I/index.html new file mode 100644 index 000000000..bd9f6e441 --- /dev/null +++ b/docs/guides/APPENDIX-I/index.html @@ -0,0 +1,600 @@ + + + + + + + + Appendix-I - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Application Configuration

+

The following parameters are used by the system. You can define them in either the application.properties or +application.yml file.

+

When you use both application.properties and application.yml, the parameters in application.properties will take +precedence.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyValue (example)Required
application.nameApplication nameYes
spring.application.nameAlias for application nameYes*1
info.app.versionmajor.minor.build (e.g. 1.0.0)Yes
info.app.descriptionSomething about your applicationYes
web.component.scanyour own package path or parent pathYes
server.porte.g. 8083Yes*1
rest.server.porte.g. 8085Optional
websocket.server.portAlias for rest.server.portOptional
rest.automationtrue if you want to enable automationOptional
yaml.rest.automationConfig location. e.g. classpath:/rest.yamlOptional
yaml.event.over.httpConfig location classpath:/event-over-http.yamlOptional
yaml.multicastConfig location classpath:/multicast.yamlOptional
yaml.journalConfig location classpath:/journal.yamlOptional
yaml.route.substitutionConfig locationOptional
yaml.topic.substitutionConfig locationOptional
yaml.cronConfig locationOptional
yaml.flow.automationConfig location. e.g. classpath:/flows.yamlEventScript
static.html.folderclasspath:/public/Yes
spring.web.resources.static-locations(alias for static.html.folder)Yes*1
mime.typesMap of file extensions to MIME types
(application.yml only)
Optional
spring.mvc.static-path-pattern/**Yes*1
jax.rs.application.path/apiOptional*
show.env.variablescomma separated list of variable namesOptional
show.application.propertiescomma separated list of property namesOptional
cloud.connectorkafka, none, etc.Optional
cloud.servicese.g. some.interesting.serviceOptional
snake.case.serializationtrue (recommended)Optional
safe.data.modelspackages pointing to your PoJo classesOptional
protect.info.endpointstrue to disable actuators. Default: trueOptional
trace.http.headercomma separated list. Default "X-Trace-Id"Optional
index.redirectioncomma separated list of URI pathsOptional*
index.pagedefault is index.htmlOptional*
hsts.featuredefault is trueOptional*
application.feature.route.substitutiondefault is falseOptional
application.feature.topic.substitutiondefault is falseOptional
kafka.replication.factor3Kafka
cloud.client.propertiese.g. classpath:/kafka.propertiesConnector
user.cloud.client.propertiese.g. classpath:/second-kafka.propertiesConnector
default.app.group.idgroupId for the app instance.
Default: appGroup
Connector
default.monitor.group.idgroupId for the presence-monitor.
Default: monitorGroup
Connector
monitor.topictopic for the presence-monitor.
Default: service.monitor
Connector
app.topic.prefixDefault: multiplex (DO NOT change)Connector
app.partitions.per.topicMax Kafka partitions per topic.
Default: 32
Connector
max.virtual.topicsMax virtual topics = partitions * topics.
Default: 288
Connector
max.closed.user.groupsNumber of closed user groups.
Default: 10, range: 3 - 30
Connector
closed.user.groupClosed user group. Default: 1Connector
transient.data.storeDefault is "/tmp/reactive"Optional
running.in.cloudDefault is false (set to true if containerized)Optional
deferred.commit.logDefault is false (for unit tests only)Optional
kernel.thread.poolDefault 100. Not more than 200.Optional
+

* - when using the "rest-spring" library

+

Base configuration files

+

By default, the system assumes the following application configuration files:

+
    +
  1. application.properties
  2. +
  3. application.yml
  4. +
+

You can change this behavior by adding the app-config-reader.yml in your project's resources folder.

+
resources:
+  - application.properties
+  - application.yml
+
+

You can tell the system to load application configuration from different set of files. +You can use either PROPERTIES or YAML files. YAML files can use "yml" or "yaml" extension.

+

For example, you may use only "application.yml" file without scanning application.properties.

+

Special handling for PROPERTIES file

+

Since application.properties and application.yml can be used together, +the system must enforce keyspace uniqueness because YAML keyspaces are hierarchical.

+

For example, if you have x.y and x.y.z, x.y is the parent of x.y.z.

+

Therefore, you cannot set a value for the parent key since the parent is a key-value container.

+

This hierarchical rule is enforced for PROPERTIES files. +If you have x.y=3 and x.y.z=2 in the same PROPERTIES file, x.y will become a parent of x.y.z and its intended +value of 3 will be lost.

+

Optional Service

+

The OptionalService annotation may be used with the following class annotations:

+
    +
  1. BeforeApplication
  2. +
  3. MainApplication
  4. +
  5. PreLoad
  6. +
  7. WebSocketService
  8. +
+

When the OptionalService annotation is available, the system will evaluate the annotation value as a +conditional statement where it supports one or more simple condition using a key-value in the application +configuration.

+

For examples:

+

OptionalService("rest.automation") - the class will be loaded when rest.automation=true

+

OptionalService("!rest.automation") - the class will be loaded when rest.automation is false or non-exist

+

OptionalService("interesting.key=100") - the system will load the class when "interesting.key" is set to 100 +in application configuration.

+

To specify more than one condition, use a comma separated list as the value like this: +OptionalService("web.socket.enabled, rest.automation") - this tells the system to load the class when +either web.socket.enabled or rest.automation is true.

+

Static HTML contents

+

You can place static HTML files (e.g. the HTML bundle for a UI program) in the "resources/public" folder or +in the local file system using the "static.html.folder" parameter.

+

The system supports a bare minimal list of file extensions to MIME types. If your use case requires additional +MIME type mapping, you may define them in the application.yml configuration file under the mime.types +section like this:

+
mime.types:
+  pdf: 'application/pdf'
+  doc: 'application/msword'
+
+

Note that application.properties file cannot be used for the "mime.types" section because it only supports text +key-values.

+

HTTP and websocket port assignment

+

If rest.automation=true and rest.server.port or server.port are configured, the system will start +a lightweight non-blocking HTTP server. If rest.server.port is not available, it will fall back to server.port.

+

If rest.automation=false and you have a websocket server endpoint annotated as WebsocketService, the system +will start a non-blocking Websocket server with a minimalist HTTP server that provides actuator services. +If websocket.server.port is not available, it will fall back to rest.server.port or server.port.

+

If you add Spring Boot dependency, Spring Boot will use server.port to start Tomcat or similar HTTP server.

+

The built-in lightweight non-blocking HTTP server and Spring Boot can co-exist when you configure +rest.server.port and server.port to use different ports.

+

Note that the websocket.server.port parameter is an alias of rest.server.port.

+

Transient data store

+

The system handles back-pressure automatically by overflowing events from memory to a transient data store. +As a cloud native best practice, the folder must be under "/tmp". The default is "/tmp/reactive". +The "running.in.cloud" parameter must be set to false when your apps are running in IDE or in your laptop. +When running in kubernetes, it can be set to true.

+

The safe.data.models parameter

+

PoJo may contain Java code. As a result, it is possible to inject malicious code that does harm when +deserializing a PoJo. This security risk applies to any JSON serialization engine.

+

For added security and peace of mind, you may want to protect your PoJo package paths. +When the safe.data.models parameter is configured, the underlying serializers for JAX-RS, Spring RestController +and Servlets will respect this setting and enforce PoJo filtering.

+

If there is a genuine need to programmatically perform serialization, you may use the pre-configured serializer +so that the serialization behavior is consistent.

+

You can get an instance of the serializer with SimpleMapper.getInstance().getMapper().

+

The serializer may perform snake case or camel serialization depending on the parameter snake.case.serialization.

+

If you want to ensure snake case or camel, you can select the serializer like this:

+
SimpleObjectMapper snakeCaseMapper = SimpleMapper.getInstance().getSnakeCaseMapper();
+SimpleObjectMapper camelCaseMapper = SimpleMapper.getInstance().getCamelCaseMapper();
+
+

The trace.http.header parameter

+

The trace.http.header parameter sets the HTTP header for trace ID. When configured with more than one label, +the system will retrieve trace ID from the corresponding HTTP header and propagate it through the transaction +that may be served by multiple services.

+

If trace ID is presented in an HTTP request, the system will use the same label to set HTTP response traceId header.

+
X-Trace-Id: a9a4e1ec-1663-4c52-b4c3-7b34b3e33697
+or
+X-Correlation-Id: a9a4e1ec-1663-4c52-b4c3-7b34b3e33697
+
+

Kafka specific configuration

+

If you use the kafka-connector (cloud connector) and kafka-presence (presence monitor), you may want to +externalize kafka.properties like this:

+
cloud.client.properties=file:/tmp/config/kafka.properties
+
+

Note that "classpath" refers to embedded config file in the "resources" folder in your source code and "file" +refers to an external config file.

+

You want also use the embedded config file as a backup like this:

+
cloud.client.properties=file:/tmp/config/kafka.properties,classpath:/kafka.properties
+
+

Distributed trace

+

To enable distributed trace logging, please set this in log4j2.xml:

+
<logger name="org.platformlambda.core.services.DistributedTrace" level="INFO" />
+
+

Built-in XML serializer

+

The platform-core includes built-in serializers for JSON and XML in the AsyncHttpClient, JAX-RS and +Spring RestController. The XML serializer is designed for simple use cases. If you need to handle more +complex XML data structure, you can disable the XML serializer by adding the following HTTP request +header.

+
X-Raw-Xml=true
+
+


+ + + + + + + + + + + + + + + +
Chapter-10HomeAppendix-II
Migration GuideTable of ContentsReserved Route Names
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/APPENDIX-II/index.html b/docs/guides/APPENDIX-II/index.html new file mode 100644 index 000000000..6586d26ea --- /dev/null +++ b/docs/guides/APPENDIX-II/index.html @@ -0,0 +1,400 @@ + + + + + + + + Appendix-II - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Reserved Route Names

+

The Mercury foundation code is written using the same core API and each function has a route name.

+

The following route names are reserved. Please DO NOT use them in your application functions to avoid breaking +the system unintentionally.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RoutePurposeModules
actuator.servicesActuator endpoint servicesplatform-core
elastic.queue.cleanupElastic event buffer clean up taskplatform-core
distributed.tracingDistributed tracing loggerplatform-core
system.ws.server.cleanupWebsocket server cleanup serviceplatform-core
http.auth.handlerREST automation authentication routerplatform-core
event.api.serviceEvent API serviceplatform-core
stream.to.bytesEvent API helper functionplatform-core
system.service.registryDistributed routing registryConnector
system.service.queryDistributed routing queryConnector
cloud.connector.healthCloud connector health serviceConnector
cloud.managerCloud manager serviceConnector
presence.servicePresence signal serviceConnector
presence.housekeeperPresence keep-alive serviceConnector
cloud.connectorCloud event emitterConnector
init.multiplex.*reserved for event stream startupConnector
completion.multiplex.*reserved for event stream clean upConnector
async.http.requestHTTP request event handlerREST automation
async.http.responseHTTP response event handlerREST automation
cron.schedulerCron job schedulerSimple Scheduler
init.service.monitor.*reserved for event stream startupService monitor
completion.service.monitor.*reserved for event stream clean upService monitor
+

Optional user defined functions

+

The following optional route names will be detected by the system for additional user defined features.

+ + + + + + + + + + + + + + + + + + + + + +
RoutePurpose
additional.infoUser application function to return information
about your application status
distributed.trace.forwarderCustom function to forward performance metrics
to a telemetry system
transaction.journal.recorderCustom function to record transaction request-response
payloads into an audit DB
+

The additional.info function, if implemented, will be invoked from the "/info" endpoint and its response +will be merged into the "/info" response.

+

For distributed.trace.forwarder and transaction.journal.recorder, please refer to Chapter-5 +for details.

+

Reserved event header names

+

The following event headers are injected by the system as READ only metadata. They are available from the +input "headers". However, they are not part of the EventEnvelope.

+ + + + + + + + + + + + + + + + + + + + + +
HeaderPurpose
my_routeroute name of your function
my_trace_idtrace ID, if any, for the incoming event
my_trace_pathtrace path, if any, for the incoming event
+

You can create a trackable PostOffice using the "headers" and the "instance" parameters in the input arguments +of your function. The FastRPC instance requires only the "headers" parameters.

+
// Java
+PostOffice po = new PostOffice(headers, instance);
+
+// Kotlin
+val fastRPC = FastRPC(headers);
+
+


+ + + + + + + + + + + + + + + +
Appendix-IHomeAppendix-III
Application ConfigurationTable of ContentsActuator and HTTP client
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/APPENDIX-III/index.html b/docs/guides/APPENDIX-III/index.html new file mode 100644 index 000000000..5fa009f8a --- /dev/null +++ b/docs/guides/APPENDIX-III/index.html @@ -0,0 +1,422 @@ + + + + + + + + Appendix-III - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Actuators and HTTP client

+

Actuator endpoints

+

The following admin endpoints are available.

+
GET /info
+GET /info/routes
+GET /info/lib
+GET /env
+GET /health
+GET /livenessprobe
+POST /shutdown
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EndpointPurpose
/infoDescribe the application
/info/routesShow public routing table
/info/libList libraries packed with this executable
/envList all private and public function route names and selected environment variables
/healthApplication health check endpoint
/livenessprobeCheck if application is running normally
/shutdownOperator may use this endpoint to do a POST command to stop the application
+

For the shutdown endpoint, you must provide an X-App-Instance HTTP header where the value is the "origin ID" +of the application. You can get the value from the "/info" endpoint.

+

Custom health services

+

You can extend the "/health" endpoint by implementing and registering lambda functions to be added to the +"health check" dependencies.

+
mandatory.health.dependencies=cloud.connector.health, demo.health
+optional.health.dependencies=other.service.health
+
+

Your custom health service must respond to the following requests:

+
    +
  1. Info request (type=info) - it should return a map that includes service name and href (protocol, hostname and port)
  2. +
  3. Health check (type=health) - it should return a text string of the health check. e.g. read/write test result. + It can throw AppException with status code and error message if health check fails.
  4. +
+

A sample health service is available in the DemoHealth class of the lambda-example project as follows:

+
@PreLoad(route="demo.health", instances=5)
+public class DemoHealth implements LambdaFunction {
+
+    private static final String TYPE = "type";
+    private static final String INFO = "info";
+    private static final String HEALTH = "health";
+
+    @Override
+    public Object handleEvent(Map<String, String> headers, Object input, int instance) {
+        /*
+         * The interface contract for a health check service includes both INFO and HEALTH responses
+         */
+        if (INFO.equals(headers.get(TYPE))) {
+            Map<String, Object> result = new HashMap<>();
+            result.put("service", "demo.service");
+            result.put("href", "http://127.0.0.1");
+            return result;
+        }
+        if (HEALTH.equals(headers.get(TYPE))) {
+            /*
+             * This is a place-holder for checking a downstream service.
+             *
+             * You may implement your own logic to test if a downstream service is running fine.
+             * If running, just return a health status message.
+             * Otherwise,
+             *      throw new AppException(status, message)
+             */
+            return "demo.service is running fine";
+        }
+        throw new IllegalArgumentException("type must be info or health");
+    }
+}
+
+
+

AsyncHttpClient service

+

The "async.http.request" function can be used as a non-blocking HTTP client.

+

To make an HTTP request to an external REST endpoint, you can create an HTTP request object using the +AsyncHttpRequest class and make an async RPC call to the "async.http.request" function like this:

+
PostOffice po = new PostOffice(headers, instance);
+AsyncHttpRequest req = new AsyncHttpRequest();
+req.setMethod("GET");
+req.setHeader("accept", "application/json");
+req.setUrl("/api/hello/world?hello world=abc");
+req.setQueryParameter("x1", "y");
+List<String> list = new ArrayList<>();
+list.add("a");
+list.add("b");
+req.setQueryParameter("x2", list);
+req.setTargetHost("http://127.0.0.1:8083");
+EventEnvelope request = new EventEnvelope().setTo("async.http.request").setBody(req);
+Future<EventEnvelope> res = po.asyncRequest(request, 5000);
+res.onSuccess(response -> {
+   // do something with the result 
+});
+
+

In a suspend function using KotlinLambdaFunction, the same logic may look like this:

+
val fastRPC = FastRPC(headers)
+val req = AsyncHttpRequest()
+req.setMethod("GET")
+req.setHeader("accept", "application/json")
+req.setUrl("/api/hello/world?hello world=abc")
+req.setQueryParameter("x1", "y")
+val list: MutableList<String> = ArrayList()
+list.add("a")
+list.add("b")
+req.setQueryParameter("x2", list)
+req.setTargetHost("http://127.0.0.1:8083")
+val request = EventEnvelope().setTo("async.http.request").setBody(req)
+val response = fastRPC.awaitRequest(request, 5000)
+// do something with the result
+
+

Send HTTP request body for HTTP PUT, POST and PATCH methods

+

For most cases, you can just set a HashMap into the request body and specify content-type as JSON or XML. +The system will perform serialization properly.

+

Example code may look like this:

+
AsyncHttpRequest req = new AsyncHttpRequest();
+req.setMethod("POST");
+req.setHeader("accept", "application/json");
+req.setHeader("content-type", "application/json");
+req.setUrl("/api/book");
+req.setTargetHost("https://service_provider_host");
+req.setBody(mapOfKeyValues);
+// where keyValues is a HashMap
+
+

Send HTTP request body as a stream

+

For larger payload, you may use the streaming method. See sample code below:

+
int len;
+byte[] buffer = new byte[4096];
+FileInputStream in = new FileInputStream(myFile);
+ObjectStreamIO stream = new ObjectStreamIO(timeoutInSeconds);
+ObjectStreamWriter out = stream.getOutputStream();
+while ((len = in.read(buffer, 0, buffer.length)) != -1) {
+    out.write(buffer, 0, len);
+}
+// closing the output stream would send a EOF signal to the stream
+out.close();
+// tell the HTTP client to read the input stream
+req.setStreamRoute(stream.getInputStreamId());
+
+

Read HTTP response body stream

+

If content length is not given, the response body will be received as a stream.

+

Your application should check if the HTTP response header "stream" exists. Its value is an input "stream ID".

+

For simplicity and readability, we recommend using "suspend function" to read the input byte-array stream.

+

It may look like this:

+
val po = PostOffice(headers, instance)
+val fastRPC = FastRPC(headers)
+
+val req = EventEnvelope().setTo(streamId).setHeader("type", "read")
+while (true) {
+    val event = fastRPC.awaitRequest(req, 5000)
+    if (event.status == 408) {
+        // handle input stream timeout
+        break
+    }
+    if ("eof" == event.headers["type"]) {
+        po.send(streamId, Kv("type", "close"))
+        break
+    }
+    if ("data" == event.headers["type"]) {
+        val block = event.body
+        if (block is ByteArray) {
+            // handle the data block from the input stream
+        }
+    }
+}
+
+

Content length for HTTP request

+

IMPORTANT: Do not set the "content-length" HTTP header because the system will automatically compute the +correct content-length for small payload. For large payload, it will use the chunking method. +

+ + + + + + + + + + + + + +
Appendix-IIHome
Reserved Route NamesTable of Contents
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/CHAPTER-1/index.html b/docs/guides/CHAPTER-1/index.html new file mode 100644 index 000000000..7fed28270 --- /dev/null +++ b/docs/guides/CHAPTER-1/index.html @@ -0,0 +1,546 @@ + + + + + + + + Chapter-1 - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Introduction

+

Mercury version 3 is a toolkit for event-driven programming that is the foundation for composable application.

+

At the platform level, composable architecture refers to loosely coupled platform services, utilities, and +business applications. With modular design, you can assemble platform components and applications to create +new use cases or to adjust for ever-changing business environment and requirements. Domain driven design (DDD), +Command Query Responsibility Segregation (CQRS) and Microservices patterns are the popular tools that architects +use to build composable architecture. You may deploy application in container, serverless or other means.

+

At the application level, a composable application means that an application is assembled from modular software +components or functions that are self-contained and pluggable. You can mix-n-match functions to form new applications. +You can retire outdated functions without adverse side effect to a production system. Multiple versions of a function +can exist, and you can decide how to route user requests to different versions of a function. Applications would be +easier to design, develop, maintain, deploy, and scale.

+

While you can write a composable application using event-driven programming, the best way to build a composable +application is a declarative approach where event choreography of self-contained functions is performed by +an event manager. Declarative approach for building composable applications is shown in:

+

Mercury v4: https://github.com/Accenture/mercury-composable

+

Documentation: https://accenture.github.io/mercury-composable/

+

Composable application architecture

+
+

Figure 1 - Composable application architecture

+
+

architecture.png

+

As shown in Figure 1, a minimalist composable application consists of three user defined components:

+
    +
  1. Main modules that provides an entry point to your application
  2. +
  3. One or more business logic modules (shown as "function-1" to "function-3" in the diagram)
  4. +
  5. An event orchestration module to command the business logic modules to work together as an application
  6. +
+

and a composable event engine that provides:

+
    +
  1. REST automation
  2. +
  3. An in-memory event system (aka "event loop")
  4. +
  5. Local pub/sub system
  6. +
+

Main module

+

Each application has an entry point. You may implement an entry point in a main application like this:

+
@MainApplication
+public class MainApp implements EntryPoint {
+   public static void main(String[] args) {
+      AutoStart.main(args);
+   }
+   @Override
+   public void start(String[] args) {
+        // your startup logic here
+      log.info("Started");
+   }
+}
+
+

For a command line use case, your main application ("MainApp") module would get command line arguments and +send the request as an event to a business logic function for processing.

+

For a backend application, the MainApp is usually used to do some "initialization" or setup steps for your +services.

+

Business logic modules

+

Your user function module may look like this:

+
@PreLoad(route = "hello.simple", instances = 10)
+public class SimpleDemoEndpoint implements TypedLambdaFunction<AsyncHttpRequest, Object> {
+    @Override
+    public Object handleEvent(Map<String, String> headers, AsyncHttpRequest input, int instance) {
+        // business logic here
+        return result;
+    }
+}
+
+

Each function in a composable application should be implemented in the first principle of "input-process-output". +It should be stateless and self-contained. i.e. it has no direct dependencies with any other functions in the +composable application. Each function is addressable by a unique "route name" and you can use PoJo for input and output.

+

In the above example, the function is called "hello.simple". The input is an AsyncHttpRequest object, meaning that +this function is a "Backend for Frontend (BFF)" module that is invoked by a REST endpoint.

+

When a function finishes processing, its output will be delivered to the next function.

+
+

Writing code in the first principle of "input-process-output" promotes Test Driven Development (TDD) because + interface contact is clearly defined. Self-containment means code is more readable too.

+
+

Event orchestration

+

A transaction can pass through one or more user functions. In this case, you can write a user function to receive +request from a user, make requests to some user functions, and consolidate the responses before responding to the +user.

+

Note that event orchestration is optional. In the most basic REST application, the REST automation system can send +the user request to a function directly. When the function finishes processing, its output will be routed as +a HTTP response to the user.

+

The in-memory event system

+

Event routing is done behind the curtain by the composable engine which consists of the REST automation service, +an in-memory event system ("event loop") and an optional localized pub/sub system.

+

REST automation

+

REST automation creates REST endpoints by configuration rather than code. You can define a REST endpoint like this:

+
  - service: "hello.world"
+    methods: ['GET']
+    url: "/api/hello/world"
+    timeout: 10s
+
+

In this example, when a HTTP request is received at the URL path "/api/hello/world", the REST automation system +will convert the HTTP request into an event for onward delivery to the user defined function "hello.world". +Your function will receive the HTTP request as input and return a result set that will be sent as a HTTP response +to the user.

+

For more sophisticated business logic, you can write a function to receive the HTTP request and do +"event orchestration". i.e. you can do data transformation and send "events" to other user functions to +process the request.

+

In-memory event system

+

The composable engine encapsulates the Eclipse vertx event bus library for event routing. It exposes the +"PostOffice" API for your orchestration function to send async or RPC events.

+

Local pub/sub system

+

The in-memory event system is designed for point-to-point delivery. In some use cases, you may like to have +a broadcast channel so that more than one function can receive the same event. For example, sending notification +events to multiple functions. The optional local pub/sub system provides this multicast capability.

+

Other user facing channels

+

While REST is the most popular user facing interface, there are other communication means such as event triggers +in a serverless environment. You can write a function to listen to these external event triggers and send the events +to your user defined functions. This custom "adapter" pattern is illustrated as the dotted line path in Figure 1.

+

Build the platform libraries

+

The first step is to build Mercury libraries from source. +To simplify the process, you may publish the libraries to your enterprise artifactory.

+
mkdir sandbox
+cd sandox
+git clone https://github.com/Accenture/mercury.git
+cd mercury
+mvn clean install
+
+

The above sample script clones the Mercury open sources project and builds the libraries from source.

+

The pre-requisite is maven 3.8.6 and openjdk 1.8 or higher. We have tested mercury with Java version 1.8 to 19.

+

This will build the mercury libraries and the sample applications.

+

The platform-core project is the foundation library for writing composable application.

+

Run the lambda-example application

+

Assuming you follow the suggested project directory above, you can run a sample composable application +called "lambda-example" like this:

+
cd sandbox/mercury/examples/lambda-example
+java -jar target/lambda-example-3.0.9.jar
+
+

You will find the following console output when the app starts

+
Exact API paths [/api/event, /api/hello/download, /api/hello/upload, /api/hello/world]
+Wildcard API paths [/api/hello/download/{filename}, /api/hello/generic/{id}]
+
+

Application parameters are defined in the resources/application.properties file (or application.yml if you prefer). +When rest.automation=true is defined, the system will parse the "rest.yaml" configuration for REST endpoints.

+

Light-weight non-blocking HTTP server

+

When REST automation is turned on, the system will start a lightweight non-blocking HTTP server. +By default, it will search for the "rest.yaml" file from "/tmp/config/rest.yaml" and then from "classpath:/rest.yaml". +Classpath refers to configuration files under the "resources" folder in your source code project.

+

To instruct the system to load from a specific path. You can add the yaml.rest.automation parameter.

+

To select another server port, change the rest.server.port parameter.

+
rest.server.port=8085
+rest.automation=true
+yaml.rest.automation=classpath:/rest.yaml
+
+

To create a REST endpoint, you can add an entry in the "rest" section of the "rest.yaml" config file like this:

+
  - service: "hello.download"
+    methods: [ 'GET' ]
+    url: "/api/hello/download"
+    timeout: 20s
+    cors: cors_1
+    headers: header_1
+    tracing: true
+
+

The above example creates the "/api/hello/download" endpoint to route requests to the "hello.download" function. +We will elaborate more about REST automation in Chapter-3.

+

Function is an event handler

+

A function is executed when an event arrives. You can define a "route name" for each function. +It is created by a class implementing one of the following interfaces:

+
    +
  1. TypedLambdaFunction allows you to use PoJo or HashMap as input and output
  2. +
  3. LambdaFunction is untyped, but it will transport PoJo from the caller to the input of your function
  4. +
  5. KotlinLambdaFunction is a typed lambda function using Kotlin suspend function
  6. +
+

Execute the "hello.world" function

+

With the application started in a command terminal, please use a browser to point to: +http://127.0.0.1:8085/api/hello/world

+

It will echo the HTTP headers from the browser like this:

+
{
+  "headers": {},
+  "instance": 1,
+  "origin": "20230324b709495174a649f1b36d401f43167ba9",
+  "body": {
+    "headers": {
+      "sec-fetch-mode": "navigate",
+      "sec-fetch-site": "none",
+      "sec-ch-ua-mobile": "?0",
+      "accept-language": "en-US,en;q=0.9",
+      "sec-ch-ua-platform": "\"Windows\"",
+      "upgrade-insecure-requests": "1",
+      "sec-fetch-user": "?1",
+      "accept": "text/html,application/xhtml+xml,application/xml,*/*",
+      "sec-fetch-dest": "document",
+      "user-agent": "Mozilla/5.0 Chrome/111.0.0.0"
+    },
+    "method": "GET",
+    "ip": "127.0.0.1",
+    "https": false,
+    "url": "/api/hello/world",
+    "timeout": 10
+  }
+}
+
+

Where is the "hello.world" function?

+

The function is defined in the MainApp class in the source project with the following segment of code:

+
LambdaFunction echo = (headers, input, instance) -> {
+    log.info("echo #{} got a request", instance);
+    Map<String, Object> result = new HashMap<>();
+    result.put("headers", headers);
+    result.put("body", input);
+    result.put("instance", instance);
+    result.put("origin", platform.getOrigin());
+    return result;
+};
+// Register the above inline lambda function
+platform.register("hello.world", echo, 10);
+
+

The Hello World function is written as an "inline lambda function". It is registered programmatically using +the platform.register API.

+

The rest of the functions are written using regular classes implementing the LambdaFunction, TypedLambdaFunction +and KotlinLambdaFunction interfaces.

+

TypedLambdaFunction

+

Let's examine the SimpleDemoEndpoint example under the "services" folder. It may look like this:

+
@PreLoad(route = "hello.simple", instances = 10)
+public class SimpleDemoEndpoint implements TypedLambdaFunction<AsyncHttpRequest, Object> {
+    @Override
+    public Object handleEvent(Map<String, String> headers, AsyncHttpRequest input, int instance) {
+        // business logic here
+    }
+}
+
+

The PreLoad annotation assigns a route name to the Java class and registers it with an in-memory event system. +The instances parameter tells the system to create a number of workers to serve concurrent requests.

+
+

Note that you don't need a lot of workers to handle a larger number of users + and requests provided that your function can finish execution very quickly.

+
+

By default, functions are executed as "coroutine" unless you specify the KernelThreadRunner annotation to tell +the system to run the function using kernel thread pool.

+

There are three function execution strategies (Kernel thread pool, coroutine and suspend function). +We will explain the concept in Chapter-2

+

In a composable application, a function is designed using the first principle of "input-process-output".

+

In the "hello.simple" function, the input is an HTTP request expressed as a class of AsyncHttpRequest. +You can ignore headers input argument for the moment. We will cover it later.

+

The output is declared as "Object" so that the function can return any data structure using a HashMap or PoJo.

+

You may want to review the REST endpoint /api/simple/{task}/* in the rest.yaml config file to see how it is +connected to the "hello.simple" function.

+

We take a minimalist approach for the rest.yaml syntax. The parser will detect any syntax errors. Please check +application log to ensure all REST endpoint entries in rest.yaml file are valid.

+

Write your first function

+

Using the lambda-example as a template, let's create your first function by adding a function in the +"services" package folder. You will give it the route name "my.first.function" in the "PreLoad" annotation.

+
+

Note that route name must use lower case letters and numbers separated by the period character.

+
+

+@PreLoad(route = "my.first.function", instances = 10)
+public class MyFirstFunction implements TypedLambdaFunction<AsyncHttpRequest, Object> {
+
+    @Override
+    public Object handleEvent(Map<String, String> headers, AsyncHttpRequest input, int instance) {
+        // your business logic here
+        return input;
+    }
+}
+
+

To connect this function with a REST endpoint, you can declare a new REST endpoint in the rest.yaml like this:

+
  - service: "my.first.function"
+    methods: [ 'GET' ]
+    url: "/api/hello/my/function"
+    timeout: 20s
+    cors: cors_1
+    headers: header_1
+    tracing: true
+
+

If you do not put any business logic, the above function will echo the incoming HTTP request object back to the +browser.

+

Now you can examine the input HTTP request object and perform some data transformation before returning a result.

+

The AsyncHttpRequest class allows you to access data structure such as HTTP method, URL, path parameters, +query parameters, cookies, etc.

+

When you click the "rebuild" button in IDE and run the "MainApp", the new function will be available in the +application. Alternatively, you can also do mvn clean package to generate a new executable JAR and run the +JAR from command line.

+

To test your new function, visit http://127.0.0.1:8085/api/hello/my/function

+

Event driven design

+

Your function automatically uses an in-memory event bus. The HTTP request from the browser is converted to +an event by the system for delivery to your function as the "input" argument.

+

The underlying HTTP server is asynchronous and non-blocking. +i.e. it does not consume CPU resources while waiting for a response.

+

This composable architecture allows you to design and implement applications so that you have precise control of +performance and throughput. Performance tuning is much easier.

+

Deploy your new application

+

You can assemble related functions in a single composable application, and it can be compiled and built into +a single "executable" for deployment using mvn clean package.

+

The executable JAR is in the target folder.

+

Composable application is by definition cloud native. It is designed to be deployable using Kubernetes or serverless.

+

A sample Dockerfile for your executable JAR may look like this:

+
FROM adoptopenjdk/openjdk11:jre-11.0.11_9-alpine
+EXPOSE 8083
+WORKDIR /app
+COPY target/your-app-name.jar .
+ENTRYPOINT ["java","-jar","your-app-name.jar"]
+
+


+ + + + + + + + + + + + + +
HomeChapter-2
Table of ContentsFunction Execution Strategy
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/CHAPTER-10/index.html b/docs/guides/CHAPTER-10/index.html new file mode 100644 index 000000000..927d45b97 --- /dev/null +++ b/docs/guides/CHAPTER-10/index.html @@ -0,0 +1,388 @@ + + + + + + + + Chapter-10 - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Migration Guide

+

Let's discuss some migration tips from Mercury version 2 to 3.

+

Breaking changes

+

Mercury version 3 is a fully non-blocking event system.

+

If you are using Mercury version 2 for production, please note that version 2 codebase has been archived +to the "release/v2-8-0" branch.

+

To enjoy rapid software development and higher application performance and throughput, we recommend porting +your application to Mercury version 3 as soon as possible.

+

The following are the breaking changes that require some code refactoring:

+
    +
  1. Retired blocking APIs - the "po.request" methods for RPC have been replaced by the new "FastRPC" APIs.
  2. +
  3. Distributed tracing - a new "PostOffice" class is available for compatibility with coroutine.
  4. +
  5. Support three function execution strategies - kernel thread pool, coroutine and suspend function.
  6. +
+

We understand the inconvenience of a major release upgrade in production environment. We believe that the benefits +would out-weight the refactoring effort. Your new code will be composable, easier to read and faster.

+

Writing code with Mercury version 3 platform-core is straight forward. By default, all functions run as coroutines. +To tell the system to execute the function in a kernel thread pool, you may use the KernelThreadRunner annotation.

+

To write a suspend function, you can use IDE (JetBrains Intellij) automated code conversion to copy-n-paste +Java statements into a KotlinLambdaFunction. This is the easiest way to port code. The conversion accuracy is high. +With some minor touch up, you would get your new functions up and running quickly.

+

Step-by-step upgrade

+

Global replace of "PostOffice" to "EventEmitter"

+

The old PostOffice has been renamed as "EventEmitter". You can do a "global search and replace" to change +the class name.

+

Fix broken code for RPC calls

+

Since blocking APIs have been removed, the original PostOffice's "request" methods are no longer available.

+

There are two ways to refactor the RPC calls.

+

Convert code to asynchronous RPC calls

+

You can use the "asyncRequest" methods for RPC and fork-n-join. Since the asyncRequest's result is a "Future" object. +You must implement the "onSuccess" and optionally the "onFailure" logic blocks.

+

Since your new code is asynchronous, the function will immediately return before a future response arrives.

+

If your function may be called by another function, this would break your code. For this use case, you can annotate +your function as an "EventInterceptor" and return a dummy "null" value.

+

As an EventInterceptor, you can inspect metadata of the incoming event to retrieve the "replyTo" and "correlationId".

+
@EventInterceptor
+@PreLoad(route="my.function", instances=10)
+public class MyFunction implements TypedLambdaFunction<EventEnvelope, Void> {
+    @Override
+    public Void handleEvent(Map<String, String> headers, EventEnvelope input, int instance) {
+        PostOffice po = new PostOffice(headers, instance);
+        // make asyncRequest RPC call
+        EventEnvelope request = new EventEnvelope().setTo("some.target.service")
+                                        .setBody(input.getBody());
+        po.asyncRequest(request, 5000)
+                .onSuccess(result -> {
+                    String replyTo = input.getReplyTo();
+                    String correlationId = input.getCorrelationId();
+                    if (replyTo != null && correlationId != null) {
+                        EventEnvelope response = new EventEnvelope();
+                        response.setTo(replyTo).setBody(result.getBody())
+                                .setCorrelationId(correlationId);
+                        po.send(response);
+                    }
+                });
+        return null;
+    }
+}
+
+

In the above example, "my.function" will immediately return a dummy "null" value which will be ignored by the +event system.

+

When it receives a response from a downstream service, it can return result to the upstream service by +asynchronously sending a response.

+

Convert RPC code to a suspend function

+

You can convert your function containing RPC calls to a suspend function using the KotlinLambdaFunction interface.

+

It may look like this:

+
@PreLoad(route="my.function", instances=10)
+class MyFunction: KotlinLambdaFunction<EventEnvelope, Any> {
+    override suspend fun handleEvent(headers: Map<String, String>, input: EventEnvelope, 
+                                     instance: Int): Any {
+        val fastRPC = FastRPC(headers)
+        val request = EventEnvelope().setTo("some.target.service").setBody(input.body)
+        return fastRPC.awaitRequest(request, 5000)
+    }
+}
+
+

The above example serves the same purpose as the asynchronous "my.function" earlier. +The code is much easier to read because it is expressed in a sequential manner.

+

Sequential non-blocking code communicates the intent clearly, and we highly recommend this coding style.

+

If you are new to Kotlin, you may want to leverage the Intellij IDE automated code conversion feature.

+

Just create a dummy Java class as a sketchpad. Write your code in Java and copy-n-paste the Java statements +into the new Kotlin class. The IDE will convert the code automatically. The code conversion is highly accurate. +With some minor touch up, your new code will be up and running quickly.

+

The new PostOffice API

+

The new PostOffice class is backward compatible with the original asynchronous RPC and fork-n-join methods.

+

You can obtain an instance of the PostOffice API in the "handleEvent" method of your function.

+

The PostOffice constructor takes function route name, optional trace ID and path from the headers of the incoming +event. These are READ only metadata inserted by the event system. It also needs the worker instance number to +track the current transaction.

+
@Override
+public Map<String, Object> handleEvent(Map<String, String> headers, 
+                                      EventEnvelope event, int instance) {
+    PostOffice po = new PostOffice(headers, instance);
+    // your business logic here
+}
+
+

When you use the PostOffice to send events or make RPC calls to other functions, the system can propagate +distributed tracing information along the transaction flow automatically.

+

The new FastRPC API

+

The non-blocking "awaitRequest" methods for RPC and fork-n-join are available in a new FastRPC kotlin class. +The constructor is similar to the PostOffice.

+
val fastRPC = FastRPC(headers)
+
+

Distributed tracing

+

The new PostOffice and FastRPC will propagate distributed tracing information along multiple functions in +a transaction path. It will automatically detect if "tracing" is enabled for a transaction.

+

AsyncHttpClient service

+

In Mercury version 3, the "async.http.request" function can be used as a non-blocking HTTP client.

+

To make an HTTP request to an external REST endpoint, you can create an HTTP request object using the AsyncHttpRequest +class and make an async RPC call to the "async.http.request" function like this:

+
PostOffice po = new PostOffice(headers, instance);
+AsyncHttpRequest req = new AsyncHttpRequest();
+req.setMethod("GET");
+req.setHeader("accept", "application/json");
+req.setUrl("/api/hello/world?hello world=abc");
+req.setQueryParameter("x1", "y");
+List<String> list = new ArrayList<>();
+list.add("a");
+list.add("b");
+req.setQueryParameter("x2", list);
+req.setTargetHost("http://127.0.0.1:8083");
+EventEnvelope request = new EventEnvelope().setTo("async.http.request").setBody(req);
+Future<EventEnvelope> res = po.asyncRequest(request, RPC_TIMEOUT);
+res.onSuccess(response -> {
+   // do something with the result 
+});
+
+

In a suspend function using KotlinLambdaFunction, the same logic may look like this:

+
val req = AsyncHttpRequest()
+req.setMethod("GET")
+req.setHeader("accept", "application/json")
+req.setUrl("/api/hello/world?hello world=abc")
+req.setQueryParameter("x1", "y")
+val list: MutableList<String> = ArrayList()
+list.add("a")
+list.add("b")
+req.setQueryParameter("x2", list)
+req.setTargetHost("http://127.0.0.1:8083")
+val request = EventEnvelope().setTo("some.target.service").setBody(req)
+val response = fastRPC.awaitRequest(request, 5000)
+// do something with the result
+
+

There is virtually no performance difference between the asynchronous approach and sequential non-blocking style. +However, the latter demands less CPU resources and yields higher throughput.

+

Kernel thread pool

+

A Java function implementing the LambdaFunction or TypedLambdaFunction will be executed as a coroutine.

+

To tell the system to run a function using kernel thread pool, you can add the KernelThreadRunner annotation. +When using a kernel thread pool, please reduce the number of concurrent worker instances when you register +your function.

+

You can register your function using the PreLoad annotation. For on-demand functions, you can programmatically +register the function using the platform APIs.

+

Java provides preemptive multitasking using kernel threads. It offers the highest performance in terms of +operations per second. If your function is computational intensive and long-running, this function execution +strategy is ideal.

+

However, please be reminded that kernel thread pool is a finite resources and thus an application should not run too +many concurrent kernel threads. The context switching overheads would significantly reduce overall performance +when the number of concurrent kernel threads exceed the available CPU power. A rule of thumb is to keep the number +of concurrent kernel threads to around 100.

+

Coroutine

+

By default, the system will execute functions in the event loop, thus reducing CPU load.

+

Suspend function

+

For a function that make RPC calls, we would recommend writing it as a suspend function using the KotlinLambdaFunction +interface. This yields higher throughput to support more concurrent users and sessions.

+

Things to avoid

+

You should avoid blocking methods in your functions. For example, the "Synchronous" keyword, Object wait and lock, +"Thread" sleep method, BlockingQueue, etc.

+

The non-blocking "delay" API is a direct replacement of the "Thread.sleep" method. You can also use the PostOffice's +"sendLater" API to schedule an event.

+

Conclusion

+

While Mercury has been enhanced from the ground up, the core APIs are intact. The main breaking change is the +removal of blocking RPC APIs. You should leverage IDE automated code conversion to reduce migration risks.

+

The three function execution strategies would provide low-level control of how your application runs, making +performance tuning more scientific. +

+ + + + + + + + + + + + + + + +
Chapter-9HomeAppendix-I
API overviewTable of ContentsApplication Configuration
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/CHAPTER-2/index.html b/docs/guides/CHAPTER-2/index.html new file mode 100644 index 000000000..96fb7affe --- /dev/null +++ b/docs/guides/CHAPTER-2/index.html @@ -0,0 +1,438 @@ + + + + + + + + Chapter-2 - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Function Execution Strategies

+

Define a function

+

In a composable application, each user function is self-contained with zero dependencies with other user functions.

+

A function is a class that implements the LambdaFunction, TypedLambdaFunction or KotlinLambdaFunction interface. +Within each function boundary, it may have private methods that are fully contained within the class.

+

As discussed in Chapter-1, a function may look like this:

+
@PreLoad(route = "my.first.function", instances = 10)
+public class MyFirstFunction implements TypedLambdaFunction<AsyncHttpRequest, Object> {
+
+    @Override
+    public Object handleEvent(Map<String, String> headers, AsyncHttpRequest input, int instance) {
+        // your business logic here
+        return input;
+    }
+}
+
+

A function is an event listener with the "handleEvent" method. The data structures of input and output are defined +by API interface contract during application design phase. A "route" or "topic" is associated with each function. +When an event arrives to the topic, the function is executed where the event is de-serialized as the function's input.

+

In the above example, the input is AsyncHttpRequest because this function is designed to handle an HTTP request event +from a REST endpoint defined in the "rest.yaml" configuration file. We set the output as "Object" so that there is +flexibility in returning a HashMap or a PoJo. You can also enforce the use of a PoJo by updating the output type.

+

A single transaction may involve multiple functions. For example, the user submits a form from a browser that +sends an HTTP request to a function. In MVC pattern, the function receiving the user's input is the "controller". +It carries out input validation and forwards the event to a business logic function (the "view") +that performs some processing and then submits the event to a data persistent function (the "model") to save +a record into the database.

+

In cloud native application, the transaction flow may be more sophisticated than the typical "mvc" style. You can do +"event orchestration" in the function receiving the HTTP request and then make event requests to various functions.

+

This "event orchestration" can be done by code using the "PostOffice" and/or "FastRPC" API.

+

To further reduce coding effort, you can perform "event orchestration" by configuration using "Event Script". +This feature is available in Mercury version 4.0:

+

Mercury v4: https://github.com/Accenture/mercury-composable

+

Documentation: https://accenture.github.io/mercury-composable/

+

Extensible authentication function

+

You can add authentication function using the optional authentication tag in a service. In "rest.yaml", a service +for a REST endpoint refers to a function in your application.

+

An authentication function can be written using a TypedLambdaFunction that takes the input as a "AsyncHttpRequest". +Your authentication function can return a boolean value to indicate if the request should be accepted or rejected.

+

A typical authentication function may validate an HTTP header or cookie. e.g. forward the "Bearer token" from the +"Authorization" header to your organization's OAuth 2.0 Identity Provider for validation.

+

To approve an incoming request, your custom authentication function can return true.

+

Optionally, you can add "session" key-values by returning an EventEnvelope like this:

+
return new EventEnvelope().setHeader("user_id", "A12345").setBody(true);
+
+

The above example approves the incoming request and returns a "session" variable ("user_id": "A12345") to the next task.

+

If your authentication function returns false, the user will receive a "HTTP-401 Unauthorized" error response.

+

You can also control the status code and error message by throwing an AppException like this:

+
throw new AppException(401, "Invalid credentials");
+
+

A composable application is assembled from a collection of modular functions. For example, data persistence functions +and authentication functions are likely to be reusable in many applications.

+

Number of workers for a function

+
@PreLoad(route = "my.first.function", instances = 10)
+
+

In the above function, the parameter "instances" tells the system to reserve a number of workers for the function. +Workers are running on-demand to handle concurrent user requests.

+

Note that you can use smaller number of workers to handle many concurrent users if your function finishes +processing very quickly. If not, you should reserve more workers to handle the work load.

+

Concurrency requires careful planning for optimal performance and throughput. +Let's review the strategies for function execution.

+

Three strategies for function execution

+

A function is executed when an event arrives. There are three function execution strategies.

+ + + + + + + + + + + + + + + + + + + + + + + + + +
StrategyAdvantageDisadvantage
Kernel threadsHighest performance in terms of
operations per seconds
Lower number of concurrent threads
due to high context switching overheads
CoroutineHighest throughput in terms of
concurrent users served by virtual
threads concurrently
Not suitable for long running tasks
Suspend functionSynchronous "non-blocking" for
RPC (request-response) that
makes code easier to read and
maintain
Not suitable for long running tasks
+

Kernel thread pool

+

When you write a function using LambdaFunction and TypedLambdaFunction with the KernelThreadRunner annotation, +the function will be executed using "kernel thread pool" and the Java VM will run your function in native +"preemptive multitasking" mode.

+

While preemptive multitasking fully utilizes the CPU, its context switching overheads may increase as the number of +kernel threads grow. As a rule of thumb, you should control the maximum number of kernel threads to less than 200.

+

The parameter kernel.thread.pool is defined with a default value of 100. You can change this value to adjust to +the actual CPU power in your environment. Keep the default value for best performance unless you have tested the +limit in your environment.

+
+

When you have more concurrent requests, your application may slow down because some functions + are blocked when the number of concurrent kernel threads is reached.

+
+

You should reduce the number of "instances" (i.e. worker pool) for a function to a small number so that your +application does not exceed the maximum limit of the kernel.thread.pool parameter.

+

Kernel threads are precious and finite resources. When your function is computational intensive or making +external HTTP or database calls in a synchronous blocking manner, you may use it with a small number +of worker instances.

+

To rapidly release kernel thread resources, you can write "asynchronous" code. i.e. for event-driven programming, +you can send event to another function asynchronously, and you can create a callback function to listen +to responses.

+

For RPC call, you can use the asyncRequest method to write asynchronous RPC calls. However, coding for asynchronous +RPC pattern is more challenging. For example, you may want to return a "pending" result immediately using HTTP-202. +Your code will move on to execute using a "future" that will execute callback methods (onSuccess and onFailure). +Another approach is to annotate the function as an EventInterceptor so that your function can respond to the user +in a "future" callback.

+

For ease of programming, we recommend using suspend function to handle RPC calls. It allows you to program your +function in a sequential manner similar to the "async/await" pattern of other programming languages.

+

Coroutine

+

By default, the system will run your function as a coroutine unless you specify KernelThreadRunner annotation in +your function or declared it as a suspend function using KotlinLambdaFunction interface.

+

Normally, coroutines are executed in an event loop using a single kernel thread. Note that the underlying Eclipse +vertx is a multithreaded event system that executes coroutines in a small number of event loops concurrently for +better performance. As a result, the system can handle tens of thousands of coroutines running concurrently.

+

Since coroutine is running in a single thread, you must avoid writing "blocking" code because it would slow down +the whole application significantly.

+

If your function can finish processing very quickly, coroutine is ideal.

+

Suspend function

+

A suspend function is a coroutine that can be suspended and resumed. The best use case for a suspend function is +for handling of "sequential non-blocking" request-response. This is the same as "async/await" in node.js and other +programming language.

+

To implement a "suspend function", you must implement the KotlinLambdaFunction interface and write code in Kotlin.

+

If you are new to Kotlin, please download and run JetBrains Intellij IDE. The quickest way to get productive in Kotlin +is to write a few statements of Java code in a placeholder class and then copy-n-paste the Java statements into the +KotlinLambdaFunction's handleEvent method. Intellij will automatically convert Java code into Kotlin.

+

The automated code conversion is mostly accurate (roughly 90%). You may need some touch up to polish the converted +Kotlin code.

+

In a suspend function, you can use a set of "await" methods to make non-blocking request-response (RPC) calls. +For example, to make a RPC call to another function, you can use the awaitRequest method.

+

Please refer to the FileUploadDemo class in the "examples/lambda-example" project.

+
val po = PostOffice(headers, instance)
+val fastRPC = FastRPC(headers)
+
+val req = EventEnvelope().setTo(streamId).setHeader(TYPE, READ)
+while (true) {
+    val event = fastRPC.awaitRequest(req, 5000)
+    // handle the response event
+    if (EOF == event.headers[TYPE]) {
+        log.info("{} saved", file)
+        awaitBlocking {
+            out.close()
+        }
+        po.send(streamId, Kv(TYPE, CLOSE))
+        break;
+    }
+    if (DATA == event.headers[TYPE]) {
+        val block = event.body
+        if (block is ByteArray) {
+            total += block.size
+            log.info("Saving {} - {} bytes", filename, block.size)
+            awaitBlocking {
+                out.write(block)
+            }
+        }
+    }
+}
+
+

In the above code segment, it has a "while" loop to make RPC calls to continuously "fetch" blocks of data +from a stream. The status of the stream is indicated in the event header "type". It will exit the "while" loop +when it detects the "End of Stream (EOF)" signal.

+

Suspend function will be "suspended" when it is waiting for a response. When it is suspended, it does not +consume CPU resources, thus your application can handle a large number of concurrent users and requests.

+

Coroutines run in a "cooperative multitasking" manner. Technically, each function is running sequentially. +However, when many functions are suspended during waiting, it appears that all functions are running concurrently.

+

You may notice that there is an awaitBlocking wrapper in the code segment.

+

Sometimes, you cannot avoid blocking code. In the above example, the Java's FileOutputStream is a blocking method. +To ensure that a small piece of blocking code in a coroutine does not slow down the "event loop", +you can apply the awaitBlocking wrapper method. The system will run the blocking code in a separate worker thread +without blocking the event loop.

+

In addition to the "await" sets of API, the delay(milliseconds) method puts your function into sleep in a +non-blocking manner. The yield() method is useful when your function requires more time to execute complex +business logic. You can add the yield() statement before you execute a block of code. The yield method releases +control to the event loop so that other coroutines and suspend functions will not be blocked by a heavy weighted +function.

+

Suspend function is a powerful way to write high throughput application. Your code is presented in a sequential +flow that is easier to write and maintain.

+

You may want to try the demo "file upload" REST endpoint to see how suspend function behaves. If you follow Chapter-1, +your lambda example application is already running. To test the file upload endpoint, here is a simple Python script:

+
import requests
+files = {'file': open('some_data_file.txt', 'rb')}
+r = requests.post('http://127.0.0.1:8085/api/upload', files=files)
+print(r.text)
+
+

This assumes you have the python "requests" package installed. If not, please do pip install requests to install +the dependency.

+

The uploaded file will be kept in the "/tmp/upload-download-demo" folder.

+

To download the file, point your browser to http://127.0.0.1:8085/api/download/some_data_file.txt +Your browser will usually save the file in the "Downloads" folder.

+

You may notice that the FileDownloadDemo class is written in Java using the interface +TypedLambdaFunction<AsyncHttpRequest, EventEnvelope>. The FileDownloadDemo class will run using a kernel thread.

+

Note that each function is independent and the functions with different execution strategies can communicate in events.

+

The output of your function is an "EventEnvelope" so that you can set the HTTP response header correctly. +e.g. content type and filename.

+

When downloading a file, the FileDownloadDemo function will block if it is sending a large file. +Therefore, you want it to run as a kernel thread.

+

For very large file download, you may want to write the FileDownloadDemo function using asynchronous programming +with the EventInterceptor annotation or implement a suspend function using KotlinLambdaFunction. Suspend function +is non-blocking. +

+ + + + + + + + + + + + + + + +
Chapter-1HomeChapter-3
IntroductionTable of ContentsREST Automation
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/CHAPTER-3/index.html b/docs/guides/CHAPTER-3/index.html new file mode 100644 index 000000000..410886e72 --- /dev/null +++ b/docs/guides/CHAPTER-3/index.html @@ -0,0 +1,450 @@ + + + + + + + + Chapter-3 - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

REST Automation

+

The platform-core foundation library contains a built-in non-blocking HTTP server that you can use to create REST +endpoints. Behind the curtain, it is using the vertx web client and server libraries.

+

The REST automation system is not a code generator. The REST endpoints in the rest.yaml file are handled by +the system directly - "Config is the code".

+

We will use the "rest.yaml" sample configuration file in the "lambda-example" project to elaborate the configuration +approach.

+

The rest.yaml configuration has three sections:

+
    +
  1. REST endpoint definition
  2. +
  3. CORS header processing
  4. +
  5. HTTP header transformation
  6. +
+

Turn on the REST automation engine

+

REST automation is optional. To turn on REST automation, add or update the following parameters in the +application.properties file (or application.yml if you like).

+
rest.server.port=8085
+rest.automation=true
+yaml.rest.automation=classpath:/rest.yaml
+
+

When rest.automation=true, you can configure the server port using rest.server.port or server.port.

+

REST automation can co-exist with Spring Boot. Please use rest.server.port for REST automation and +server.port for Spring Boot.

+

The yaml.rest.automation tells the system the location of the rest.yaml configuration file.

+

You can configure more than one location and the system will search them sequentially. The following example +tells the system to load rest.yaml from "/tmp/config/rest.yaml". If the file is not available, it will use +the rest.yaml in the project's resources folder.

+
yaml.rest.automation=file:/tmp/config/rest.yaml, classpath:/rest.yaml
+
+

Defining a REST endpoint

+

The "rest" section of the rest.yaml configuration file may contain one or more REST endpoints.

+

A REST endpoint may look like this:

+
  - service: ["hello.world"]
+    methods: ['GET', 'PUT', 'POST', 'HEAD', 'PATCH', 'DELETE']
+    url: "/api/hello/world"
+    timeout: 10s
+    cors: cors_1
+    headers: header_1
+    threshold: 30000
+    tracing: true
+
+

In this example, the URL for the REST endpoint is "/api/hello/world" and it accepts a list of HTTP methods. +When an HTTP request is sent to the URL, the HTTP event will be sent to the function declared with service route name +"hello.world". The input event will be the "AsyncHttpRequest" object. Since the "hello.world" function is written +as an inline LambdaFunction in the lambda-example application, the AsyncHttpRequest is converted to a HashMap.

+

To process the input as an AsyncHttpRequest object, the function must be written as a regular class. See the +"services" folder of the lambda-example for additional examples.

+

The "timeout" value is the maximum time that REST endpoint will wait for a response from your function. +If there is no response within the specified time interval, the user will receive an HTTP-408 timeout exception.

+

The "authentication" tag is optional. If configured, the route name given in the authentication tag will be used. +The input event will be delivered to a function with the authentication route name. In this example, it is +"v1.api.auth".

+

Your custom authentication function may look like this:

+
@PreLoad(route = "v1.api.auth", instances = 10)
+public class SimpleAuthentication implements TypedLambdaFunction<AsyncHttpRequest, Object> {
+
+    @Override
+    public Object handleEvent(Map<String, String> headers, AsyncHttpRequest input, int instance) {
+        // Your authentication logic here. The return value should be true or false.
+        return result;
+    }
+}
+
+

Your authentication function can return a boolean value to indicate if the request should be accepted or rejected.

+

If true, the system will send the HTTP request to the service. In this example, it is the "hello.world" function. +If false, the user will receive an "HTTP-401 Unauthorized" exception.

+

Optionally, you can use the authentication function to return some session information after authentication. +For example, your authentication can forward the "Authorization" header of the incoming HTTP request to your +organization's OAuth 2.0 Identity Provider for authentication.

+

To return session information to the next function, the authentication function can return an EventEnvelope. +It can set the session information as key-values in the response event headers.

+

In the lambda-example application, there is a demo authentication function in the AuthDemo class with the +"v1.api.auth" route name. To demonstrate passing session information, the AuthDemo class set the header +"user=demo" in the result EventEnvelope.

+

You can test this by visiting http://127.0.0.1:8085/api/hello/generic/1 to invoke the "hello.generic" function.

+

The console will print:

+
DistributedTrace:55 - trace={path=GET /api/hello/generic/1, service=v1.api.auth, success=true,
+  origin=20230326f84dd5f298b64be4901119ce8b6c18be, exec_time=0.056, start=2023-03-26T20:08:01.702Z, 
+  from=http.request, id=aa983244cef7455cbada03c9c2132453, round_trip=1.347, status=200}
+HelloGeneric:56 - Got session information {user=demo}
+DistributedTrace:55 - trace={path=GET /api/hello/generic/1, service=hello.generic, success=true, 
+  origin=20230326f84dd5f298b64be4901119ce8b6c18be, start=2023-03-26T20:08:01.704Z, exec_time=0.506, 
+  from=v1.api.auth, id=aa983244cef7455cbada03c9c2132453, status=200}
+DistributedTrace:55 - trace={path=GET /api/hello/generic/1, service=async.http.response, 
+  success=true, origin=20230326f84dd5f298b64be4901119ce8b6c18be, start=2023-03-26T20:08:01.705Z, 
+  exec_time=0.431, from=hello.generic, id=aa983244cef7455cbada03c9c2132453, status=200}
+
+

This illustrates that the HTTP request has been processed by the "v1.api.auth" function. The "hello.generic" function +is wired to the "/api/hello/generic/{id}" endpoint as follows:

+
  - service: "hello.generic"
+    methods: ['GET']
+    url: "/api/hello/generic/{id}"
+    # Turn on authentication pointing to the "v1.api.auth" function
+    authentication: "v1.api.auth"
+    timeout: 20s
+    cors: cors_1
+    headers: header_1
+    tracing: true
+
+

The tracing tag tells the system to turn on "distributed tracing". In the console log shown above, you see +three lines of log from "distributed trace" showing that the HTTP request is processed by "v1.api.auth" and +"hello.generic" before returning result to the browser using the "async.http.response" function.

+
+

Note: the "async.http.response" is a built-in function to send the HTTP response to the browser.

+
+

The optional cors and headers tags point to the specific CORS and HEADERS sections respectively.

+

CORS section

+

For ease of development, you can define CORS headers using the CORS section like this.

+

This is a convenient feature for development. For cloud native production system, it is most likely that +CORS processing is done at the API gateway level.

+

You can define different sets of CORS headers using different IDs.

+
cors:
+  - id: cors_1
+    options:
+      - "Access-Control-Allow-Origin: ${api.origin:*}"
+      - "Access-Control-Allow-Methods: GET, DELETE, PUT, POST, PATCH, OPTIONS"
+      - "Access-Control-Allow-Headers: Origin, Authorization, X-Session-Id, X-Correlation-Id,
+                                       Accept, Content-Type, X-Requested-With"
+      - "Access-Control-Max-Age: 86400"
+    headers:
+      - "Access-Control-Allow-Origin: ${api.origin:*}"
+      - "Access-Control-Allow-Methods: GET, DELETE, PUT, POST, PATCH, OPTIONS"
+      - "Access-Control-Allow-Headers: Origin, Authorization, X-Session-Id, X-Correlation-Id, 
+                                       Accept, Content-Type, X-Requested-With"
+      - "Access-Control-Allow-Credentials: true"
+
+

HEADERS section

+

The HEADERS section is used to do some simple transformation for HTTP request and response headers.

+

You can add, keep or drop headers for HTTP request and response. Sample HEADERS section is shown below.

+
headers:
+  - id: header_1
+    request:
+      #
+      # headers to be inserted
+      #    add: ["hello-world: nice"]
+      #
+      # keep and drop are mutually exclusive where keep has precedent over drop
+      # i.e. when keep is not empty, it will drop all headers except those to be kept
+      # when keep is empty and drop is not, it will drop only the headers in the drop list
+      # e.g.
+      # keep: ['x-session-id', 'user-agent']
+      # drop: ['Upgrade-Insecure-Requests', 'cache-control', 'accept-encoding', 'connection']
+      #
+      drop: ['Upgrade-Insecure-Requests', 'cache-control', 'accept-encoding', 'connection']
+
+    response:
+      #
+      # the system can filter the response headers set by a target service,
+      # but it cannot remove any response headers set by the underlying servlet container.
+      # However, you may override non-essential headers using the "add" directive.
+      # i.e. don't touch essential headers such as content-length.
+      #
+      #     keep: ['only_this_header_and_drop_all']
+      #     drop: ['drop_only_these_headers', 'another_drop_header']
+      #
+      #      add: ["server: mercury"]
+      #
+      # You may want to add cache-control to disable browser and CDN caching.
+      # add: ["Cache-Control: no-cache, no-store", "Pragma: no-cache", 
+      #       "Expires: Thu, 01 Jan 1970 00:00:00 GMT"]
+      #
+      add:
+        - "Strict-Transport-Security: max-age=31536000"
+        - "Cache-Control: no-cache, no-store"
+        - "Pragma: no-cache"
+        - "Expires: Thu, 01 Jan 1970 00:00:00 GMT"
+
+

Static content

+

Static content (HTML/CSS/JS bundle), if any, can be placed in the "resources/public" folder in your +application project root. It is because the default value for the "static.html.folder" parameter +in the application configuration is "classpath:/resources/public". If you want to place your +static content elsewhere, you may adjust this parameter. You may point it to the local file system +such as "file:/tmp/html".

+

For security reason, you may add the following configuration in the rest.yaml. +The following example is shown in the unit test section of the platform-core library module.

+
#
+# Optional static content handling for HTML/CSS/JS bundle
+# -------------------------------------------------------
+#
+# no-cache-pages - tells the browser not to cache some specific pages
+#
+# The "filter" section is a programmatic way to protect certain static content.
+#
+# The filter can be used to inspect HTTP path, headers and query parameters.
+# The typical use case is to check cookies and perform browser redirection
+# for SSO login. Another use case is to selectively add security HTTP
+# response headers such as cache control and X-Frame-Options. You can also
+# perform HTTP to HTTPS redirection.
+#
+# Syntax for the "no-cache-pages", "path" and "exclusion" parameters are:
+# 1. Exact match - complete path
+# 2. Match "startsWith" - use a single "*" as the suffix
+# 3. Match "endsWith" - use a single "*" as the prefix
+#
+# If filter is configured, the path and service parameters are mandatory
+# and the exclusion parameter is optional.
+#
+# In the following example, it will intercept the home page, all contents
+# under "/assets/" and any files with extensions ".html" and ".js".
+# It will ignore all CSS files.
+#
+static-content:
+  no-cache-pages: ["/", "/index.html"]
+  filter:
+    path: ["/", "/assets/*", "*.html", "*.js"]
+    exclusion: ["*.css"]
+    service: "http.request.filter"
+
+

A sample request filter function is available in the platform-core project like this:

+
@PreLoad(route="http.request.filter", instances=100)
+public class GetRequestFilter implements TypedLambdaFunction<AsyncHttpRequest, EventEnvelope> {
+
+    @Override
+    public EventEnvelope handleEvent(Map<String, String> headers, AsyncHttpRequest input, int instance) {
+        return new EventEnvelope().setHeader("x-filter", "demo");
+    }
+}
+
+

In the above http.request.filter, it adds a HTTP response header "X-Filter" for the unit test +to validate.

+

If you set status code in the return EventEnvelope to 302 and add a header "Location", the system +will redirect the browser to the given URL in the location header. Please be careful to avoid +HTTP redirection loop.

+

Similarly, you can throw exception and the HTTP request will be rejected with the given status +code and error message accordingly.

+


+ + + + + + + + + + + + + + + +
Chapter-2HomeChapter-4
Function Execution StrategiesTable of ContentsEvent Orchestration
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/CHAPTER-4/index.html b/docs/guides/CHAPTER-4/index.html new file mode 100644 index 000000000..730fbbaa8 --- /dev/null +++ b/docs/guides/CHAPTER-4/index.html @@ -0,0 +1,551 @@ + + + + + + + + Chapter-4 - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Event Orchestration

+

In traditional programming, we can write modular software components and wire them together as a single application. +There are many ways to do that. You can rely on a "dependency injection" framework. In many cases, you would need +to write orchestration logic to coordinate how the various components talk to each other to process a transaction.

+

In a composable application, you write modular functions using the first principle of "input-process-output".

+

Functions communicate with each other using events and each function has a "handleEvent" method to process "input" +and return result as "output". Writing software component in the first principle makes Test Driven Development (TDD) +straight forward. You can write mock function and unit tests before you put in actual business logic.

+

Mocking an event-driven function in a composable application is as simple as overriding the function's route name +with a mock function.

+

Register a function with the in-memory event system

+

There are two ways to register a function:

+
    +
  1. Programmatic registration
  2. +
  3. Declarative registration
  4. +
+

In programmatic registration, you can register a function like this:

+
Platform platform = Platform.getInstance();
+platform.registerPrivate("my.function", new MyFunction(), 10);
+
+

In the above example, You obtain a singleton instance of the Platform API class and use it to register a private +function MyFunction with a route name "my.function".

+

In declarative approach, you use the PreLoad annotation to register a class with an event handler.

+

Your function should implement the LambdaFunction, TypedLambdaFunction or KotlinLambdaFunction. +While LambdaFunction is untyped, the event system can transport PoJo and your function should +test the object type and cast it to the correct PoJo.

+

TypedLambdaFunction and KotlinLambdaFunction are typed, and you must declare the input and output classes +according to the input/output API contract of your function.

+

For example, the SimpleDemoEndpoint has the "PreLoad" annotation to declare the route name and number of worker +instances.

+

By default, LambdaFunction and TypedLambdaFunction are executed as "coroutine" for the worker instances. +To tell the system to run it using kernel threads, you can add the KernelThreadRunner annotation.

+
@KernelThreadRunner
+@PreLoad(route = "hello.simple", instances = 10)
+public class SimpleDemoEndpoint implements TypedLambdaFunction<AsyncHttpRequest, Object> {
+    @Override
+    public Object handleEvent(Map<String, String> headers, AsyncHttpRequest input, int instance) {
+        // business logic here
+    }
+}
+
+

Once a function is created using the declarative method, you can override it with a mock function by using the +programmatic registration method in a unit test.

+

Private vs public functions

+

When you use the programmatic registration approach, you can use the "register" or the "registerPrivate" method to +set the function as "public" or "private" respectively. For declarative approach, the PreLoad annotation +contains a parameter to define the visibility of the function.

+
// or register it as "public"
+platform.register("my.function", new MyFunction(), 10);
+
+// register a function as "private"
+platform.registerPrivate("my.function", new MyFunction(), 10);
+
+

A private function is visible by other functions in the same application memory space.

+

A public function is accessible by other function from another application instance using service mesh or +"Event over HTTP" method. We will discuss inter-container communication in Chapter-7 and +Chapter-8.

+

Post Office API

+

To send an asynchronous event or an event RPC call from one function to another, you can use the PostOffice APIs.

+

In your function, you can obtain an instance of the PostOffice like this:

+
@Override
+public Object handleEvent(Map<String, String> headers, AsyncHttpRequest input, int instance) {
+    PostOffice po = new PostOffice(headers, instance);
+    // e.g. po.send and po.asyncRequest for sending asynchronous event and making RPC call
+}
+
+

The PostOffice API detects if tracing is enabled in the incoming request. If yes, it will propagate tracing +information to "downstream" functions.

+

Event patterns

+
    +
  1. RPC “Request-response”, best for interactivity
  2. +
  3. Asynchronous e.g. Drop-n-forget
  4. +
  5. Callback e.g. Progressive rendering
  6. +
  7. Pipeline e.g. Work-flow application
  8. +
  9. Streaming e.g. File transfer
  10. +
+

Request-response (RPC)

+

In enterprise application, RPC is the most common pattern in making call from one function to another.

+

The "calling" function makes a request and waits for the response from the "called" function.

+

In Mercury version 3, there are 2 types of RPC calls - "asynchronous" and "sequential non-blocking".

+

Asynchronous RPC

+

You can use the asyncRequest method to make an asynchronous RPC call. Asynchronous means that the response +will be delivered to the onSuccess or onFailure callback method.

+

Note that normal response and exception are sent to the onSuccess method and timeout exception to the onFailure +method.

+

If you set "timeoutException" to false, the timeout exception will be delivered to the onSuccess callback and +the onFailure callback will be ignored.

+
Future<EventEnvelope> asyncRequest(final EventEnvelope event, long timeout) 
+                                   throws IOException;
+Future<EventEnvelope> asyncRequest(final EventEnvelope event, long timeout, 
+                                   boolean timeoutException) throws IOException;
+
+// example
+EventEnvelope request = new EventEnvelope().setTo(SERVICE).setBody(TEXT);
+Future<EventEnvelope> response = po.asyncRequest(request, 2000);
+response.onSuccess(result -> {
+    // handle the response event
+}).onFailure(ex -> {
+    // handle timeout exception
+});
+
+

The timeout value is measured in milliseconds.

+

Asynchronous fork-n-join

+

A special version of RPC is the fork-n-join API. This allows you to make concurrent requests to multiple functions. +The system will consolidate all responses and return them as a list of events.

+

Normal responses and user defined exceptions are sent to the onSuccess method and timeout exception to the onFailure +method. Your function will receive all responses or a timeout exception.

+

If you set "timeoutException" to false, partial results will be delivered to the onSuccess method when one or +more services fail to respond on-time. The onFailure method is not required.

+
Future<List<EventEnvelope>> asyncRequest(final List<EventEnvelope> event, long timeout) 
+                                         throws IOException;
+
+Future<List<EventEnvelope>> asyncRequest(final List<EventEnvelope> event, long timeout, 
+                                         boolean timeoutException) throws IOException;
+
+// example
+List<EventEnvelope> requests = new ArrayList<>();
+requests.add(new EventEnvelope().setTo(SERVICE1).setBody(TEXT1));
+requests.add(new EventEnvelope().setTo(SERVICE2).setBody(TEXT2));
+Future<List<EventEnvelope>> responses = po.asyncRequest(requests, 2000);
+responses.onSuccess(events -> {
+    // handle the response events
+}).onFailure(ex -> {
+    // handle timeout exception
+});
+
+

Asynchronous programming technique

+

When your function is a service by itself, asynchronous RPC and fork-n-join require different programming approaches.

+

There are two ways to do that: +1. Your function returns an immediate result and waits for the response(s) to the onSuccess or onFailure callback +2. Your function is implemented as an "EventInterceptor"

+

For the first approach, your function can return an immediate result telling the caller that your function would need +time to process the request. This works when the caller can be reached by a callback.

+

For the second approach, your function is annotated with the keyword EventInterceptor. +It can immediately return a "null" response that will be ignored by the event system. Your function can inspect +the "replyTo" address and correlation ID in the incoming event and include them in a future response to the caller.

+

Sequential non-blocking RPC and fork-n-join

+

To simplify coding, you can implement a "suspend function" using the KotlinLambdaFunction interface.

+

The following code segment illustrates the creation of the "hello.world" function that makes a non-blocking RPC +call to "another.service".

+
@PreLoad(route="hello.world", instances=10)
+class FileUploadDemo: KotlinLambdaFunction<AsyncHttpRequest, Any> {
+    override suspend fun handleEvent(headers: Map<String, String>, input: AsyncHttpRequest, 
+                                     instance: Int): Any {
+        val fastRPC = FastRPC(headers)
+        // your business logic here...
+        val req = EventEnvelope().setTo("another.service").setBody(myPoJo)
+        return fastRPC.awaitRequest(req, 5000)
+    }
+}
+
+

The API method signature for non-blocking RPC and fork-n-join are as follows:

+
@Throws(IOException::class)
+suspend fun awaitRequest(request: EventEnvelope, timeout: Long): EventEnvelope
+
+@Throws(IOException::class)
+suspend fun awaitRequest(requests: List<EventEnvelope>, timeout: Long): List<EventEnvelope>
+
+

Asynchronous drop-n-forget

+

To make an asynchronous call from one function to another, use the send method.

+
void send(String to, Kv... parameters) throws IOException;
+void send(String to, Object body) throws IOException;
+void send(String to, Object body, Kv... parameters) throws IOException;
+void send(final EventEnvelope event) throws IOException;
+
+

Kv is a key-value pair for holding one parameter.

+

Asynchronous event calls are handled in the background so that your function can continue processing. +For example, sending a notification message to a user.

+

Callback

+

You can declare another function as a "callback". When you send a request to another function, you can set the +"replyTo" address in the request event. When a response is received, your callback function will be invoked to +handle the response event.

+
EventEnvelope req = new EventEnvelope().setTo("some.service")
+                        .setBody(myPoJo).setReplyTo("my.callback");
+po.send(req);
+
+

In the above example, you have a callback function with route name "my.callback". You send the request event +with a MyPoJo object as payload to the "some.service" function. When a response is received, the "my.callback" +function will get the response as input.

+

Pipeline

+

Pipeline is a linked list of event calls. There are many ways to do pipeline. One way is to keep the pipeline plan +in an event's header and pass the event across multiple functions where you can set the "replyTo" address from the +pipeline plan. You should handle exception cases when a pipeline breaks in the middle of a transaction.

+

An example of the pipeline header key-value may look like this:

+
pipeline=service.1, service.2, service.3, service.4, service.5
+
+

In the above example, when the pipeline event is received by a function, the function can check its position +in the pipeline by comparing its own route name with the pipeline plan.

+
PostOffice po = new PostOffice(headers, instance);
+
+// some business logic here...
+String myRoute = po.getRoute();
+
+

Suppose myRoute is "service.2", the function can send the response event to "service.3". +When "service.3" receives the event, it can send its response event to the next one. i.e. "service.4".

+

When the event reaches the last service ("service.5"), the processing will complete.

+

Streaming

+

If you set a function as singleton (i.e. one worker instance), it will receive event in an orderly fashion. +This way you can "stream" events to the function, and it will process the events one by one.

+

Another means to do streaming is to create an "ObjectStreamIO" event stream like this:

+
ObjectStreamIO stream = new ObjectStreamIO(60);
+ObjectStreamWriter out = new ObjectStreamWriter(stream.getOutputStreamId());
+out.write(messageOne);
+out.write(messageTwo);
+out.close();
+
+String streamId = stream.getInputStreamId();
+// pass the streamId to another function
+
+

In the code segment above, your function creates an object event stream and writes 2 messages into the stream +It obtains the streamId of the event stream and sends it to another function. The other function can read the +data blocks orderly.

+

You must declare "end of stream" by closing the output stream. If you do not close an output stream, +it remains open and idle. If a function is trying to read an input stream using the stream ID and the +next data block is not available, it will time out.

+

A stream will be automatically closed when the idle inactivity timer is reached. In the above example, +ObjectStreamIO(60) means an idle inactivity timer of 60 seconds.

+
+

IMPORTANT: To improve the non-blocking design of your function, you can implement your function as a + KotlinLambdaFunction. If you need to send many blocks of data continuously in a "while" + loop, you should add the "yield()" statement before it writes a block of data to the + output stream. This way, a long-running function will be non-blocking.

+
+

There are two ways to read an input event stream - asynchronous or sequential non-blocking.

+

AsyncObjectStreamReader

+

To read events from a stream, you can create an instance of the AsyncObjectStreamReader like this:

+
AsyncObjectStreamReader in = new AsyncObjectStreamReader(stream.getInputStreamId(), 8000);
+Future<Object> block = in.get();
+block.onSuccess(b -> {
+    if (b != null) {
+        // process the data block
+    } else {
+        // end of stream. Do additional processing.
+        in.close();
+    }
+});
+
+

The above illustrates reading the first block of data. The function would need to iteratively read the stream +until end of stream (i.e. when the stream returns null). As a result, asynchronous application code for stream +processing is more challenging to write.

+

Sequential non-blocking method

+

The industry trend is to use sequential non-blocking method instead of "asynchronous callback" because your code +will be much easier to read.

+

You can use the awaitRequest method to read the next block of data from an event stream.

+

An example for reading a stream is shown in the FileUploadDemo kotlin class in the lambda-example project. +It is using a simple "while" loop to read the stream. When the function fetches the next block of data using +the awaitRequest method, the function is suspended until the next data block or "end of stream" signal is received.

+

It may look like this:

+
val po = PostOffice(headers, instance)
+val fastRPC = FastRPC(headers)
+
+val req = EventEnvelope().setTo(streamId).setHeader(TYPE, READ)
+while (true) {
+    val event = fastRPC.awaitRequest(req, 5000)
+    if (event.status == 408) {
+        // handle input stream timeout
+        break
+    }
+    if ("eof" == event.headers["type"]) {
+        po.send(streamId, Kv("type", "close"))
+        break
+    }
+    if ("data" == event.headers["type"]) {
+        val block = event.body
+        if (block is ByteArray) {
+            // handle the data block from the input stream
+        }
+    }
+}
+
+

Since the code style is "sequential non-blocking", using a "while" loop does not block the "event loop" provided +that you are using an "await" API inside the while-loop.

+

In this fashion, the intent of the code is clear. Sequential non-blocking method offers high throughput because +it does not consume CPU resources while the function is waiting for a response from another function.

+

We recommend sequential non-blocking style for more sophisticated event streaming logic.

+
+

Note: "await" methods are only supported in KotlinLambdaFunction which is a suspend function. + When Java 19 virtual thread feature becomes officially available, we will enhance + the function execution strategies accordingly.

+
+

Orchestration layer

+

Once you have implemented modular functions in a self-contained manner, the best practice is to write one or more +functions to do "event orchestration".

+

Think of the orchestration function as a music conductor who guides the whole team to perform.

+

For event orchestration, your function can be the "conductor" that sends events to the individual functions so that +they operate together as a single application. To simplify design, the best practice is to apply event orchestration +for each transaction or use case. The event orchestration function also serves as a living documentation about how +your application works. It makes your code more readable.

+

Event Script

+

To automate event orchestration, there is an enterprise add-on module called "Event Script". +This is the idea of "config over code" or "declarative programming". The primary purpose of "Event Script" +is to reduce coding effort so that the team can focus in improving application design and code quality.

+

To use event script, please upgrade to Mercury v4.

+

Mercury v4: https://github.com/Accenture/mercury-composable

+

Documentation: https://accenture.github.io/mercury-composable/

+

In the next chapter, we will discuss the build, test and deploy process. +

+ + + + + + + + + + + + + + + +
Chapter-3HomeChapter-5
REST AutomationTable of ContentsBuild, Test and Deploy
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/CHAPTER-5/index.html b/docs/guides/CHAPTER-5/index.html new file mode 100644 index 000000000..ac4b532ae --- /dev/null +++ b/docs/guides/CHAPTER-5/index.html @@ -0,0 +1,557 @@ + + + + + + + + Chapter-5 - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Build, Test and Deploy

+

The first step in writing an application is to create an entry point for your application.

+

Main application

+

A minimalist main application template is shown as follows:

+
@MainApplication
+public class MainApp implements EntryPoint {
+   public static void main(String[] args) {
+      AutoStart.main(args);
+   }
+   @Override
+   public void start(String[] args) {
+        // your startup logic here
+      log.info("Started");
+   }
+}
+
+

Note that MainApplication is mandatory. You must have at least one "main application" module.

+
+

Note: Please adjust the parameter "web.component.scan" in application.properties + to point to your user application package(s) in your source code project.

+
+

If your application does not require additional startup logic, you may just print a greeting message.

+

The AutoStart.main() statement in the "main" method is used when you want to start your application within the IDE. +You can "right-click" the main method and select "run".

+

You can also build and run the application from command line like this:

+
cd sandbox/mercury/examples/lambda-example
+mvn clean package
+java -jar target/lambda-example-3.0.9.jar
+
+

The lambda-example is a sample application that you can use as a template to write your own code. Please review +the pom.xml and the source directory structure. The pom.xml is pre-configured to support Java and Kotlin.

+

In the lambda-example project root, you will find the following directories:

+
src/main/java
+src/main/kotlin
+src/test/java
+
+

Note that kotlin unit test directory is not included because you can test all functions in Java unit tests.

+

Since all functions are connected using the in-memory event bus, you can test any function by sending events +from a unit test module in Java. If you are comfortable with the Kotlin language, you may also set up Kotlin +unit tests accordingly. There is no harm having both types of unit tests in the same project.

+

Source code documentation

+

Since the source project contains both Java and Kotlin, we have replaced javadoc maven plugin with Jetbrains "dokka" +documentation engine for both Java and Kotlin. Javadoc is useful if you want to write and publish your own libraries.

+

To generate Java and Kotlin source documentation, please run "mvn dokka:dokka". You may "cd" to the platform-core +project to try the maven dokka command to generate some source documentation. The home page will be available +in "target/dokka/index.html"

+

Writing your functions

+

Please follow the step-by-step learning guide in Chapter-1 to write your own functions. You can then +configure new REST endpoints to use your new functions.

+

In Chapter-1, we have discussed the three function execution strategies to optimize your application +to the full potential of stability, performance and throughput.

+

HTTP forwarding

+

In Chapter-3, we have presented the configuration syntax for the "rest.yaml" REST automation +definition file. Please review the sample rest.yaml file in the lambda-example project. You may notice that +it has an entry for HTTP forwarding. The following entry in the sample rest.yaml file illustrates an HTTP +forwarding endpoint. In HTTP forwarding, you can replace the "service" route name with a direct HTTP target host. +You can do "URL rewrite" to change the URL path to the target endpoint path. In the below example, +/api/v1/* will be mapped to /api/* in the target endpoint.

+
  - service: "http://127.0.0.1:${rest.server.port}"
+    trust_all_cert: true
+    methods: ['GET', 'PUT', 'POST']
+    url: "/api/v1/*"
+    url_rewrite: ['/api/v1', '/api']
+    timeout: 20
+    cors: cors_1
+    headers: header_1
+    tracing: true
+
+

Sending HTTP request event to more than one service

+

One feature in REST automation "rest.yaml" configuration is that you can configure more than one function in the +"service" section. In the following example, there are two function route names ("hello.world" and "hello.copy"). +The first one "hello.world" is the primary service provider. The second one "hello.copy" will receive a copy of +the incoming event automatically.

+

This feature allows you to write new version of a function without disruption to current functionality. Once you are +happy with the new version of function, you can route the endpoint directly to the new version by updating the +"rest.yaml" configuration file.

+
  - service: ["hello.world", "hello.copy"]
+
+

Writing your first unit test

+

Please refer to "rpcTest" method in the "HelloWorldTest" class in the lambda-example to get started.

+

In unit test, we want to start the main application so that all the functions are ready for tests.

+

First, we write a "TestBase" class to use the BeforeClass setup method to start the main application like this:

+
public class TestBase {
+
+    private static final AtomicInteger seq = new AtomicInteger(0);
+
+    @BeforeClass
+    public static void setup() {
+        if (seq.incrementAndGet() == 1) {
+            AutoStart.main(new String[0]);
+        }
+    }
+}
+
+

The atomic integer "seq" is used to ensure the main application entry point is executed only once.

+

Your first unit test may look like this:

+
@SuppressWarnings("unchecked")
+@Test
+public void rpcTest() throws IOException, InterruptedException {
+    Utility util = Utility.getInstance();
+    BlockingQueue<EventEnvelope> bench = new ArrayBlockingQueue<>(1);
+    String name = "hello";
+    String address = "world";
+    String telephone = "123-456-7890";
+    DemoPoJo pojo = new DemoPoJo(name, address, telephone);
+    PostOffice po = new PostOffice("unit.test", "12345", "POST /api/hello/world");
+    EventEnvelope request = new EventEnvelope().setTo("hello.world")
+                                .setHeader("a", "b").setBody(pojo.toMap());
+    po.asyncRequest(request, 800).onSuccess(bench::offer);
+    EventEnvelope response = bench.poll(10, TimeUnit.SECONDS);
+    assert response != null;
+    Assert.assertEquals(HashMap.class, response.getBody().getClass());
+    MultiLevelMap map = new MultiLevelMap((Map<String, Object>) response.getBody());
+    Assert.assertEquals("b", map.getElement("headers.a"));
+    Assert.assertEquals(name, map.getElement("body.name"));
+    Assert.assertEquals(address, map.getElement("body.address"));
+    Assert.assertEquals(telephone, map.getElement("body.telephone"));
+    Assert.assertEquals(util.date2str(pojo.time), map.getElement("body.time"));
+}
+
+

Note that the PostOffice instance can be created with tracing information in a Unit Test. The above example +tells the system that the sender is "unit.test", the trace ID is 12345 and the trace path is "POST /api/hello/world".

+

For unit test, we need to convert the asynchronous code into "synchronous" execution so that unit test can run +sequentially. "BlockingQueue" is a good choice for this.

+

The "hello.world" is an echo function. The above unit test sends an event containing a key-value {"a":"b"} and +the payload of a HashMap from the DemoPoJo.

+

If the function is designed to handle PoJo, we can send PoJo directly instead of a Map.

+
+

IMPORTANT: blocking code should only be used for unit tests. DO NOT use blocking code in your + application code because it will block the event system and dramatically slow down + your application.

+
+

Convenient utility classes

+

The Utility and MultiLevelMap classes are convenient tools for unit tests. In the above example, we use the +Utility class to convert a date object into a UTC timestamp. It is because date object is serialized as a UTC +timestamp in an event.

+

The MultiLevelMap supports reading an element using the convenient "dot and bracket" format.

+

For example, given a map like this:

+
{
+  "body":
+  {
+    "time": "2023-03-27T18:10:34.234Z",
+    "hello": [1, 2, 3]
+  }
+}
+
+ + + + + + + + + + + + + + + + + + + + +
ExampleCommandResult
1map.getElement("body.time")2023-03-27T18:10:34.234Z
2map.getElement("body.hello[2]")3
+

The second unit test

+

Let's do a unit test for PoJo. In this second unit test, it sends a RPC request to the "hello.pojo" function that +is designed to return a SamplePoJo object with some mock data.

+

Please refer to "pojoRpcTest" method in the "PoJoTest" class in the lambda-example for details.

+

The unit test verifies that the "hello.pojo" has correctly returned the SamplePoJo object with the pre-defined +mock value.

+
@Test
+public void pojoTest() throws IOException, InterruptedException {
+    Integer ID = 1;
+    String NAME = "Simple PoJo class";
+    String ADDRESS = "100 World Blvd, Planet Earth";
+    BlockingQueue<EventEnvelope> bench = new ArrayBlockingQueue<>(1);
+    PostOffice po = new PostOffice("unit.test", "20001", "GET /api/hello/pojo");
+    EventEnvelope request = new EventEnvelope().setTo("hello.pojo").setHeader("id", "1");
+    po.asyncRequest(request, 800).onSuccess(bench::offer);
+    EventEnvelope response = bench.poll(10, TimeUnit.SECONDS);
+    assert response != null;
+    Assert.assertEquals(SamplePoJo.class, response.getBody().getClass());
+    SamplePoJo pojo = response.getBody(SamplePoJo.class);
+    Assert.assertEquals(ID, pojo.getId());
+    Assert.assertEquals(NAME, pojo.getName());
+    Assert.assertEquals(ADDRESS, pojo.getAddress());
+}
+
+

Note that you can do class "casting" or use the built-in casting API as shown below:

+
+

SamplePoJo pojo = (SamplePoJo) response.getBody()

+

SamplePoJo pojo = response.getBody(SamplePoJo.class)

+
+

The third unit test

+

Testing Kotlin suspend functions is challenging. However, testing suspend function using events is straight forward +because of loose coupling.

+

Let's do a unit test for the lambda-example's FileUploadDemo function. Its route name is "hello.upload".

+

Please refer to "uploadTest" method in the "SuspendFunctionTest" class in the lambda-example for details.

+
@SuppressWarnings("unchecked")
+@Test
+public void uploadTest() throws IOException, InterruptedException {
+    String FILENAME = "unit-test-data.txt";
+    BlockingQueue<EventEnvelope> bench = new ArrayBlockingQueue<>(1);
+    Utility util = Utility.getInstance();
+    PostOffice po = PostOffice.getInstance();
+    int len = 0;
+    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+    ObjectStreamIO stream = new ObjectStreamIO();
+    ObjectStreamWriter out = new ObjectStreamWriter(stream.getOutputStreamId());
+    for (int i=0; i < 10; i++) {
+        String line = "hello world "+i+"\n";
+        byte[] d = util.getUTF(line);
+        out.write(d);
+        bytes.write(d);
+        len += d.length;
+    }
+    out.close();
+    // emulate a multi-part file upload
+    AsyncHttpRequest req = new AsyncHttpRequest();
+    req.setMethod("POST");
+    req.setUrl("/api/upload/demo");
+    req.setTargetHost("http://127.0.0.1:8080");
+    req.setHeader("accept", "application/json");
+    req.setHeader("content-type", "multipart/form-data");
+    req.setContentLength(len);
+    req.setFileName(FILENAME);
+    req.setStreamRoute(stream.getInputStreamId());
+    // send the HTTP request event to the "hello.upload" function
+    EventEnvelope request = new EventEnvelope().setTo("hello.upload").setBody(req);
+    po.asyncRequest(request, 8000).onSuccess(bench::offer);
+    EventEnvelope response = bench.poll(10, TimeUnit.SECONDS);
+    assert response != null;
+    Assert.assertEquals(HashMap.class, response.getBody().getClass());
+    Map<String, Object> map = (Map<String, Object>) response.getBody();
+    System.out.println(response.getBody());
+    Assert.assertEquals(len, map.get("expected_size"));
+    Assert.assertEquals(len, map.get("actual_size"));
+    Assert.assertEquals(FILENAME, map.get("filename"));
+    Assert.assertEquals("Upload completed", map.get("message"));
+    // finally check that "hello.upload" has saved the test file
+    File dir = new File("/tmp/upload-download-demo");
+    File file = new File(dir, FILENAME);
+    Assert.assertTrue(file.exists());
+    Assert.assertEquals(len, file.length());
+    // compare file content
+    byte[] b = Utility.getInstance().file2bytes(file);
+    Assert.assertArrayEquals(bytes.toByteArray(), b);
+}
+
+

In the above unit test, we use the ObjectStreamIO to emulate a file stream and write 10 blocks of data into it. +The unit test then makes an RPC call to the "hello.upload" with the emulated HTTP request event.

+

The "hello.upload" is a Kotlin suspend function. It will be executed when the event arrives. +After saving the test file, it will return an HTTP response object that the unit test can validate.

+

In this fashion, you can create unit tests to test suspend functions in an event-driven manner.

+

Deployment

+

The pom.xml is pre-configured to generate an executable JAR. The following is extracted from the pom.xml.

+

The main class is AutoStart that will load the "main application" and use it as the entry point to +run the application.

+
<plugin>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-maven-plugin</artifactId>
+    <configuration>
+        <mainClass>org.platformlambda.core.system.AutoStart</mainClass>
+    </configuration>
+    <executions>
+        <execution>
+            <id>build-info</id>
+            <goals>
+                <goal>build-info</goal>
+            </goals>
+        </execution>
+    </executions>
+</plugin>
+
+

Composable application is designed to be deployable using Kubernetes or serverless.

+

A sample Dockerfile for an executable JAR may look like this:

+
FROM mcr.microsoft.com/openjdk/jdk:11-ubuntu
+EXPOSE 8083
+WORKDIR /app
+COPY target/your-app-name.jar .
+ENTRYPOINT ["java","-jar","your-app-name.jar"]
+
+

Distributed tracing

+

The system has a built-in distributed tracing feature. You can enable tracing for any REST endpoint by adding +"tracing=true" in the endpoint definition in the "rest.yaml" configuration file.

+

You may also upload performance metrics from the distributed tracing data to your favorite telemetry system dashboard.

+

To do that, please implement a custom metrics function with the route name distributed.trace.forwarder.

+

The input to the function will be a HashMap like this:

+
trace={path=/api/upload/demo, service=hello.upload, success=true, 
+       origin=2023032731e2a5eeae8f4da09f3d9ac6b55fb0a4, 
+       exec_time=77.462, start=2023-03-27T19:38:30.061Z, 
+       from=http.request, id=12345, round_trip=132.296, status=200}
+
+

The system will detect if distributed.trace.forwarder is available. If yes, it will forward performance metrics +from distributed trace to your custom function.

+

Request-response journaling

+

Optionally, you may also implement a custom audit function named transaction.journal.recorder to monitor +request-response payloads.

+

To enable journaling, please add this to the application.properties file.

+
journal.yaml=classpath:/journal.yaml
+
+

and add the "journal.yaml" configuration file to the project's resources folder with content like this:

+
journal:
+  - "my.test.function"
+  - "another.function"
+
+

In the above example, the "my.test.function" and "another.function" will be monitored and their request-response +payloads will be forwarded to your custom audit function. The input to your audit function will be a HashMap +containing the performance metrics data and a "journal" section with the request and response payloads in clear form.

+
+

IMPORTANT: journaling may contain sensitive personally identifiable data and secrets. Please check + security compliance before storing them into access restricted audit data store.

+
+


+ + + + + + + + + + + + + + + +
Chapter-4HomeChapter-6
Event orchestrationTable of ContentsSpring Boot
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/CHAPTER-6/index.html b/docs/guides/CHAPTER-6/index.html new file mode 100644 index 000000000..e58c7ce1b --- /dev/null +++ b/docs/guides/CHAPTER-6/index.html @@ -0,0 +1,403 @@ + + + + + + + + Chapter-6 - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Spring Boot Integration

+

While the platform-core foundation code includes a lightweight non-blocking HTTP server, you can also turn your +application into an executable Spring Boot application.

+

There are two ways to do that:

+
    +
  1. Add dependency for Spring Boot version 2.7.10 (or version 3.0.5) and implement your Spring Boot main application
  2. +
  3. Add the rest-spring-2 or rest-spring-3 add-on library for a pre-configured Spring Boot experience
  4. +
+

Add platform-core to an existing Spring Boot application

+

For option 1, the platform-core library can co-exist with Spring Boot. You can write code specific to Spring Boot +and the Spring framework ecosystem. Please make sure you add the following startup code to your Spring Boot +main application like this:

+
@SpringBootApplication
+public class MyMainApp extends SpringBootServletInitializer {
+
+    public static void main(String[] args) {
+        AutoStart.main(args);
+        SpringApplication.run(MyMainApp.class, args);
+    }
+
+}
+
+

We suggest running AutoStart.main before the SpringApplication.run statement. This would allow the platform-core +foundation code to load the event-listener functions into memory before Spring Boot starts.

+

Use the rest-spring library in your application

+

You can add the rest-spring-2 or rest-spring-3 library in your application and turn it into a pre-configured +Spring Boot 2 or 3 application.

+

The "rest-spring" library configures Spring Boot's serializers (XML and JSON) to behave consistently as the +built-in lightweight non-blocking HTTP server.

+

If you want to disable the lightweight HTTP server, you can set rest.automation=false in application.properties. +The REST automation engine and the lightweight HTTP server will be turned off.

+
+

IMPORTANT: the platform-core library assumes the application configuration files to be either + application.yml or application.properties. If you use custom Spring profile, please keep the + application.yml or application.properties for the platform-core. If you use default Spring + profile, both platform-core and Spring Boot will use the same configuration files.

+
+

You can customize your error page using the default errorPage.html by copying it from the platform-core's or +rest-spring's resources folder to your source project. The default page is shown below.

+

This is the HTML error page that the platform-core or rest-spring library uses. You can update it with +your corporate style guide. Please keep the parameters (status, message, path, warning) intact.

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <title>HTTP Error</title>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+</head>
+<body>
+
+<div>
+    <h3>HTTP-${status}</h3>
+    <div>${warning}</div><br/>
+    <table>
+        <tbody>
+        <tr><td style="font-style: italic; width: 100px">Type</td><td>error</td></tr>
+        <tr><td style="font-style: italic; width: 100px">Status</td><td>${status}</td></tr>
+        <tr><td style="font-style: italic; width: 100px">Message</td><td>${message}</td></tr>
+        <tr><td style="font-style: italic; width: 100px">Path</td><td>${path}</td></tr>
+        </tbody>
+    </table>
+
+</div>
+</body>
+</html>
+
+

If you want to keep REST automation's lightweight HTTP server together with Spring Boot's Tomcat or other +application server, please add the following to your application.properties file:

+
server.port=8083
+rest.server.port=8085
+rest.automation=true
+
+

The platform-core and Spring Boot will use rest.server.port and server.port respectively.

+

The rest-spring-2-example demo application

+

Let's review the rest-spring-2-example demo application in the "examples/rest-spring-2-example" project.

+

You can use the rest-spring-2-example as a template to create a Spring Boot application.

+

In addition to the REST automation engine that let you create REST endpoints by configuration, you can also +programmatically create REST endpoints with the following approaches:

+
    +
  1. JAX-RS REST endpoints
  2. +
  3. Spring RestControllers
  4. +
  5. Servlet 3.1 WebServlets
  6. +
+

We will examine asynchronous REST endpoint with the AsyncHelloWorld class.

+

Since the platform-core is event-driven, we would like to use JAX-RS asynchronous HTTP context AsyncResponse +in the REST endpoints so that the endpoint does not block.

+
@Path("/hello")
+public class AsyncHelloWorld {
+
+    private static final AtomicInteger seq = new AtomicInteger(0);
+
+    @GET
+    @Path("/world")
+    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
+    public void hello(@Context HttpServletRequest request,
+                      @Suspended AsyncResponse response) {
+
+        String traceId = Utility.getInstance().getUuid();
+        PostOffice po = new PostOffice("hello.world.endpoint", traceId, "GET /api/hello/world");
+        Map<String, Object> forward = new HashMap<>();
+
+        Enumeration<String> headers = request.getHeaderNames();
+        while (headers.hasMoreElements()) {
+            String key = headers.nextElement();
+            forward.put(key, request.getHeader(key));
+        }
+        // As a demo, just put the incoming HTTP headers as a payload and add a counter
+        // The echo service will return both.
+        int n = seq.incrementAndGet();
+        EventEnvelope req = new EventEnvelope();
+        req.setTo("hello.world").setBody(forward).setHeader("seq", n);
+        Future<EventEnvelope> res = po.asyncRequest(req, 3000);
+        res.onSuccess(event -> {
+            Map<String, Object> result = new HashMap<>();
+            result.put("status", event.getStatus());
+            result.put("headers", event.getHeaders());
+            result.put("body", event.getBody());
+            result.put("execution_time", event.getExecutionTime());
+            result.put("round_trip", event.getRoundTrip());
+            response.resume(result);
+        });
+        res.onFailure(ex -> response.resume(new AppException(408, ex.getMessage())));
+    }
+}
+
+

In this hello world REST endpoint, JAX-RS runs the "hello" method asynchronously without waiting for a response.

+

The example code copies the HTTP requests and sends it as the request payload to the "hello.world" function. +The function is defined in the MainApp like this:

+
Platform platform = Platform.getInstance();
+LambdaFunction echo = (headers, input, instance) -> {
+    Map<String, Object> result = new HashMap<>();
+    result.put("headers", headers);
+    result.put("body", input);
+    result.put("instance", instance);
+    result.put("origin", platform.getOrigin());
+    return result;
+};
+platform.register("hello.world", echo, 20);
+
+

When "hello.world" responds, its result set will be returned to the onSuccess method as a "future response".

+

The "onSuccess" method then sends the response to the browser using the JAX-RS resume mechanism.

+

The AsyncHelloConcurrent is the same as the AsyncHelloWorld except that it performs a "fork-n-join" operation +to multiple instances of the "hello.world" function.

+

Unlike "rest.yaml" that defines tracing by configuration, you can turn on tracing programmatically in a JAX-RS +endpoint. To enable tracing, the function sets the trace ID and path in the PostOffice constructor.

+

When you try the endpoint at http://127.0.0.1:8083/api/hello/world, it will echo your HTTP request headers. +In the command terminal, you will see tracing information in the console log like this:

+
DistributedTrace:67 - trace={path=GET /api/hello/world, service=hello.world, success=true, 
+  origin=20230403364f70ebeb54477f91986289dfcd7b75, exec_time=0.249, start=2023-04-03T04:42:43.445Z, 
+  from=hello.world.endpoint, id=e12e871096ba4938b871ee72ef09aa0a, round_trip=20.018, status=200}
+
+

Lightweight non-blocking websocket server

+

If you want to turn on a non-blocking websocket server, you can add the following configuration to +application.properties.

+
server.port=8083
+websocket.server.port=8085
+
+

The above assumes Spring Boot runs on port 8083 and the websocket server runs on port 8085.

+
+

Note that "websocket.server.port" is an alias of "rest.server.port"

+
+

You can create a websocket service with a Java class like this:

+
@WebSocketService("hello")
+public class WsEchoDemo implements LambdaFunction {
+
+    @Override
+    public Object handleEvent(Map<String, String> headers, Object body, int instance) {
+        // handle the incoming websocket events (type = open, close, bytes or string)
+    }
+}
+
+

The above creates a websocket service at the URL "/ws/hello" server endpoint.

+

Please review the example code in the WsEchoDemo class in the rest-spring-2-example project for details.

+

If you want to use Spring Boot's Tomcat websocket server, you can disable the non-blocking websocket server feature +by removing the websocket.server.port configuration and any websocket service classes with the WebSocketService +annotation.

+

To try out the demo websocket server, visit http://127.0.0.1:8083 and select "Websocket demo".

+

Spring Boot version 3

+

The rest-spring-3 subproject is a pre-configured Spring Boot 3 library.

+

In "rest-spring-3", Spring WebFlux replaces JAX-RS as the asynchronous HTTP servlet engine.

+


+ + + + + + + + + + + + + + + +
Chapter-5HomeChapter-7
Build, Test and DeployTable of ContentsEvent over HTTP
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/CHAPTER-7/index.html b/docs/guides/CHAPTER-7/index.html new file mode 100644 index 000000000..b9d875892 --- /dev/null +++ b/docs/guides/CHAPTER-7/index.html @@ -0,0 +1,373 @@ + + + + + + + + Chapter-7 - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Event over HTTP

+

The in-memory event system allows functions to communicate with each other in the same application memory space.

+

In composable architecture, applications are modular components in a network. Some transactions may require +the services of more than one application. "Event over HTTP" extends the event system beyond a single application.

+

The Event API service (event.api.service) is a built-in function in the system.

+

The Event API endpoint

+

To enable "Event over HTTP", you must first turn on the REST automation engine with the following parameters +in the application.properties file:

+
rest.server.port=8085
+rest.automation=true
+
+

and then check if the following entry is configured in the "rest.yaml" endpoint definition file. +If not, update "rest.yaml" accordingly. The "timeout" value is set to 60 seconds to fit common use cases.

+
  - service: [ "event.api.service" ]
+    methods: [ 'POST' ]
+    url: "/api/event"
+    timeout: 60s
+    tracing: true
+
+

This will expose the Event API endpoint at port 8085 and URL "/api/event".

+

In kubernetes, The Event API endpoint of each application is reachable through internal DNS and there is no need +to create "ingress" for this purpose.

+

Test drive Event API

+

You may now test drive the Event API service.

+

First, build and run the lambda-example application in port 8085.

+
cd examples/lambda-example
+java -jar target/lambda-example-3.0.9.jar
+
+

Second, build and run the rest-spring-example application.

+
cd examples/rest-spring-example-2
+java -jar target/rest-spring-2-example-3.0.9.jar
+
+

The rest-spring-2-example application will run as a Spring Boot application in port 8083 and 8086.

+

These two applications will start independently.

+

You may point your browser to http://127.0.0.1:8083/api/pojo/http/1 to invoke the HelloPojoEventOverHttp +endpoint service that will in turn makes an Event API call to the lambda-example's "hello.pojo" service.

+

You will see the following response in the browser. This means the rest-spring-example application has successfully +made an event API call to the lambda-example application using the Event API endpoint.

+
{
+  "id": 1,
+  "name": "Simple PoJo class",
+  "address": "100 World Blvd, Planet Earth",
+  "date": "2023-03-27T23:17:19.257Z",
+  "instance": 6,
+  "seq": 66,
+  "origin": "2023032791b6938a47614cf48779b1cf02fc89c4"
+}
+
+

To examine how the application makes the Event API call, please refer to the HelloPojoEventOverHttp class +in the rest-spring-example. The class is extracted below:

+
@Path("/pojo")
+public class HelloPoJoEventOverHttp {
+
+    @GET
+    @Path("/http/{id}")
+    @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
+    public void getPoJo(@PathParam("id") Integer id, @Suspended AsyncResponse response) {
+        AppConfigReader config = AppConfigReader.getInstance();
+        String remotePort = config.getProperty("lambda.example.port", "8085");
+        String remoteEndpoint = "http://127.0.0.1:"+remotePort+"/api/event";
+        String traceId = Utility.getInstance().getUuid();
+        PostOffice po = new PostOffice("hello.pojo.endpoint", traceId, "GET /api/pojo/http");
+        EventEnvelope req = new EventEnvelope().setTo("hello.pojo").setHeader("id", id);
+        Future<EventEnvelope> res = po.asyncRequest(req, 5000, Collections.emptyMap(), remoteEndpoint, true);
+        res.onSuccess(event -> {
+            // confirm that the PoJo object is transported correctly over the event stream system
+            if (event.getBody() instanceof SamplePoJo) {
+                response.resume(event.getBody());
+            } else {
+                response.resume(new AppException(event.getStatus(), event.getError()));
+            }
+        });
+        res.onFailure(response::resume);
+    }
+}
+
+

The method signatures of the Event API is shown as follows:

+

Asynchronous API (Java)

+
public Future<EventEnvelope> asyncRequest(final EventEnvelope event, long timeout,
+                                          Map<String, String> headers,
+                                          String eventEndpoint, boolean rpc) throws IOException;
+
+

Sequential non-blocking API (Kotlin suspend function)

+
suspend fun awaitRequest(request: EventEnvelope?, timeout: Long, 
+                          headers: Map<String, String>,
+                          eventEndpoint: String, rpc: Boolean): EventEnvelope
+
+

Optionally, you may add security headers in the "headers" argument. e.g. the "Authorization" header.

+

The eventEndpoint is a fully qualified URL. e.g. http://peer/api/event

+

The "rpc" boolean value is set to true so that the response from the service of the peer application instance +will be delivered. For drop-n-forget use case, you can set the "rpc" value to false. It will immediately return +an HTTP-202 response.

+

Event-over-HTTP using configuration

+

While you can call the "Event-over-HTTP" APIs programmatically, it would be more convenient to automate it with a +configuration. This service abstraction means that user applications do not need to know where the target services are.

+

You can enable Event-over-HTTP configuration by adding this parameter in application.properties:

+
#
+# Optional event-over-http target maps
+#
+yaml.event.over.http=classpath:/event-over-http.yaml
+
+

and then create the configuration file "event-over-http.yaml" like this:

+
event:
+  http:
+  - route: 'event.http.test'
+    target: 'http://127.0.0.1:${server.port}/api/event'
+    # optional security headers
+    headers:
+      authorization: 'demo'
+  - route: 'event.save.get'
+    target: 'http://127.0.0.1:${server.port}/api/event'
+    headers:
+      authorization: 'demo'
+
+

In the above example, there are two routes (event.http.test and event.save.get) with target URLs. If additional +authentication is required for the peer's "/api/event" endpoint, you may add a set of security headers in each +route.

+

When you send asynchronous event or make a RPC call to "event.save.get" service, it will be forwarded to the +peer's "event-over-HTTP" endpoint (/api/event) accordingly.

+

You may also add variable references to the application.properties (or application.yaml) file, such as +"server.port" in this example.

+
+

Note: The configuration based "event-over-HTTP" feature does not support fork-n-join request API.

+
+

Advantages

+

The Event API exposes all public functions of an application instance to the network using a single REST endpoint.

+

The advantages of Event API includes:

+
    +
  1. Convenient - you do not need to write or configure individual endpoint for each public service
  2. +
  3. Efficient - events are transported in binary format from one application to another
  4. +
  5. Secure - you can protect the Event API endpoint with an authentication service
  6. +
+

The following configuration adds authentication service to the Event API endpoint:

+
  - service: [ "event.api.service" ]
+    methods: [ 'POST' ]
+    url: "/api/event"
+    timeout: 60s
+    authentication: "v1.api.auth"
+    tracing: true
+
+

This enforces every incoming request to the Event API endpoint to be authenticated by the "v1.api.auth" service +before passing to the Event API service. You can plug in your own authentication service such as OAuth 2.0 +"bearer token" validation.

+

Please refer to Chapter-3 - REST automation for details. +

+ + + + + + + + + + + + + + + +
Chapter-6HomeChapter-8
Spring BootTable of ContentsService Mesh
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/CHAPTER-8/index.html b/docs/guides/CHAPTER-8/index.html new file mode 100644 index 000000000..6295baa9c --- /dev/null +++ b/docs/guides/CHAPTER-8/index.html @@ -0,0 +1,498 @@ + + + + + + + + Chapter-8 - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Service Mesh

+

Service mesh is a dedicated infrastructure layer to facilitate inter-container communication using "sidecar" and +"control plane".

+

Service mesh systems require additional administrative containers (PODs) for "control plane" and "service discovery."

+

The additional infrastructure requirements vary among products.

+

Using kafka as a service mesh

+

We will discuss using Kafka as a minimalist service mesh.

+
+

Note: Service mesh is optional. You can use "event over HTTP" for inter-container + communication if service mesh is not suitable.

+
+

Typically, a service mesh system uses a "side-car" to sit next to the application container in the same POD to provide +service discovery and network proxy services.

+

Kafka is a network event stream system. We have implemented libraries for a few "cloud connectors" to support +Kafka and Hazelcast as examples.

+

Instead of using a side-car proxy, the system maintains a distributed routing table in each application instance. +When a function requests the service of another function which is not in the same memory space, the "cloud.connector" +module will bridge the event to the peer application through a network event system like Kafka.

+

As shown in the following table, if "service.1" and "service.2" are in the same memory space of an application, +they will communicate using the in-memory event bus.

+

If they are in different applications and the applications are configured with Kafka, the two functions will +communicate via the "cloud.connector" service.

+ + + + + + + + + + + + + +
In-memory event busNetwork event stream
"service.1" -> "service.2""service.1" -> "cloud.connector" -> "service.2"
+

The system supports Kafka, Hazelcast out of the box. For example, to select kafka, +you can configure application.properties like this:

+
cloud.connector=kafka
+
+

The "cloud.connector" parameter can be set to "none", "kafka" or "hazelcast". +The default parameter of "cloud.connector" is "none". This means the application is not using +any network event system "connector", thus running independently.

+

Let's set up a minimalist service mesh with Kafka to see how it works.

+

Set up a standalone Kafka server for development

+

You need a Kafka cluster as the network event stream system. For development and testing, you can build +and run a standalone Kafka server like this. Note that the mvn clean package command is optional because +the executable JAR should be available after the mvn clean install command in Chapter-1.

+
cd connectors/adapters/kafka/kafka-standalone
+mvn clean package
+java -jar target/kafka-standalone-3.0.9.jar
+
+

The standalone Kafka server will start at port 9092. You may adjust the "server.properties" in the standalone-kafka +project when necessary.

+

When the kafka server is started, it will create two temporary directories in the "/tmp" folder:

+
    +
  1. "/tmp/zookeeper"
  2. +
  3. "/tmp/kafka-logs"
  4. +
+
+

The kafka server is designed for development purpose only. The kafka and zookeeper data stores + will be cleared when the server is restarted.

+
+

Prepare the kafka-presence application

+

The "kafka-presence" is a "presence monitor" application. It is a minimalist "control plane" in service mesh +terminology.

+

What is a presence monitor? A presence monitor is the control plane that assigns unique "topic" for each +user application instance.

+

It monitors the "presence" of each application. If an application fails or stops, the presence monitor will +advertise the event to the rest of the system so that each application container will update its corresponding +distributed routing table, thus bypassing the failed application and its services.

+

If an application has more than one container instance deployed, they will work together to share load evenly.

+

You will start the presence monitor like this:

+
cd connectors/adapters/kafka/kafka-presence
+java -jar target/kafka-presence-3.0.9.jar
+
+

By default, the kafka-connector will run at port 8080. Partial start-up log is shown below:

+
AppStarter:344 - Modules loaded in 2,370 ms
+AppStarter:334 - Websocket server running on port-8080
+ServiceLifeCycle:73 - service.monitor, partition 0 ready
+HouseKeeper:72 - Registered monitor (me) 2023032896b12f9de149459f9c8b71ad8b6b49fa
+
+

The presence monitor will use the topic "service.monitor" to connect to the Kafka server and register itself +as a presence monitor.

+

Presence monitor is resilient. You can run more than one instance to back up each other. +If you are not using Docker or Kubernetes, you need to change the "server.port" parameter of the second instance +to 8081 so that the two application instances can run in the same laptop.

+

Launch the rest-spring-2-example and lambda-example with kafka

+

Let's run the rest-spring-2-example (rest-spring-3-example) and lambda-example applications with +Kafka connector turned on.

+

For demo purpose, the rest-spring-2-example and lambda-example are pre-configured with "kafka-connector". +If you do not need these libraries, please remove them from the pom.xml built script.

+

Since kafka-connector is pre-configured, we can start the two demo applications like this:

+
cd examples/rest-spring-2-example
+java -Dcloud.connector=kafka -Dmandatory.health.dependencies=cloud.connector.health 
+     -jar target/rest-spring-2-example-3.0.9.jar
+
+
cd examples/lambda-example
+java -Dcloud.connector=kafka -Dmandatory.health.dependencies=cloud.connector.health 
+     -jar target/lambda-example-3.0.9.jar
+
+

The above command uses the "-D" parameters to configure the "cloud.connector" and "mandatory.health.dependencies".

+

The parameter mandatory.health.dependencies=cloud.connector.health tells the system to turn on the health check +endpoint for the application.

+

For the rest-spring-2-example, the start-up log may look like this:

+
AppStarter:344 - Modules loaded in 2,825 ms
+PresenceConnector:155 - Connected pc.abb4a4de.in, 127.0.0.1:8080, 
+                        /ws/presence/202303282583899cf43a49b98f0522492b9ca178
+EventConsumer:160 - Subscribed multiplex.0001.0
+ServiceLifeCycle:73 - multiplex.0001, partition 0 ready
+
+

This means that the rest-spring-2-example has successfully connected to the presence monitor at port 8080. +It has subscribed to the topic "multiplex.0001" partition 0.

+

For the lambda-example, the log may look like this:

+
AppStarter:344 - Modules loaded in 2,742 m
+PresenceConnector:155 - Connected pc.991a2be0.in, 127.0.0.1:8080, 
+                        /ws/presence/2023032808d82ebe2c0d4e5aa9ca96b3813bdd25
+EventConsumer:160 - Subscribed multiplex.0001.1
+ServiceLifeCycle:73 - multiplex.0001, partition 1 ready
+ServiceRegistry:242 - Peer 202303282583899cf43a49b98f0522492b9ca178 joins (rest-spring-2-example 3.0.0)
+ServiceRegistry:383 - hello.world (rest-spring-2-example, WEB.202303282583899cf43a49b98f0522492b9ca178) registered
+
+

You notice that the lambda-example has discovered the rest-spring-2-example through Kafka and added the +"hello.world" to the distributed routing table.

+

At this point, the rest-spring-2-example will find the lambda-example application as well:

+
ServiceRegistry:242 - Peer 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 joins (lambda-example 3.0.0)
+ServiceRegistry:383 - hello.world (lambda-example, 
+                                   APP.2023032808d82ebe2c0d4e5aa9ca96b3813bdd25) registered
+ServiceRegistry:383 - hello.pojo (lambda-example, 
+                                   APP.2023032808d82ebe2c0d4e5aa9ca96b3813bdd25) registered
+
+

This is real-time service discovery coordinated by the "kafka-presence" monitor application.

+

Now you have created a minimalist event-driven service mesh.

+

Send an event request from rest-spring-2-example to lambda-example

+

In Chapter-7, you have sent a request from the rest-spring-2-example to the lambda-example using +"Event over HTTP" without a service mesh.

+

In this section, you can make the same request using service mesh.

+

Please point your browser to http://127.0.0.1:8083/api/pojo/mesh/1 +You will see the following response in your browser.

+
{
+  "id": 1,
+  "name": "Simple PoJo class",
+  "address": "100 World Blvd, Planet Earth",
+  "date": "2023-03-28T17:53:41.696Z",
+  "instance": 1,
+  "seq": 1,
+  "origin": "2023032808d82ebe2c0d4e5aa9ca96b3813bdd25"
+}
+
+

Presence monitor info endpoint

+

You can check the service mesh status from the presence monitor's "/info" endpoint.

+

You can visit http://127.0.0.1:8080/info and it will show something like this:

+
{
+  "app": {
+    "name": "kafka-presence",
+    "description": "Presence Monitor",
+    "version": "3.0.0"
+  },
+  "personality": "RESOURCES",
+  "additional_info": {
+    "total": {
+      "topics": 2,
+      "virtual_topics": 2,
+      "connections": 2
+    },
+    "topics": [
+      "multiplex.0001 (32)",
+      "service.monitor (11)"
+    ],
+    "virtual_topics": [
+      "multiplex.0001-000 -> 202303282583899cf43a49b98f0522492b9ca178, rest-spring-2-example v3.0.0",
+      "multiplex.0001-001 -> 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25, lambda-example v3.0.0"
+    ],
+    "connections": [
+      {
+        "elapsed": "25 minutes 12 seconds",
+        "created": "2023-03-28T17:43:13Z",
+        "origin": "2023032808d82ebe2c0d4e5aa9ca96b3813bdd25",
+        "name": "lambda-example",
+        "topic": "multiplex.0001-001",
+        "monitor": "2023032896b12f9de149459f9c8b71ad8b6b49fa",
+        "type": "APP",
+        "updated": "2023-03-28T18:08:25Z",
+        "version": "3.0.0",
+        "seq": 65,
+        "group": 1
+      },
+      {
+        "elapsed": "29 minutes 42 seconds",
+        "created": "2023-03-28T17:38:47Z",
+        "origin": "202303282583899cf43a49b98f0522492b9ca178",
+        "name": "rest-spring-2-example",
+        "topic": "multiplex.0001-000",
+        "monitor": "2023032896b12f9de149459f9c8b71ad8b6b49fa",
+        "type": "WEB",
+        "updated": "2023-03-28T18:08:29Z",
+        "version": "3.0.0",
+        "seq": 75,
+        "group": 1
+      }
+    ],
+    "monitors": [
+      "2023032896b12f9de149459f9c8b71ad8b6b49fa - 2023-03-28T18:08:46Z"
+    ]
+  },
+  "vm": {
+    "java_vm_version": "18.0.2.1+1",
+    "java_runtime_version": "18.0.2.1+1",
+    "java_version": "18.0.2.1"
+  },
+  "origin": "2023032896b12f9de149459f9c8b71ad8b6b49fa",
+  "time": {
+    "current": "2023-03-28T18:08:47.613Z",
+    "start": "2023-03-28T17:31:23.611Z"
+  }
+}
+
+

In this example, it shows that there are two user applications (rest-spring-2-example and lambda-example) connected.

+

Presence monitor health endpoint

+

The presence monitor has a "/health" endpoint.

+

You can visit http://127.0.0.1:8080/health and it will show something like this:

+
{
+  "upstream": [
+    {
+      "route": "cloud.connector.health",
+      "status_code": 200,
+      "service": "kafka",
+      "topics": "on-demand",
+      "href": "127.0.0.1:9092",
+      "message": "Loopback test took 3 ms; System contains 2 topics",
+      "required": true
+    }
+  ],
+  "origin": "2023032896b12f9de149459f9c8b71ad8b6b49fa",
+  "name": "kafka-presence",
+  "status": "UP"
+}
+
+

User application health endpoint

+

Similarly, you can check the health status of the rest-spring-2-example application with http://127.0.0.1:8083/health

+
{
+  "upstream": [
+    {
+      "route": "cloud.connector.health",
+      "status_code": 200,
+      "service": "kafka",
+      "topics": "on-demand",
+      "href": "127.0.0.1:9092",
+      "message": "Loopback test took 4 ms",
+      "required": true
+    }
+  ],
+  "origin": "202303282583899cf43a49b98f0522492b9ca178",
+  "name": "rest-spring-example",
+  "status": "UP"
+}
+
+

It looks similar to the health status of the presence monitor. However, only the presence monitor shows the total +number of topics because it handles topic issuance to each user application instance.

+

Actuator endpoints

+

Additional actuator endpoints includes:

+
    +
  1. library endpoint ("/info/lib") - you can check the packaged libraries for each application
  2. +
  3. distributed routing table ("/info/routes") - this will display the distributed routing table for public functions
  4. +
  5. environment ("/env") - it shows all functions (public and private) with number of workers.
  6. +
  7. livenessproble ("/livenessprobe") - this should display "OK" to indicate the application is running
  8. +
+

Stop an application

+

You can press "control-C" to stop an application. Let's stop the lambda-example application.

+

Once you stopped lamdba-example from the command line, the rest-spring-2-example will detect it:

+
ServiceRegistry:278 - Peer 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 left (lambda-example 3.0.0)
+ServiceRegistry:401 - hello.world 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 unregistered
+ServiceRegistry:401 - hello.pojo 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 unregistered
+
+

The rest-spring-2-example will update its distributed routing table automatically.

+

You will also find log messages in the kafka-presence application like this:

+
MonitorService:120 - Member 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 left
+TopicController:250 - multiplex.0001-001 released by 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25,
+                                                     lambda-example, 3.0.0
+
+

When an application instance stops, the presence monitor will detect the event, remove it from the registry and +release the topic associated with the disconnected application instance.

+

The presence monitor is using the "presence" feature in websocket, thus we call it "presence" monitor.

+


+ + + + + + + + + + + + + + + +
Chapter-7HomeCHAPTER-9
Event over HTTPTable of ContentsAPI Overview
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/CHAPTER-9/index.html b/docs/guides/CHAPTER-9/index.html new file mode 100644 index 000000000..f17250dac --- /dev/null +++ b/docs/guides/CHAPTER-9/index.html @@ -0,0 +1,751 @@ + + + + + + + + Chapter-9 - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

API Overview

+

Main application

+

Each application has an entry point. You may implement an entry point in a main application like this:

+
@MainApplication
+public class MainApp implements EntryPoint {
+   public static void main(String[] args) {
+      AutoStart.main(args);
+   }
+   @Override
+   public void start(String[] args) {
+        // your startup logic here
+      log.info("Started");
+   }
+}
+
+

In your main application, you will implement the EntryPoint interface to override the "start" method. +Typically, a main application is used to initiate some application start up procedure.

+

In some case when your application does not need any start up logic, you can just print a message to indicate +that your application has started.

+

You may want to keep the static "main" method which can be used to run your application inside an IDE.

+

The pom.xml build script is designed to run the AppStarter start up function that will execute your main +application's start method.

+

In some case, your application may have more than one main application module. You can decide the sequence of +execution using the "sequence" parameter in the MainApplication annotation. The module with the smallest sequence +number will run first.

+

Optional environment setup before MainApplication

+

Sometimes, it may be required to set up some environment configuration before your main application starts. +You can implement a BeforeApplication module. Its syntax is similar to the MainApplication.

+
@BeforeApplication
+public class EnvSetup implements EntryPoint {
+
+   @Override
+   public void start(String[] args) {
+        // your environment setup logic here
+      log.info("initialized");
+   }
+}
+
+

The BeforeApplication logic will run before your MainApplication module. This is useful when you want to do +special handling of environment variables. For example, decrypt an environment variable secret, construct an X.509 +certificate, and save it in the "/tmp" folder before your main application starts.

+

Event envelope

+

Mercury version 3 is an event engine that encapsulates Eclipse Vertx and Kotlin coroutine and suspend function.

+

A composable application is a collection of functions that communicate with each other in events. +Each event is transported by an event envelope. Let's examine the envelope.

+

There are 3 elements in an event envelope:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTypePurpose
1metadataIncludes unique ID, target function name, reply address
correlation ID, status, exception, trace ID and path
2headersUser defined key-value pairs
3bodyEvent payload (primitive, hash map or PoJo)
+

Headers and body are optional, but you must provide at least one of them. If the envelope do not have any headers +or body, the system will send your event as a "ping" command to the target function. The response acknowledgements +that the target function exists. This ping/pong protocol tests the event loop or service mesh. This test mechanism +is useful for DevSecOps admin dashboard.

+

Custom exception using AppException

+

To reject an incoming request, you can throw an AppException like this:

+
// example-1
+throw new AppException(400, "My custom error message");
+// example-2
+throw new AppException(400, "My custom error message", ex);
+
+

Example-1 - a simple exception with status code (400) and an error message

+

Example-2 - includes a nested exception

+

As a best practice, we recommend using error codes that are compatible with HTTP status codes.

+

Defining a user function in Java

+

You can write a function in Java like this:

+
@PreLoad(route = "hello.simple", instances = 10)
+public class SimpleDemoEndpoint implements TypedLambdaFunction<AsyncHttpRequest, Object> {
+    @Override
+    public Object handleEvent(Map<String, String> headers, AsyncHttpRequest input, int instance) {
+        // business logic here
+        return result;
+    }
+}
+
+

By default, a Java function will run as a coroutine. To tell the system that you want to run the function using +kernel thread pool, you can add the KernelThreadRunner annotation.

+

The PreLoad annotation tells the system to preload the function into memory and register it into the event loop. +You must provide a "route name" and configure the number of concurrent workers ("instances").

+

Route name is used by the event loop to find your function in memory. A route name must use lower letters and numbers, +and it must have at least one dot as a word separator. e.g. "hello.simple" is a proper route name but "HelloSimple" +is not.

+

You can implement your function using the LambdaFunction or TypedLambdaFunction. The latter allows you to define +the input and output classes.

+

The system will map the event body into the input argument and the event headers into the headers argument. +The instance argument informs your function which worker is serving the current request.

+

Similarly, you can also write a "suspend function" in Kotlin like this:

+
@PreLoad(route = "hello.world", instances = 10, isPrivate = false, 
+         envInstances = "instances.hello.world")
+class HelloWorld : KotlinLambdaFunction<Any?, Map<String, Any>> {
+
+    @Throws(Exception::class)
+    override suspend fun handleEvent(headers: Map<String, String>, input: Any?, 
+                                     instance: Int): Map<String, Any> {
+        // business logic here
+        return result;
+    }
+}
+
+

In the suspend function example above, you may notice the optional envInstances parameter. This tells the system +to use a parameter from the application.properties (or application.yml) to configure the number of workers for the +function. When the parameter defined in "envInstances" is not found, the "instances" parameter is used as the +default value.

+

Inspect event metadata

+

There are some reserved metadata for route name ("my_route"), trace ID ("my_trace_id") and trace path ("my_trace_path") +in the "headers" argument. They do not exist in the incoming event envelope. Instead, the system automatically +insert them as read-only metadata.

+

They are used when your code want to obtain an instance of PostOffice or FastRPC.

+

To inspect all metadata, you can declare the input as "EventEnvelope". The system will map the whole event envelope +into the "input" argument. You can retrieve the replyTo address and other useful metadata.

+

Note that the "replyTo" address is optional. It only exists when the caller is making an RPC call to your function. +If the caller sends an asynchronous request, the "replyTo" value is null.

+

Platform API

+

You can obtain a singleton instance of the Platform object to do the following:

+

Register a function

+

We recommend using the PreLoad annotation in a class to declare the function route name, number of worker instances +and whether the function is public or private.

+

In some use cases where you want to create and destroy functions on demand, you can register them programmatically.

+

In the following example, it registers "my.function" using the MyFunction class as a public function and +"another.function" with the AnotherFunction class as a private function. It then registers two kotlin functions +in public and private scope respectively.

+
Platform platform = Platform.getInstance();
+
+// register a public function
+platform.register("my.function", new MyFunction(), 10);
+
+// register a private function
+platform.registerPrivate("another.function", new AnotherFunction(), 20);
+
+// register a public suspend function
+platform.registerKoltin("my.suspend.function", new MySuspendFunction(), 10);
+
+// register a private suspend function
+platform.registerKoltinPrivate("another.suspend.function", new AnotherSuspendFunction(), 10);
+
+

What is a public function?

+

A public function is visible by any application instances in the same network. When a function is declared as +"public", the function is reachable through the EventAPI REST endpoint or a service mesh.

+

A private function is invisible outside the memory space of the application instance that it resides. +This allows application to encapsulate business logic according to domain boundary. You can assemble closely +related functions as a composable application that can be deployed independently.

+

Release a function

+

In some use cases, you want to release a function on-demand when it is no longer required.

+
platform.release("another.function");
+
+

The above API will unload the function from memory and release it from the "event loop".

+

Check if a function is available

+

You can check if a function with the named route has been deployed.

+
if (platform.hasRoute("another.function")) {
+    // do something
+}
+
+

Wait for a function to be ready

+

Functions are registered asynchronously. For functions registered using the PreLoad annotation, they are available +to your application when the MainApplication starts.

+

For functions that are registered on-demand, you can wait for the function to get ready like this:

+
Future<Boolean> status = platform.waitForProvider("cloud.connector", 10);
+status.onSuccess(ready -> {
+   // business logic when "cloud.connector" is ready 
+});
+
+

Note that the "onFailure" method is not required. The onSuccess will return true or false. In the above example, +your application waits for up to 10 seconds. If the function (i.e. the "provider") is available, the API will invoke +the "onSuccess" method immediately.

+

Obtain the unique application instance ID

+

When an application instance starts, a unique ID is generated. We call this the "Origin ID".

+
String originId = po.getOrigin();
+
+

When running the application in a minimalist service mesh using Kafka or similar network event stream system, +the origin ID is used to uniquely identify the application instance.

+

The origin ID is automatically appended to the "replyTo" address when making a RPC call over a network event stream +so that the system can send the response event back to the "originator" or "calling" application instance.

+

Set application personality

+

An application may have one of the following personality:

+
    +
  1. REST - the deployed application is user facing
  2. +
  3. APP - the deployed application serves business logic
  4. +
  5. RESOURCES - this is a resource-tier service. e.g. database service, MQ gateway, legacy service proxy, utility, etc.
  6. +
+

You can change the application personality like this:

+
// the default value is "APP"
+ServerPersonality.getInstance().setType(ServerPersonality.Type.REST);
+
+

The personality setting is for documentation purpose only. It does not affect the behavior of your application. +It will appear in the application "/info" endpoint.

+

PostOffice API

+

You can obtain an instance of the PostOffice from the input "headers" and "instance" parameters in the input +arguments of your function.

+
PostOffice po = new PostOffice(headers, instance);
+
+

The PostOffice is the event manager that you can use to send asynchronous events or to make RPC requests. +The constructor uses the READ only metadata in the "headers" argument in the "handleEvent" method of your function.

+

Send an asynchronous event to a function

+

You can send an asynchronous event like this.

+
// example-1
+po.send("another.function", "test message");
+
+// example-2
+po.send("another.function", new Kv("some_key", "some_value"), new kv("another_key", "another_value"));
+
+// example-3
+po.send("another.function", somePoJo, new Kv("some_key", "some_value"));
+
+// example-4
+EventEnvelope event = new EventEnvelope().setTo("another.function")
+                            .setHeader("some_key", "some_value").setBody(somePoJo);
+po.send(event)
+
+// example-5
+po.sendLater(event, new Date(System.currentTimeMillis() + 5000));
+
+
    +
  1. Example-1 sends the text string "test message" to the target service named "another.function".
  2. +
  3. Example-2 sends two key-values as "headers" parameters to the same service.
  4. +
  5. Example-3 sends a PoJo and a key-value pair to the same service.
  6. +
  7. Example-4 is the same as example-3. It is using an EventEnvelope to construct the request.
  8. +
  9. Example-5 schedules an event to be sent 5 seconds later.
  10. +
+

The first 3 APIs are convenient methods and the system will automatically create an EventEnvelope to hold the +target route name, key-values and/or event payload.

+

Make an asynchronous RPC call

+

You can make RPC call like this:

+
// example-1
+EventEnvelope request = new EventEnvelope().setTo("another.function")
+                            .setHeader("some_key", "some_value").setBody(somePoJo);
+Future<EventEnvelope> response = po.asyncRequest(request, 5000);
+response.onSuccess(result -> {
+    // result is the response event
+});
+response.onFailure(e -> {
+    // handle timeout exception
+});
+
+// example-2
+Future<EventEnvelope> response = po.asyncRequest(request, 5000, false);
+response.onSuccess(result -> {
+    // result is the response event
+    // Timeout exception is returned as a response event with status=408
+});
+
+// example-3 with the "rpc" boolean parameter set to true
+Future<EventEnvelope> response = po.asyncRequest(request, 5000, "http://peer/api/event", true);
+response.onSuccess(result -> {
+    // result is the response event
+});
+response.onFailure(e -> {
+    // handle timeout exception
+});
+
+
    +
  1. Example-1 makes a RPC call with a 5-second timeout to "another.function".
  2. +
  3. Example-2 sets the "timeoutException" to false, telling system to return timeout exception as a regular event.
  4. +
  5. Example-3 makes an "event over HTTP" RPC call to "another.function" in another application instance called "peer".
  6. +
+

"Event over HTTP" is an important topic. Please refer to Chapter 7 for more details.

+

Perform a fork-n-join RPC call to multiple functions

+

In a similar fashion, you can make a fork-n-join call that sends request events in parallel to more than one function.

+
// example-1
+EventEnvelope request1 = new EventEnvelope().setTo("this.function")
+                            .setHeader("hello", "world").setBody("test message");
+EventEnvelope request2 = new EventEnvelope().setTo("that.function")
+                            .setHeader("good", "day").setBody(somePoJo);
+List<EventEnvelope> requests = new ArrayList<>();
+requests.add(request1);
+requests.add(request2);
+Future<List<EventEnvelope>> responses = po.asyncRequest(requests, 5000);
+response.onSuccess(results -> {
+    // results contains the response events
+});
+response.onFailure(e -> {
+    // handle timeout exception
+});
+
+// example-2
+Future<List<EventEnvelope>> responses = po.asyncRequest(requests, 5000, false);
+response.onSuccess(results -> {
+    // results contains the response events.
+    // Partial result list is returned if one or more functions did not respond.
+});
+
+

Make a sequential non-blocking RPC call

+

You can make a sequential non-blocking RPC call from one function to another. The FastRPC is similar to the PostOffice. +It is the event manager for KotlinLambdaFunction. You can create an instance of the FastRPC using the "headers" +parameters in the input arguments of your function.

+
val fastRPC = new FastRPC(headers)
+val request = EventEnvelope().setTo("another.function")
+                            .setHeader("some_key", "some_value").setBody(somePoJo)
+// example-1
+val response = fastRPC.awaitRequest(request, 5000)
+// handle the response event
+
+// example-2 with the "rpc" boolean parameter set to true
+val response = fastRPC.awaitRequest(request, 5000, "http://peer/api/event", true)
+// handle the response event
+
+
    +
  1. Example-1 performs a non-blocking RPC call
  2. +
  3. Example-2 makes a non-blocking "Event Over HTTP" RPC call
  4. +
+

Note that timeout exception is returned as a regular event with status 408.

+

Sequential non-blocking code is easier to read. Moreover, it handles more concurrent users and requests +without consuming a lot of CPU resources because it is "suspended" while waiting for a response from another function.

+

Perform a sequential non-blocking fork-n-join call to multiple functions

+

You can make a sequential non-blocking fork-n-join call using the FastRPC API like this:

+
val fastRPC = FastRPC(headers)
+val template = EventEnvelope().setTo("hello.world").setHeader("someKey", "someValue")
+val requests  = ArrayList<EventEnvelope>()
+// create a list of 4 request events
+for (i in 0..3) {
+    requests.add(EventEnvelope(template.toBytes()).setBody(i).setCorrelationId("cid-$i"))
+}
+val responses: List<EventEnvelope> = fastRPC.awaitRequest(requests, 5000)
+// handle the response events
+
+

In the above example, the function creates a list of request events from a template event with target service +"hello.world". It sets the number 0 to 3 to the individual events with unique correlation IDs.

+

The response events contain the same set of correlation IDs so that your business logic can decide how to +handle individual response event.

+

The result may be a partial list of response events if one or more functions failed to respond on time.

+

Check if a function with a named route exists

+

The PostOffice provides the "exists()" method that is similar to the "platform.hasRoute()" command.

+

The difference is that the "exists()" method can discover functions of another application instance when running +in the "service mesh" mode.

+

If your application is not deployed in a service mesh, the PostOffice's "exists" and Platform's "hasRoute" APIs +will provide the same result.

+
boolean found = po.exists("another.function");
+if (found) {
+    // do something
+}
+
+

Retrieve trace ID and path

+

If you want to know the route name and optional trace ID and path, you can use the following APIs.

+

For example, if tracing is enabled, the trace ID will be available. You can put the trace ID in application log +messages. This would group log messages of the same transaction together when you search the trace ID from +a centralized logging dashboard such as Splunk.

+
String myRoute = po.getRoute();
+String traceId = po.getTraceId();
+String tracePath = po.getTracePath();
+
+

Trace annotation

+

You can use the PostOffice instance to annotate a trace in your function like this:

+
// annotate a trace with the key-value "hello:world"
+po.annotateTrace("hello", "world");
+
+

This is useful when you want to attach transaction specific information in the performance metrics. +For example, the traces may be used in production transaction analytics.

+
+

IMPORTANT: do not annotate sensitive or secret information such as PII, PHI, PCI data because + the trace is visible in application log. It may also be forwarded to a centralized + telemetry dashboard.

+
+

Configuration API

+

Your function can access the main application configuration from the platform like this:

+
AppConfigReader config = AppConfigReader.getInstance();
+// the value can be string or a primitive
+Object value = config.get('my.parameter');
+// the return value will be converted to a string
+String text = config.getProperty('my.parameter');
+
+

The system uses the standard dot-bracket format for a parameter name.

+
+

e.g. "hello.world", "some.key[2]"

+
+

You can override the main application configuration at run-time using the Java argument "-D".

+
+

e.g. "java -Dserver.port=8080 -jar myApp.jar"

+
+

Additional configuration files can be added with the ConfigReader API like this:

+
// filePath should have location prefix "classpath:/" or "file:/"
+ConfigReader reader = new ConfigReader();
+reader.load(filePath);
+
+

The configuration system supports environment variable or reference to the main application configuration +using the dollar-bracket syntax ${reference:default_value}.

+
+

e.g. "some.key=${MY_ENV_VARIABLE}", "some.key=${my.key}"

+
+

Custom serializer

+

We are using GSON as the underlying serializer to handle common use cases. However, there may be +situation that you want to use your own custom serialization library.

+

To do that, you may write a serializer that implements the CustomSerializer interface:

+
public interface CustomSerializer {
+
+    public Map<String, Object> toMap(Object obj);
+
+    public <T> T toPoJo(Object obj, Class<T> toValueType);
+
+}
+
+

You may configure a user function to use a custom serializer by adding the "customSerializer" parameter +in the PreLoad annotation. For example,

+
@PreLoad(route="my.user.function", customSerializer = JacksonSerializer.class)
+public class MyUserFunction implements TypedLambdaFunction<SimplePoJo, SimplePoJo> {
+    @Override
+    public SimplePoJo handleEvent(Map<String, String> headers, SimplePoJo input, int instance) {
+        return input;
+    }
+}
+
+

If you register your function dynamically in code, you can use the following platform API to assign +a custom serializer.

+
public void setCustomSerializer(String route, CustomSerializer mapper);
+// e.g.
+// platform.setCustomSerializer("my.function", new JacksonSerializer());
+
+

If you use the PostOffice to programmatically send event or make event RPC call and you need +custom serializer, you can create a PostOffice instance like this:

+
// this should be the first statement in the "handleEvent" method.
+PostOffice po = new PostOffice(headers, instance, new MyCustomSerializer());
+
+

The outgoing event using the PostOffice will use the custom serializer automatically. +To interpret an event response from a RPC call, you can use the following PostOffice API:

+
MyPoJo result = po.getResponseBodyAsPoJo(responseEvent, MyPoJo.class);
+
+

Minimalist API design for event orchestration

+

As a best practice, we advocate a minimalist approach in API integration. +To build powerful composable applications, the above set of APIs is sufficient to perform +"event orchestration" where you write code to coordinate how the various functions work together as a +single "executable". Please refer to Chapter-4 for more details about event orchestration.

+

Since Mercury is used in production installations, we will exercise the best effort to keep the core API stable.

+

Other APIs in the toolkits are used internally to build the engine itself, and they may change from time to time. +They are mostly convenient methods and utilities. The engine is fully encapsulated and any internal API changes +are not likely to impact your applications.

+

Optional Event Scripting

+

To further reduce coding effort, you can perform "event orchestration" by configuration using "Event Script". +It is available as an enterprise add-on module from Accenture.

+

Co-existence with other development frameworks

+

Mercury libraries are designed to co-exist with your favorite frameworks and tools. Inside a class implementing +the LambdaFunction, TypedLambdaFunction or KotlinLambdaFunction, you can use any coding style and frameworks +as you like, including sequential, object-oriented and reactive programming styles.

+

Mercury version 3 has a built-in lightweight non-blocking HTTP server, but you can also use Spring Boot and other +application server framework with it.

+

A sample Spring Boot integration is provided in the "rest-spring" project. It is an optional feature, and you can +decide to use a regular Spring Boot application with Mercury or to pick the customized Spring Boot in the +"rest-spring" library.

+

Template application for quick start

+

You can use the lambda-example project as a template to start writing your own applications. It is preconfigured +to support kernel threads, coroutine and suspend function.

+

Source code update frequency

+

This project is licensed under the Apache 2.0 open sources license. We will update the public codebase after +it passes regression tests and meets stability and performance benchmarks in our production systems.

+

Mercury is developed as an engine for you to build the latest cloud native and composable applications. +While we are updating the technology frequently, the essential internals and the core APIs are stable.

+

We are monitoring the progress of the upcoming Java 19 Virtual Thread feature and will include it in our API +when it becomes officially available.

+

Technical support

+

For enterprise clients, optional technical support is available. Please contact your Accenture representative +for details. +

+ + + + + + + + + + + + + + + +
Chapter-8HomeChapter-10
Service MeshTable of ContentsMigration Guide
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/TABLE-OF-CONTENTS/index.html b/docs/guides/TABLE-OF-CONTENTS/index.html new file mode 100644 index 000000000..b32c79ac7 --- /dev/null +++ b/docs/guides/TABLE-OF-CONTENTS/index.html @@ -0,0 +1,213 @@ + + + + + + + + Contents - Mercury + + + + + + + + + + + + + +
+ + +
+ + + +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/diagrams/architecture.png b/docs/guides/diagrams/architecture.png new file mode 100644 index 000000000..b55138ae2 Binary files /dev/null and b/docs/guides/diagrams/architecture.png differ diff --git a/docs/img/favicon.ico b/docs/img/favicon.ico new file mode 100644 index 000000000..e85006a3c Binary files /dev/null and b/docs/img/favicon.ico differ diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 000000000..a4b4b0f9b --- /dev/null +++ b/docs/index.html @@ -0,0 +1,510 @@ + + + + + + + + Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Welcome to the Mercury project

+

The Mercury project is created with one primary objective - +to make software easy to write, read, test, deploy, scale and manage.

+

Mercury has been powering mission-critical cloud native production applications for the last few years. +In version 3, Mercury has evolved to the next level. It is bringing event-driven design and the best of +preemptive and cooperative multitasking as a foundation to build "composable applications."

+

You have low-level control to precisely tune your application for optimal performance and throughput +using three function execution strategies:

+
    +
  1. kernel thread pool
  2. +
  3. coroutine
  4. +
  5. suspend function
  6. +
+

Mercury version 3 achieves virtual threading using coroutine and suspend function.

+

In version 4, it fully embraces Java 21 virtual thread feature that was officially available since 12/2023.

+

August, 2024

+

Mercury version 3

+

Mercury version 3 is a software development toolkit for event-driven programming.

+

Event-driven programming allows functional modules to communicate with each other using events instead +of direct method calls. This allows the functions to be executed asynchronously, improving overall +application performance.

+

IMPORTANT: You should use Mercury version 4 unless you need backward compatibility for your production systems.

+

Please visit Mercury Version 4 using the links below:

+

Mercury v4: https://github.com/Accenture/mercury-composable

+

Documentation: https://accenture.github.io/mercury-composable/

+

Differences between Mercury version 3 and 4

+

The key differences of Mercury version 3 and the latest Mercury-Composable version 4 are:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryMercury 3.0Mercury 4.x
Java versionSupports Java 1.8 or higherRequires Java 21 or higher
Event managementOrchestration by codeEvent choreography by configuration
MultitaskingCoroutine and kernelJava 21 virtual threads, coroutine and kernel
Functional isolationKernelThreadRunnerVirtual Threads and KernelThreadRunner
+

Breaking changes

+

By default, the system runs all functions as "coroutines" where previous versions run them using +a kernel thread pool.

+

The CoroutineRunner annotation has been removed and replaced with the new KernelThreadRunner annotation.

+

The "rest.automation.yaml" key is renamed as "yaml.rest.automation" after we unify the parsing behavior +of application.properties with application.yml.

+

Write your first composable application

+

To get started with your first application, please refer to the Chapter 4, Developer Guide.

+

Introduction to composable architecture

+

In cloud migration and IT modernization, we evaluate application portfolio and recommend different +disposition strategies based on the 7R migration methodology.

+
7R: Retire, retain, re-host, re-platform, replace, re-architect and re-imagine.
+
+

The most common observation during IT modernization discovery is that there are many complex monolithic applications +that are hard to modernize quickly.

+

IT modernization is like moving into a new home. It would be the opportunity to clean up and to improve for +business agility and strategic competitiveness.

+

Composable architecture is gaining visibility recently because it accelerates organization transformation towards +a cloud native future. We will discuss how we may reduce modernization risks with this approach.

+

Composability

+

Composability applies to both platform and application levels.

+

We can trace the root of composability to Service Oriented Architecture (SOA) in 2000 or a technical bulletin on +"Flow-Based Programming" by IBM in 1971. This is the idea that architecture and applications are built using +modular building blocks and each block is self-contained with predictable behavior.

+

At the platform level, composable architecture refers to loosely coupled platform services, utilities, and +business applications. With modular design, you can assemble platform components and applications to create +new use cases or to adjust for ever-changing business environment and requirements. Domain driven design (DDD), +Command Query Responsibility Segregation (CQRS) and Microservices patterns are the popular tools that architects +use to build composable architecture. You may deploy application in container, serverless or other means.

+

At the application level, a composable application means that an application is assembled from modular software +components or functions that are self-contained and pluggable. You can mix-n-match functions to form new applications. +You can retire outdated functions without adverse side effect to a production system. Multiple versions of a function +can exist, and you can decide how to route user requests to different versions of a function. Applications would be +easier to design, develop, maintain, deploy, and scale.

+

Composable architecture and applications contribute to business agility.

+

Building a composable application

+

Microservices

+

Since 2014, microservices architectural pattern helps to decompose a big application into smaller pieces of +“self-contained” services. We also apply digital decoupling techniques to services and domains. Smaller is better. +However, we are writing code in the same old fashion. One method is calling other methods directly. Functional and +reactive programming techniques are means to run code in a non-blocking manner, for example Reactive Streams, Akka, +Vertx, Quarkus Multi/Uni and Spring Reactive Flux/Mono. These are excellent tools, but they do not reduce the +complexity of business applications.

+

Composable application

+

To make an application composable, the software components within a single application should be loosely coupled +where each component has zero or minimal dependencies.

+

Unlike traditional programming approach, composable application is built from the top down. First, we describe +a business transaction as an event flow. Second, from the event flow, we identify individual functions for +business logic. Third, we write user story for each function and write code in a self-contained manner. +Finally, we write orchestration code to coordinate event flow among the functions, so they work together +as a single application.

+

The individual functions become the building block for a composable application. We can mix-n-match different +sets of functions to address different business use cases.

+

Event is the communication conduit

+

Cloud native applications are deployed as containers or serverless functions. Ideally, they communicate using events. +For example, the CQRS design pattern is well accepted for building high performance cloud native applications.

+
+

Figure 1 - Cloud native applications use event streams to communicate

+
+

figure-1.png

+

However, within a single application unit, the application is mostly built in a traditional way. +i.e. one function is calling other functions and libraries directly, thus making the modules and libraries +tightly coupled. As a result, microservices may become smaller monolithic applications.

+

To overcome this limitation, we can employ “event-driven design” to make the microservices application unit composable.

+

An application unit is a collection of functions in memory and an “event bus” is the communication conduit to connect +the functions together to form a single executable.

+
+

Figure 2 – Functions use in-memory event bus to communicate

+
+

figure-2.png

+

In-memory event bus

+

For a composable application, each function is written using the first principle of “input-process-output” where +input and output payloads are delivered as events. All input and output are immutable to reduce unintended bugs +and side effects.

+

Since input and output for each function is well-defined, test-driven development (TDD) can be done naturally. +It is also easier to define a user story for each function and the developer does not need to study and integrate +multiple levels of dependencies, resulting in higher quality code.

+
+

Figure 3 - The first principle of a function

+
+

figure-3.png

+

What is a “function”? For example, reading a record from a database and performing some data transformation, +doing a calculation with a formula, etc.

+
+

Figure 4 - Connecting output of one function to input of another

+
+

figure-4.png

+

As shown in Figure 4, if function-1 wants to send a request to function-2, we can write “event orchestration code” +to put the output from function-1 into an event envelope and send it over an in-memory event bus. The event system +will transport the event envelope to function-2, extract the payload and submit it as “input” to function-2

+

Function execution strategy

+

In event-driven application design, a function is executed when an event arrives as “input.” When a function +finishes processing, your application can command the event system to route the result set (“output”) as an +event to another function.

+
+

Figure 5 - Executing function through event flow

+
+

figure-5.png

+

As shown in Figure 5, functions can send/receive events using an in-memory event bus (aka "event loop").

+

This event-driven architecture provides the foundation to design and implement composable applications. +Each function is self-contained and loosely coupled by event flow.

+

A function receiving an event needs to be executed. There are three ways to do that:

+
    +
  1. Kernel thread pool
  2. +
  3. Coroutine
  4. +
  5. Suspend function
  6. +
+

Kernel thread pool

+

Java supports “preemptive multitasking” using kernel threads. Multiple functions can execute in parallel. +Preemptive multitasking leverages the multiple cores of a CPU to yield higher performance.

+

Preemptive multitasking is performed at the kernel level and the operating system is doing the context switching. +As a result, the maximum number of kernel threads is small. As a rule of thumb, a moderately fast computer can +support ~200 kernel threads.

+
+

Figure 6 - Multitasking of kernel threads at the hardware and OS level

+
+

figure-6.png

+

Coroutine

+

Many modern programming languages such as GoLang, Kotlin, Python and Node.js support “cooperative multitasking” +using “event loop” or “coroutine.” Instead of context switching at the kernel level, functions are executed orderly +by yielding to each other. The order of execution depends on the event flow of each business transaction.

+

Since the functions are running cooperatively, the overheads of context switching are low. “Event loop” or +“Coroutine” technology usually can support tens of thousands of “functions” running in “parallel.” +Technically, the functions are running sequentially. When each function finishes execution very quickly, +they appear as running concurrently.

+
+

Figure 7 - Cooperative multitasking of coroutines

+
+

figure-7.png

+

Java 1.8 and higher versions support event loop with open sources libraries such as Lightbend Akka and Eclipse Vertx. +A preview “virtual thread” technology is available in Java version 19. It brings cooperative multitasking by running +tens of thousands of “virtual threads” in a single kernel thread. This is a major technological breakthrough to close +the gap with other modern programming languages.

+

“Suspend function”

+

In a typical enterprise application, many functions are waiting for responses most of the time. +In preemptive multitasking, these functions are using kernel threads and consuming CPU time. +Too many active kernel threads would turn the application into slow motion.

+

“Suspend function” not only avoids overwhelming the CPU with excessive kernel threads but also leverages the +synchronous request-response opportunity into high throughput non-blocking operation.

+

As the name indicates, “suspend function” can be suspended and resumed. When it is suspended, it yields control +to the event loop so that other coroutines or suspend functions can run.

+

In Node.js and GoLang, coroutine and suspend function are the same. Suspend function refers to the “async/await” +keywords or API of coroutine. In Kotlin, the suspend function extends a coroutine to have the suspend/resume ability.

+

A function is suspended when it is waiting for a response from the network, a database or from another function. +It is resumed when a response is received.

+
+

Figure 8 - Improving throughput with suspend function

+
+

figure-8.png

+

As shown in Figure 8, a “suspend function” can suspend and resume multiple times during its execution. +When it suspends, it is not using any CPU time, thus the application has more time to serve other functions. +This mechanism is so efficient that it can significantly increase the throughput of the application. +i.e. it can handle many concurrent users, and process more requests.

+

Performance and throughput

+

The ability to select an optimal function execution strategy for a function is critical to the success of a +composable application. This allows the developer to have low level control of how the application performs and scales.

+

Without an optimal function execution strategy, performance tuning is usually an educated guess.

+

In composable application architecture, each function is self-contained and stateless. We can predict the performance +of each function by selecting an optimal function execution strategy and evaluate it with unit tests and observability. +Predicting application performance and throughput at design and development time reduces modernization risks.

+

The pros and cons of each function execution strategy are summarized below:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
StrategyAdvantageDisadvantage
Kernel threadsHighest performance in terms of
operations per seconds
Lower number of concurrent threads
due to high context switching overheads
CoroutineHighest throughput in terms of
concurrent users served by virtual
threads concurrently
Not suitable for long running tasks
Suspend functionSequential "non-blocking" for
RPC (request-response) that
makes code easier to read and
maintain
Not suitable for long running tasks
+

As shown in the table above, performance and throughput are determined by function execution strategies.

+

For example, single threaded event driven network proxies such as nginx support twenty times more concurrent +connections than multithreaded application servers.

+

On the other hand, Node.js is not suitable for long running tasks. When one function takes more time to execute, +all other functions are blocked and thus degrading the overall application performance. The latest Node.js language +adds kernel threads using the “web worker” technology to alleviate this limitation. However, web worker API is more +tedious than multithreading in Java and other programming languages.

+

The best of both worlds

+

If we simplify event-driven programming and support all three function execution strategies, we can design and +implement composable applications that deliver high performance and high throughput.

+

The “virtual thread” feature in the upcoming Java version 19 will be a good building block for function execution +strategies. Currently it is available as a “preview” feature.

+

When it becomes available later in 2023, it will have a significant impact on the Java community. It will be at par +with other programming languages that support event loop. It supports non-blocking sequential programming without +explicitly using the “async” and “await” keywords. All current open sources libraries that provide event loop +functionality would evolve.

+

To accelerate this evolution, we have implemented Mercury version 3.0 as an accelerator to build composable +applications. It supports the two pillars of composable application – In-memory event bus and selection of +function execution strategies.

+

It integrates with Eclipse Vertx to hide the complexity of event-driven programming and embraces the three function +execution strategies using kernel thread pool, coroutine and suspend function. The default execution strategy is +"coroutine" unless you specify the function using the "KernelThreadRunner" annotation. To simplify writing +“suspend function,” you can implement the “KotlinLambdaFunction” class and copy-n-paste your existing Java code +into the new Kotlin class, the IDE will automatically convert code for you. With 90% conversion efficiency, +you may need minor touch up to finish the rest.

+

We can construct a composable application with self-contained functions that execute when events arrive. +There is a simple event API that we call the “Post Office” to support sequential non-blocking RPC, async, +drop and forget, callback, workflow, pipeline, streaming and interceptor patterns.

+

Sequential non-blocking RPC reduces the effort in application modernization because we can directly port sequential +legacy code from a monolithic application to the new composable cloud native design.

+

What is "event orchestration"?

+

In traditional programming, we write code to make calls to different methods and libraries. In event-driven +programming, we write code to send events, and this is “event orchestration.” We can use events to make RPC call +just like traditional programming. It is viable to port legacy orchestration logic into event orchestration code.

+

To further reduce coding effort, we can use Event Script to do “event orchestration.” This would replace code with +simple event flow configuration.

+

To use event script, please upgrade to Mercury v4.

+

Mercury v4: https://github.com/Accenture/mercury-composable

+

Documentation: https://accenture.github.io/mercury-composable/

+

How steep is the learning curve for a developer?

+

The developer can use any coding style to write the individual functions, no matter it is sequential, object-oriented, +or reactive. One may use any favorite frameworks or libraries. There are no restrictions.

+

There is a learning curve in writing “event orchestration.” Since event orchestration supports sequential +non-blocking RPC, the developer can port existing legacy code to the modern style with direct mapping. +Typically, the learning curve is about two weeks. If you are familiar with event-driven programming, the learning +curve would be lower. To eliminate this learning curve, the developer may use Event Script that replaces orchestration +code with event flow configuration files. Event Script is designed to have virtually zero API integration for +exceptionally low learning curve.

+

Conclusion

+

Composability applies to both platform and application levels. We can design and implement better cloud native +applications that are composable using event-driven design and the three function execution strategies.

+

We can deliver application that demonstrates both high performance and high throughput, an objective that has been +technically challenging with traditional means. We can scientifically predict application performance and throughput +in design and development time, thus saving time and ensuring consistent product quality.

+

Composable approach also facilitates the migration of monolithic application into cloud native by decomposing the +application to functional level and assembling them into microservices according to domain boundary. +It reduces coding effort and application complexity, meaning less project risks.

+

Java version 19 is introducing a new “virtual thread” feature in 2023 that will make it at par with other modern +programming languages such as GoLang and Node.js. Since Java has the largest enterprise-grade open sources and +commercial libraries with easy access to a large pool of trained developers, the availability of virtual thread +technology would retain Java as the best option for application modernization and composable applications.

+

Mercury and Event Script version 3.0 bring virtual thread technology with Kotlin coroutine and suspend function +before Java version 19 becomes mainstream.

+

This opens a new frontier of cloud native applications that are composable, scalable, and easy to maintain, +thus contributing to business agility.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + + Next » + + +
+ + + + + + + + + + + diff --git a/docs/js/html5shiv.min.js b/docs/js/html5shiv.min.js new file mode 100644 index 000000000..1a01c94ba --- /dev/null +++ b/docs/js/html5shiv.min.js @@ -0,0 +1,4 @@ +/** +* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed +*/ +!function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document); diff --git a/docs/js/jquery-3.6.0.min.js b/docs/js/jquery-3.6.0.min.js new file mode 100644 index 000000000..c4c6022f2 --- /dev/null +++ b/docs/js/jquery-3.6.0.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t + + + + + + + Mercury + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • +
  • +
  • +
+
+
+
+
+ + +

Search Results

+ + + +
+ Searching... +
+ + +
+
+ +
+
+ +
+ +
+ +
+ + + + + +
+ + + + + + + + + diff --git a/docs/search/lunr.js b/docs/search/lunr.js new file mode 100644 index 000000000..aca0a167f --- /dev/null +++ b/docs/search/lunr.js @@ -0,0 +1,3475 @@ +/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 + * Copyright (C) 2020 Oliver Nightingale + * @license MIT + */ + +;(function(){ + +/** + * A convenience function for configuring and constructing + * a new lunr Index. + * + * A lunr.Builder instance is created and the pipeline setup + * with a trimmer, stop word filter and stemmer. + * + * This builder object is yielded to the configuration function + * that is passed as a parameter, allowing the list of fields + * and other builder parameters to be customised. + * + * All documents _must_ be added within the passed config function. + * + * @example + * var idx = lunr(function () { + * this.field('title') + * this.field('body') + * this.ref('id') + * + * documents.forEach(function (doc) { + * this.add(doc) + * }, this) + * }) + * + * @see {@link lunr.Builder} + * @see {@link lunr.Pipeline} + * @see {@link lunr.trimmer} + * @see {@link lunr.stopWordFilter} + * @see {@link lunr.stemmer} + * @namespace {function} lunr + */ +var lunr = function (config) { + var builder = new lunr.Builder + + builder.pipeline.add( + lunr.trimmer, + lunr.stopWordFilter, + lunr.stemmer + ) + + builder.searchPipeline.add( + lunr.stemmer + ) + + config.call(builder, builder) + return builder.build() +} + +lunr.version = "2.3.9" +/*! + * lunr.utils + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A namespace containing utils for the rest of the lunr library + * @namespace lunr.utils + */ +lunr.utils = {} + +/** + * Print a warning message to the console. + * + * @param {String} message The message to be printed. + * @memberOf lunr.utils + * @function + */ +lunr.utils.warn = (function (global) { + /* eslint-disable no-console */ + return function (message) { + if (global.console && console.warn) { + console.warn(message) + } + } + /* eslint-enable no-console */ +})(this) + +/** + * Convert an object to a string. + * + * In the case of `null` and `undefined` the function returns + * the empty string, in all other cases the result of calling + * `toString` on the passed object is returned. + * + * @param {Any} obj The object to convert to a string. + * @return {String} string representation of the passed object. + * @memberOf lunr.utils + */ +lunr.utils.asString = function (obj) { + if (obj === void 0 || obj === null) { + return "" + } else { + return obj.toString() + } +} + +/** + * Clones an object. + * + * Will create a copy of an existing object such that any mutations + * on the copy cannot affect the original. + * + * Only shallow objects are supported, passing a nested object to this + * function will cause a TypeError. + * + * Objects with primitives, and arrays of primitives are supported. + * + * @param {Object} obj The object to clone. + * @return {Object} a clone of the passed object. + * @throws {TypeError} when a nested object is passed. + * @memberOf Utils + */ +lunr.utils.clone = function (obj) { + if (obj === null || obj === undefined) { + return obj + } + + var clone = Object.create(null), + keys = Object.keys(obj) + + for (var i = 0; i < keys.length; i++) { + var key = keys[i], + val = obj[key] + + if (Array.isArray(val)) { + clone[key] = val.slice() + continue + } + + if (typeof val === 'string' || + typeof val === 'number' || + typeof val === 'boolean') { + clone[key] = val + continue + } + + throw new TypeError("clone is not deep and does not support nested objects") + } + + return clone +} +lunr.FieldRef = function (docRef, fieldName, stringValue) { + this.docRef = docRef + this.fieldName = fieldName + this._stringValue = stringValue +} + +lunr.FieldRef.joiner = "/" + +lunr.FieldRef.fromString = function (s) { + var n = s.indexOf(lunr.FieldRef.joiner) + + if (n === -1) { + throw "malformed field ref string" + } + + var fieldRef = s.slice(0, n), + docRef = s.slice(n + 1) + + return new lunr.FieldRef (docRef, fieldRef, s) +} + +lunr.FieldRef.prototype.toString = function () { + if (this._stringValue == undefined) { + this._stringValue = this.fieldName + lunr.FieldRef.joiner + this.docRef + } + + return this._stringValue +} +/*! + * lunr.Set + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A lunr set. + * + * @constructor + */ +lunr.Set = function (elements) { + this.elements = Object.create(null) + + if (elements) { + this.length = elements.length + + for (var i = 0; i < this.length; i++) { + this.elements[elements[i]] = true + } + } else { + this.length = 0 + } +} + +/** + * A complete set that contains all elements. + * + * @static + * @readonly + * @type {lunr.Set} + */ +lunr.Set.complete = { + intersect: function (other) { + return other + }, + + union: function () { + return this + }, + + contains: function () { + return true + } +} + +/** + * An empty set that contains no elements. + * + * @static + * @readonly + * @type {lunr.Set} + */ +lunr.Set.empty = { + intersect: function () { + return this + }, + + union: function (other) { + return other + }, + + contains: function () { + return false + } +} + +/** + * Returns true if this set contains the specified object. + * + * @param {object} object - Object whose presence in this set is to be tested. + * @returns {boolean} - True if this set contains the specified object. + */ +lunr.Set.prototype.contains = function (object) { + return !!this.elements[object] +} + +/** + * Returns a new set containing only the elements that are present in both + * this set and the specified set. + * + * @param {lunr.Set} other - set to intersect with this set. + * @returns {lunr.Set} a new set that is the intersection of this and the specified set. + */ + +lunr.Set.prototype.intersect = function (other) { + var a, b, elements, intersection = [] + + if (other === lunr.Set.complete) { + return this + } + + if (other === lunr.Set.empty) { + return other + } + + if (this.length < other.length) { + a = this + b = other + } else { + a = other + b = this + } + + elements = Object.keys(a.elements) + + for (var i = 0; i < elements.length; i++) { + var element = elements[i] + if (element in b.elements) { + intersection.push(element) + } + } + + return new lunr.Set (intersection) +} + +/** + * Returns a new set combining the elements of this and the specified set. + * + * @param {lunr.Set} other - set to union with this set. + * @return {lunr.Set} a new set that is the union of this and the specified set. + */ + +lunr.Set.prototype.union = function (other) { + if (other === lunr.Set.complete) { + return lunr.Set.complete + } + + if (other === lunr.Set.empty) { + return this + } + + return new lunr.Set(Object.keys(this.elements).concat(Object.keys(other.elements))) +} +/** + * A function to calculate the inverse document frequency for + * a posting. This is shared between the builder and the index + * + * @private + * @param {object} posting - The posting for a given term + * @param {number} documentCount - The total number of documents. + */ +lunr.idf = function (posting, documentCount) { + var documentsWithTerm = 0 + + for (var fieldName in posting) { + if (fieldName == '_index') continue // Ignore the term index, its not a field + documentsWithTerm += Object.keys(posting[fieldName]).length + } + + var x = (documentCount - documentsWithTerm + 0.5) / (documentsWithTerm + 0.5) + + return Math.log(1 + Math.abs(x)) +} + +/** + * A token wraps a string representation of a token + * as it is passed through the text processing pipeline. + * + * @constructor + * @param {string} [str=''] - The string token being wrapped. + * @param {object} [metadata={}] - Metadata associated with this token. + */ +lunr.Token = function (str, metadata) { + this.str = str || "" + this.metadata = metadata || {} +} + +/** + * Returns the token string that is being wrapped by this object. + * + * @returns {string} + */ +lunr.Token.prototype.toString = function () { + return this.str +} + +/** + * A token update function is used when updating or optionally + * when cloning a token. + * + * @callback lunr.Token~updateFunction + * @param {string} str - The string representation of the token. + * @param {Object} metadata - All metadata associated with this token. + */ + +/** + * Applies the given function to the wrapped string token. + * + * @example + * token.update(function (str, metadata) { + * return str.toUpperCase() + * }) + * + * @param {lunr.Token~updateFunction} fn - A function to apply to the token string. + * @returns {lunr.Token} + */ +lunr.Token.prototype.update = function (fn) { + this.str = fn(this.str, this.metadata) + return this +} + +/** + * Creates a clone of this token. Optionally a function can be + * applied to the cloned token. + * + * @param {lunr.Token~updateFunction} [fn] - An optional function to apply to the cloned token. + * @returns {lunr.Token} + */ +lunr.Token.prototype.clone = function (fn) { + fn = fn || function (s) { return s } + return new lunr.Token (fn(this.str, this.metadata), this.metadata) +} +/*! + * lunr.tokenizer + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A function for splitting a string into tokens ready to be inserted into + * the search index. Uses `lunr.tokenizer.separator` to split strings, change + * the value of this property to change how strings are split into tokens. + * + * This tokenizer will convert its parameter to a string by calling `toString` and + * then will split this string on the character in `lunr.tokenizer.separator`. + * Arrays will have their elements converted to strings and wrapped in a lunr.Token. + * + * Optional metadata can be passed to the tokenizer, this metadata will be cloned and + * added as metadata to every token that is created from the object to be tokenized. + * + * @static + * @param {?(string|object|object[])} obj - The object to convert into tokens + * @param {?object} metadata - Optional metadata to associate with every token + * @returns {lunr.Token[]} + * @see {@link lunr.Pipeline} + */ +lunr.tokenizer = function (obj, metadata) { + if (obj == null || obj == undefined) { + return [] + } + + if (Array.isArray(obj)) { + return obj.map(function (t) { + return new lunr.Token( + lunr.utils.asString(t).toLowerCase(), + lunr.utils.clone(metadata) + ) + }) + } + + var str = obj.toString().toLowerCase(), + len = str.length, + tokens = [] + + for (var sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) { + var char = str.charAt(sliceEnd), + sliceLength = sliceEnd - sliceStart + + if ((char.match(lunr.tokenizer.separator) || sliceEnd == len)) { + + if (sliceLength > 0) { + var tokenMetadata = lunr.utils.clone(metadata) || {} + tokenMetadata["position"] = [sliceStart, sliceLength] + tokenMetadata["index"] = tokens.length + + tokens.push( + new lunr.Token ( + str.slice(sliceStart, sliceEnd), + tokenMetadata + ) + ) + } + + sliceStart = sliceEnd + 1 + } + + } + + return tokens +} + +/** + * The separator used to split a string into tokens. Override this property to change the behaviour of + * `lunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens. + * + * @static + * @see lunr.tokenizer + */ +lunr.tokenizer.separator = /[\s\-]+/ +/*! + * lunr.Pipeline + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.Pipelines maintain an ordered list of functions to be applied to all + * tokens in documents entering the search index and queries being ran against + * the index. + * + * An instance of lunr.Index created with the lunr shortcut will contain a + * pipeline with a stop word filter and an English language stemmer. Extra + * functions can be added before or after either of these functions or these + * default functions can be removed. + * + * When run the pipeline will call each function in turn, passing a token, the + * index of that token in the original list of all tokens and finally a list of + * all the original tokens. + * + * The output of functions in the pipeline will be passed to the next function + * in the pipeline. To exclude a token from entering the index the function + * should return undefined, the rest of the pipeline will not be called with + * this token. + * + * For serialisation of pipelines to work, all functions used in an instance of + * a pipeline should be registered with lunr.Pipeline. Registered functions can + * then be loaded. If trying to load a serialised pipeline that uses functions + * that are not registered an error will be thrown. + * + * If not planning on serialising the pipeline then registering pipeline functions + * is not necessary. + * + * @constructor + */ +lunr.Pipeline = function () { + this._stack = [] +} + +lunr.Pipeline.registeredFunctions = Object.create(null) + +/** + * A pipeline function maps lunr.Token to lunr.Token. A lunr.Token contains the token + * string as well as all known metadata. A pipeline function can mutate the token string + * or mutate (or add) metadata for a given token. + * + * A pipeline function can indicate that the passed token should be discarded by returning + * null, undefined or an empty string. This token will not be passed to any downstream pipeline + * functions and will not be added to the index. + * + * Multiple tokens can be returned by returning an array of tokens. Each token will be passed + * to any downstream pipeline functions and all will returned tokens will be added to the index. + * + * Any number of pipeline functions may be chained together using a lunr.Pipeline. + * + * @interface lunr.PipelineFunction + * @param {lunr.Token} token - A token from the document being processed. + * @param {number} i - The index of this token in the complete list of tokens for this document/field. + * @param {lunr.Token[]} tokens - All tokens for this document/field. + * @returns {(?lunr.Token|lunr.Token[])} + */ + +/** + * Register a function with the pipeline. + * + * Functions that are used in the pipeline should be registered if the pipeline + * needs to be serialised, or a serialised pipeline needs to be loaded. + * + * Registering a function does not add it to a pipeline, functions must still be + * added to instances of the pipeline for them to be used when running a pipeline. + * + * @param {lunr.PipelineFunction} fn - The function to check for. + * @param {String} label - The label to register this function with + */ +lunr.Pipeline.registerFunction = function (fn, label) { + if (label in this.registeredFunctions) { + lunr.utils.warn('Overwriting existing registered function: ' + label) + } + + fn.label = label + lunr.Pipeline.registeredFunctions[fn.label] = fn +} + +/** + * Warns if the function is not registered as a Pipeline function. + * + * @param {lunr.PipelineFunction} fn - The function to check for. + * @private + */ +lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) { + var isRegistered = fn.label && (fn.label in this.registeredFunctions) + + if (!isRegistered) { + lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn) + } +} + +/** + * Loads a previously serialised pipeline. + * + * All functions to be loaded must already be registered with lunr.Pipeline. + * If any function from the serialised data has not been registered then an + * error will be thrown. + * + * @param {Object} serialised - The serialised pipeline to load. + * @returns {lunr.Pipeline} + */ +lunr.Pipeline.load = function (serialised) { + var pipeline = new lunr.Pipeline + + serialised.forEach(function (fnName) { + var fn = lunr.Pipeline.registeredFunctions[fnName] + + if (fn) { + pipeline.add(fn) + } else { + throw new Error('Cannot load unregistered function: ' + fnName) + } + }) + + return pipeline +} + +/** + * Adds new functions to the end of the pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction[]} functions - Any number of functions to add to the pipeline. + */ +lunr.Pipeline.prototype.add = function () { + var fns = Array.prototype.slice.call(arguments) + + fns.forEach(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + this._stack.push(fn) + }, this) +} + +/** + * Adds a single function after a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. + * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. + */ +lunr.Pipeline.prototype.after = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + if (pos == -1) { + throw new Error('Cannot find existingFn') + } + + pos = pos + 1 + this._stack.splice(pos, 0, newFn) +} + +/** + * Adds a single function before a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. + * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. + */ +lunr.Pipeline.prototype.before = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + if (pos == -1) { + throw new Error('Cannot find existingFn') + } + + this._stack.splice(pos, 0, newFn) +} + +/** + * Removes a function from the pipeline. + * + * @param {lunr.PipelineFunction} fn The function to remove from the pipeline. + */ +lunr.Pipeline.prototype.remove = function (fn) { + var pos = this._stack.indexOf(fn) + if (pos == -1) { + return + } + + this._stack.splice(pos, 1) +} + +/** + * Runs the current list of functions that make up the pipeline against the + * passed tokens. + * + * @param {Array} tokens The tokens to run through the pipeline. + * @returns {Array} + */ +lunr.Pipeline.prototype.run = function (tokens) { + var stackLength = this._stack.length + + for (var i = 0; i < stackLength; i++) { + var fn = this._stack[i] + var memo = [] + + for (var j = 0; j < tokens.length; j++) { + var result = fn(tokens[j], j, tokens) + + if (result === null || result === void 0 || result === '') continue + + if (Array.isArray(result)) { + for (var k = 0; k < result.length; k++) { + memo.push(result[k]) + } + } else { + memo.push(result) + } + } + + tokens = memo + } + + return tokens +} + +/** + * Convenience method for passing a string through a pipeline and getting + * strings out. This method takes care of wrapping the passed string in a + * token and mapping the resulting tokens back to strings. + * + * @param {string} str - The string to pass through the pipeline. + * @param {?object} metadata - Optional metadata to associate with the token + * passed to the pipeline. + * @returns {string[]} + */ +lunr.Pipeline.prototype.runString = function (str, metadata) { + var token = new lunr.Token (str, metadata) + + return this.run([token]).map(function (t) { + return t.toString() + }) +} + +/** + * Resets the pipeline by removing any existing processors. + * + */ +lunr.Pipeline.prototype.reset = function () { + this._stack = [] +} + +/** + * Returns a representation of the pipeline ready for serialisation. + * + * Logs a warning if the function has not been registered. + * + * @returns {Array} + */ +lunr.Pipeline.prototype.toJSON = function () { + return this._stack.map(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + + return fn.label + }) +} +/*! + * lunr.Vector + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A vector is used to construct the vector space of documents and queries. These + * vectors support operations to determine the similarity between two documents or + * a document and a query. + * + * Normally no parameters are required for initializing a vector, but in the case of + * loading a previously dumped vector the raw elements can be provided to the constructor. + * + * For performance reasons vectors are implemented with a flat array, where an elements + * index is immediately followed by its value. E.g. [index, value, index, value]. This + * allows the underlying array to be as sparse as possible and still offer decent + * performance when being used for vector calculations. + * + * @constructor + * @param {Number[]} [elements] - The flat list of element index and element value pairs. + */ +lunr.Vector = function (elements) { + this._magnitude = 0 + this.elements = elements || [] +} + + +/** + * Calculates the position within the vector to insert a given index. + * + * This is used internally by insert and upsert. If there are duplicate indexes then + * the position is returned as if the value for that index were to be updated, but it + * is the callers responsibility to check whether there is a duplicate at that index + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @returns {Number} + */ +lunr.Vector.prototype.positionForIndex = function (index) { + // For an empty vector the tuple can be inserted at the beginning + if (this.elements.length == 0) { + return 0 + } + + var start = 0, + end = this.elements.length / 2, + sliceLength = end - start, + pivotPoint = Math.floor(sliceLength / 2), + pivotIndex = this.elements[pivotPoint * 2] + + while (sliceLength > 1) { + if (pivotIndex < index) { + start = pivotPoint + } + + if (pivotIndex > index) { + end = pivotPoint + } + + if (pivotIndex == index) { + break + } + + sliceLength = end - start + pivotPoint = start + Math.floor(sliceLength / 2) + pivotIndex = this.elements[pivotPoint * 2] + } + + if (pivotIndex == index) { + return pivotPoint * 2 + } + + if (pivotIndex > index) { + return pivotPoint * 2 + } + + if (pivotIndex < index) { + return (pivotPoint + 1) * 2 + } +} + +/** + * Inserts an element at an index within the vector. + * + * Does not allow duplicates, will throw an error if there is already an entry + * for this index. + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @param {Number} val - The value to be inserted into the vector. + */ +lunr.Vector.prototype.insert = function (insertIdx, val) { + this.upsert(insertIdx, val, function () { + throw "duplicate index" + }) +} + +/** + * Inserts or updates an existing index within the vector. + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @param {Number} val - The value to be inserted into the vector. + * @param {function} fn - A function that is called for updates, the existing value and the + * requested value are passed as arguments + */ +lunr.Vector.prototype.upsert = function (insertIdx, val, fn) { + this._magnitude = 0 + var position = this.positionForIndex(insertIdx) + + if (this.elements[position] == insertIdx) { + this.elements[position + 1] = fn(this.elements[position + 1], val) + } else { + this.elements.splice(position, 0, insertIdx, val) + } +} + +/** + * Calculates the magnitude of this vector. + * + * @returns {Number} + */ +lunr.Vector.prototype.magnitude = function () { + if (this._magnitude) return this._magnitude + + var sumOfSquares = 0, + elementsLength = this.elements.length + + for (var i = 1; i < elementsLength; i += 2) { + var val = this.elements[i] + sumOfSquares += val * val + } + + return this._magnitude = Math.sqrt(sumOfSquares) +} + +/** + * Calculates the dot product of this vector and another vector. + * + * @param {lunr.Vector} otherVector - The vector to compute the dot product with. + * @returns {Number} + */ +lunr.Vector.prototype.dot = function (otherVector) { + var dotProduct = 0, + a = this.elements, b = otherVector.elements, + aLen = a.length, bLen = b.length, + aVal = 0, bVal = 0, + i = 0, j = 0 + + while (i < aLen && j < bLen) { + aVal = a[i], bVal = b[j] + if (aVal < bVal) { + i += 2 + } else if (aVal > bVal) { + j += 2 + } else if (aVal == bVal) { + dotProduct += a[i + 1] * b[j + 1] + i += 2 + j += 2 + } + } + + return dotProduct +} + +/** + * Calculates the similarity between this vector and another vector. + * + * @param {lunr.Vector} otherVector - The other vector to calculate the + * similarity with. + * @returns {Number} + */ +lunr.Vector.prototype.similarity = function (otherVector) { + return this.dot(otherVector) / this.magnitude() || 0 +} + +/** + * Converts the vector to an array of the elements within the vector. + * + * @returns {Number[]} + */ +lunr.Vector.prototype.toArray = function () { + var output = new Array (this.elements.length / 2) + + for (var i = 1, j = 0; i < this.elements.length; i += 2, j++) { + output[j] = this.elements[i] + } + + return output +} + +/** + * A JSON serializable representation of the vector. + * + * @returns {Number[]} + */ +lunr.Vector.prototype.toJSON = function () { + return this.elements +} +/* eslint-disable */ +/*! + * lunr.stemmer + * Copyright (C) 2020 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + +/** + * lunr.stemmer is an english language stemmer, this is a JavaScript + * implementation of the PorterStemmer taken from http://tartarus.org/~martin + * + * @static + * @implements {lunr.PipelineFunction} + * @param {lunr.Token} token - The string to stem + * @returns {lunr.Token} + * @see {@link lunr.Pipeline} + * @function + */ +lunr.stemmer = (function(){ + var step2list = { + "ational" : "ate", + "tional" : "tion", + "enci" : "ence", + "anci" : "ance", + "izer" : "ize", + "bli" : "ble", + "alli" : "al", + "entli" : "ent", + "eli" : "e", + "ousli" : "ous", + "ization" : "ize", + "ation" : "ate", + "ator" : "ate", + "alism" : "al", + "iveness" : "ive", + "fulness" : "ful", + "ousness" : "ous", + "aliti" : "al", + "iviti" : "ive", + "biliti" : "ble", + "logi" : "log" + }, + + step3list = { + "icate" : "ic", + "ative" : "", + "alize" : "al", + "iciti" : "ic", + "ical" : "ic", + "ful" : "", + "ness" : "" + }, + + c = "[^aeiou]", // consonant + v = "[aeiouy]", // vowel + C = c + "[^aeiouy]*", // consonant sequence + V = v + "[aeiou]*", // vowel sequence + + mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0 + meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1 + mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1 + s_v = "^(" + C + ")?" + v; // vowel in stem + + var re_mgr0 = new RegExp(mgr0); + var re_mgr1 = new RegExp(mgr1); + var re_meq1 = new RegExp(meq1); + var re_s_v = new RegExp(s_v); + + var re_1a = /^(.+?)(ss|i)es$/; + var re2_1a = /^(.+?)([^s])s$/; + var re_1b = /^(.+?)eed$/; + var re2_1b = /^(.+?)(ed|ing)$/; + var re_1b_2 = /.$/; + var re2_1b_2 = /(at|bl|iz)$/; + var re3_1b_2 = new RegExp("([^aeiouylsz])\\1$"); + var re4_1b_2 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var re_1c = /^(.+?[^aeiou])y$/; + var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + + var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + + var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + var re2_4 = /^(.+?)(s|t)(ion)$/; + + var re_5 = /^(.+?)e$/; + var re_5_1 = /ll$/; + var re3_5 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var porterStemmer = function porterStemmer(w) { + var stem, + suffix, + firstch, + re, + re2, + re3, + re4; + + if (w.length < 3) { return w; } + + firstch = w.substr(0,1); + if (firstch == "y") { + w = firstch.toUpperCase() + w.substr(1); + } + + // Step 1a + re = re_1a + re2 = re2_1a; + + if (re.test(w)) { w = w.replace(re,"$1$2"); } + else if (re2.test(w)) { w = w.replace(re2,"$1$2"); } + + // Step 1b + re = re_1b; + re2 = re2_1b; + if (re.test(w)) { + var fp = re.exec(w); + re = re_mgr0; + if (re.test(fp[1])) { + re = re_1b_2; + w = w.replace(re,""); + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = re_s_v; + if (re2.test(stem)) { + w = stem; + re2 = re2_1b_2; + re3 = re3_1b_2; + re4 = re4_1b_2; + if (re2.test(w)) { w = w + "e"; } + else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,""); } + else if (re4.test(w)) { w = w + "e"; } + } + } + + // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say) + re = re_1c; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem + "i"; + } + + // Step 2 + re = re_2; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step2list[suffix]; + } + } + + // Step 3 + re = re_3; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step3list[suffix]; + } + } + + // Step 4 + re = re_4; + re2 = re2_4; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + if (re.test(stem)) { + w = stem; + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = re_mgr1; + if (re2.test(stem)) { + w = stem; + } + } + + // Step 5 + re = re_5; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + re2 = re_meq1; + re3 = re3_5; + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) { + w = stem; + } + } + + re = re_5_1; + re2 = re_mgr1; + if (re.test(w) && re2.test(w)) { + re = re_1b_2; + w = w.replace(re,""); + } + + // and turn initial Y back to y + + if (firstch == "y") { + w = firstch.toLowerCase() + w.substr(1); + } + + return w; + }; + + return function (token) { + return token.update(porterStemmer); + } +})(); + +lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer') +/*! + * lunr.stopWordFilter + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.generateStopWordFilter builds a stopWordFilter function from the provided + * list of stop words. + * + * The built in lunr.stopWordFilter is built using this generator and can be used + * to generate custom stopWordFilters for applications or non English languages. + * + * @function + * @param {Array} token The token to pass through the filter + * @returns {lunr.PipelineFunction} + * @see lunr.Pipeline + * @see lunr.stopWordFilter + */ +lunr.generateStopWordFilter = function (stopWords) { + var words = stopWords.reduce(function (memo, stopWord) { + memo[stopWord] = stopWord + return memo + }, {}) + + return function (token) { + if (token && words[token.toString()] !== token.toString()) return token + } +} + +/** + * lunr.stopWordFilter is an English language stop word list filter, any words + * contained in the list will not be passed through the filter. + * + * This is intended to be used in the Pipeline. If the token does not pass the + * filter then undefined will be returned. + * + * @function + * @implements {lunr.PipelineFunction} + * @params {lunr.Token} token - A token to check for being a stop word. + * @returns {lunr.Token} + * @see {@link lunr.Pipeline} + */ +lunr.stopWordFilter = lunr.generateStopWordFilter([ + 'a', + 'able', + 'about', + 'across', + 'after', + 'all', + 'almost', + 'also', + 'am', + 'among', + 'an', + 'and', + 'any', + 'are', + 'as', + 'at', + 'be', + 'because', + 'been', + 'but', + 'by', + 'can', + 'cannot', + 'could', + 'dear', + 'did', + 'do', + 'does', + 'either', + 'else', + 'ever', + 'every', + 'for', + 'from', + 'get', + 'got', + 'had', + 'has', + 'have', + 'he', + 'her', + 'hers', + 'him', + 'his', + 'how', + 'however', + 'i', + 'if', + 'in', + 'into', + 'is', + 'it', + 'its', + 'just', + 'least', + 'let', + 'like', + 'likely', + 'may', + 'me', + 'might', + 'most', + 'must', + 'my', + 'neither', + 'no', + 'nor', + 'not', + 'of', + 'off', + 'often', + 'on', + 'only', + 'or', + 'other', + 'our', + 'own', + 'rather', + 'said', + 'say', + 'says', + 'she', + 'should', + 'since', + 'so', + 'some', + 'than', + 'that', + 'the', + 'their', + 'them', + 'then', + 'there', + 'these', + 'they', + 'this', + 'tis', + 'to', + 'too', + 'twas', + 'us', + 'wants', + 'was', + 'we', + 'were', + 'what', + 'when', + 'where', + 'which', + 'while', + 'who', + 'whom', + 'why', + 'will', + 'with', + 'would', + 'yet', + 'you', + 'your' +]) + +lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter') +/*! + * lunr.trimmer + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.trimmer is a pipeline function for trimming non word + * characters from the beginning and end of tokens before they + * enter the index. + * + * This implementation may not work correctly for non latin + * characters and should either be removed or adapted for use + * with languages with non-latin characters. + * + * @static + * @implements {lunr.PipelineFunction} + * @param {lunr.Token} token The token to pass through the filter + * @returns {lunr.Token} + * @see lunr.Pipeline + */ +lunr.trimmer = function (token) { + return token.update(function (s) { + return s.replace(/^\W+/, '').replace(/\W+$/, '') + }) +} + +lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer') +/*! + * lunr.TokenSet + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A token set is used to store the unique list of all tokens + * within an index. Token sets are also used to represent an + * incoming query to the index, this query token set and index + * token set are then intersected to find which tokens to look + * up in the inverted index. + * + * A token set can hold multiple tokens, as in the case of the + * index token set, or it can hold a single token as in the + * case of a simple query token set. + * + * Additionally token sets are used to perform wildcard matching. + * Leading, contained and trailing wildcards are supported, and + * from this edit distance matching can also be provided. + * + * Token sets are implemented as a minimal finite state automata, + * where both common prefixes and suffixes are shared between tokens. + * This helps to reduce the space used for storing the token set. + * + * @constructor + */ +lunr.TokenSet = function () { + this.final = false + this.edges = {} + this.id = lunr.TokenSet._nextId + lunr.TokenSet._nextId += 1 +} + +/** + * Keeps track of the next, auto increment, identifier to assign + * to a new tokenSet. + * + * TokenSets require a unique identifier to be correctly minimised. + * + * @private + */ +lunr.TokenSet._nextId = 1 + +/** + * Creates a TokenSet instance from the given sorted array of words. + * + * @param {String[]} arr - A sorted array of strings to create the set from. + * @returns {lunr.TokenSet} + * @throws Will throw an error if the input array is not sorted. + */ +lunr.TokenSet.fromArray = function (arr) { + var builder = new lunr.TokenSet.Builder + + for (var i = 0, len = arr.length; i < len; i++) { + builder.insert(arr[i]) + } + + builder.finish() + return builder.root +} + +/** + * Creates a token set from a query clause. + * + * @private + * @param {Object} clause - A single clause from lunr.Query. + * @param {string} clause.term - The query clause term. + * @param {number} [clause.editDistance] - The optional edit distance for the term. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.fromClause = function (clause) { + if ('editDistance' in clause) { + return lunr.TokenSet.fromFuzzyString(clause.term, clause.editDistance) + } else { + return lunr.TokenSet.fromString(clause.term) + } +} + +/** + * Creates a token set representing a single string with a specified + * edit distance. + * + * Insertions, deletions, substitutions and transpositions are each + * treated as an edit distance of 1. + * + * Increasing the allowed edit distance will have a dramatic impact + * on the performance of both creating and intersecting these TokenSets. + * It is advised to keep the edit distance less than 3. + * + * @param {string} str - The string to create the token set from. + * @param {number} editDistance - The allowed edit distance to match. + * @returns {lunr.Vector} + */ +lunr.TokenSet.fromFuzzyString = function (str, editDistance) { + var root = new lunr.TokenSet + + var stack = [{ + node: root, + editsRemaining: editDistance, + str: str + }] + + while (stack.length) { + var frame = stack.pop() + + // no edit + if (frame.str.length > 0) { + var char = frame.str.charAt(0), + noEditNode + + if (char in frame.node.edges) { + noEditNode = frame.node.edges[char] + } else { + noEditNode = new lunr.TokenSet + frame.node.edges[char] = noEditNode + } + + if (frame.str.length == 1) { + noEditNode.final = true + } + + stack.push({ + node: noEditNode, + editsRemaining: frame.editsRemaining, + str: frame.str.slice(1) + }) + } + + if (frame.editsRemaining == 0) { + continue + } + + // insertion + if ("*" in frame.node.edges) { + var insertionNode = frame.node.edges["*"] + } else { + var insertionNode = new lunr.TokenSet + frame.node.edges["*"] = insertionNode + } + + if (frame.str.length == 0) { + insertionNode.final = true + } + + stack.push({ + node: insertionNode, + editsRemaining: frame.editsRemaining - 1, + str: frame.str + }) + + // deletion + // can only do a deletion if we have enough edits remaining + // and if there are characters left to delete in the string + if (frame.str.length > 1) { + stack.push({ + node: frame.node, + editsRemaining: frame.editsRemaining - 1, + str: frame.str.slice(1) + }) + } + + // deletion + // just removing the last character from the str + if (frame.str.length == 1) { + frame.node.final = true + } + + // substitution + // can only do a substitution if we have enough edits remaining + // and if there are characters left to substitute + if (frame.str.length >= 1) { + if ("*" in frame.node.edges) { + var substitutionNode = frame.node.edges["*"] + } else { + var substitutionNode = new lunr.TokenSet + frame.node.edges["*"] = substitutionNode + } + + if (frame.str.length == 1) { + substitutionNode.final = true + } + + stack.push({ + node: substitutionNode, + editsRemaining: frame.editsRemaining - 1, + str: frame.str.slice(1) + }) + } + + // transposition + // can only do a transposition if there are edits remaining + // and there are enough characters to transpose + if (frame.str.length > 1) { + var charA = frame.str.charAt(0), + charB = frame.str.charAt(1), + transposeNode + + if (charB in frame.node.edges) { + transposeNode = frame.node.edges[charB] + } else { + transposeNode = new lunr.TokenSet + frame.node.edges[charB] = transposeNode + } + + if (frame.str.length == 1) { + transposeNode.final = true + } + + stack.push({ + node: transposeNode, + editsRemaining: frame.editsRemaining - 1, + str: charA + frame.str.slice(2) + }) + } + } + + return root +} + +/** + * Creates a TokenSet from a string. + * + * The string may contain one or more wildcard characters (*) + * that will allow wildcard matching when intersecting with + * another TokenSet. + * + * @param {string} str - The string to create a TokenSet from. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.fromString = function (str) { + var node = new lunr.TokenSet, + root = node + + /* + * Iterates through all characters within the passed string + * appending a node for each character. + * + * When a wildcard character is found then a self + * referencing edge is introduced to continually match + * any number of any characters. + */ + for (var i = 0, len = str.length; i < len; i++) { + var char = str[i], + final = (i == len - 1) + + if (char == "*") { + node.edges[char] = node + node.final = final + + } else { + var next = new lunr.TokenSet + next.final = final + + node.edges[char] = next + node = next + } + } + + return root +} + +/** + * Converts this TokenSet into an array of strings + * contained within the TokenSet. + * + * This is not intended to be used on a TokenSet that + * contains wildcards, in these cases the results are + * undefined and are likely to cause an infinite loop. + * + * @returns {string[]} + */ +lunr.TokenSet.prototype.toArray = function () { + var words = [] + + var stack = [{ + prefix: "", + node: this + }] + + while (stack.length) { + var frame = stack.pop(), + edges = Object.keys(frame.node.edges), + len = edges.length + + if (frame.node.final) { + /* In Safari, at this point the prefix is sometimes corrupted, see: + * https://github.com/olivernn/lunr.js/issues/279 Calling any + * String.prototype method forces Safari to "cast" this string to what + * it's supposed to be, fixing the bug. */ + frame.prefix.charAt(0) + words.push(frame.prefix) + } + + for (var i = 0; i < len; i++) { + var edge = edges[i] + + stack.push({ + prefix: frame.prefix.concat(edge), + node: frame.node.edges[edge] + }) + } + } + + return words +} + +/** + * Generates a string representation of a TokenSet. + * + * This is intended to allow TokenSets to be used as keys + * in objects, largely to aid the construction and minimisation + * of a TokenSet. As such it is not designed to be a human + * friendly representation of the TokenSet. + * + * @returns {string} + */ +lunr.TokenSet.prototype.toString = function () { + // NOTE: Using Object.keys here as this.edges is very likely + // to enter 'hash-mode' with many keys being added + // + // avoiding a for-in loop here as it leads to the function + // being de-optimised (at least in V8). From some simple + // benchmarks the performance is comparable, but allowing + // V8 to optimize may mean easy performance wins in the future. + + if (this._str) { + return this._str + } + + var str = this.final ? '1' : '0', + labels = Object.keys(this.edges).sort(), + len = labels.length + + for (var i = 0; i < len; i++) { + var label = labels[i], + node = this.edges[label] + + str = str + label + node.id + } + + return str +} + +/** + * Returns a new TokenSet that is the intersection of + * this TokenSet and the passed TokenSet. + * + * This intersection will take into account any wildcards + * contained within the TokenSet. + * + * @param {lunr.TokenSet} b - An other TokenSet to intersect with. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.prototype.intersect = function (b) { + var output = new lunr.TokenSet, + frame = undefined + + var stack = [{ + qNode: b, + output: output, + node: this + }] + + while (stack.length) { + frame = stack.pop() + + // NOTE: As with the #toString method, we are using + // Object.keys and a for loop instead of a for-in loop + // as both of these objects enter 'hash' mode, causing + // the function to be de-optimised in V8 + var qEdges = Object.keys(frame.qNode.edges), + qLen = qEdges.length, + nEdges = Object.keys(frame.node.edges), + nLen = nEdges.length + + for (var q = 0; q < qLen; q++) { + var qEdge = qEdges[q] + + for (var n = 0; n < nLen; n++) { + var nEdge = nEdges[n] + + if (nEdge == qEdge || qEdge == '*') { + var node = frame.node.edges[nEdge], + qNode = frame.qNode.edges[qEdge], + final = node.final && qNode.final, + next = undefined + + if (nEdge in frame.output.edges) { + // an edge already exists for this character + // no need to create a new node, just set the finality + // bit unless this node is already final + next = frame.output.edges[nEdge] + next.final = next.final || final + + } else { + // no edge exists yet, must create one + // set the finality bit and insert it + // into the output + next = new lunr.TokenSet + next.final = final + frame.output.edges[nEdge] = next + } + + stack.push({ + qNode: qNode, + output: next, + node: node + }) + } + } + } + } + + return output +} +lunr.TokenSet.Builder = function () { + this.previousWord = "" + this.root = new lunr.TokenSet + this.uncheckedNodes = [] + this.minimizedNodes = {} +} + +lunr.TokenSet.Builder.prototype.insert = function (word) { + var node, + commonPrefix = 0 + + if (word < this.previousWord) { + throw new Error ("Out of order word insertion") + } + + for (var i = 0; i < word.length && i < this.previousWord.length; i++) { + if (word[i] != this.previousWord[i]) break + commonPrefix++ + } + + this.minimize(commonPrefix) + + if (this.uncheckedNodes.length == 0) { + node = this.root + } else { + node = this.uncheckedNodes[this.uncheckedNodes.length - 1].child + } + + for (var i = commonPrefix; i < word.length; i++) { + var nextNode = new lunr.TokenSet, + char = word[i] + + node.edges[char] = nextNode + + this.uncheckedNodes.push({ + parent: node, + char: char, + child: nextNode + }) + + node = nextNode + } + + node.final = true + this.previousWord = word +} + +lunr.TokenSet.Builder.prototype.finish = function () { + this.minimize(0) +} + +lunr.TokenSet.Builder.prototype.minimize = function (downTo) { + for (var i = this.uncheckedNodes.length - 1; i >= downTo; i--) { + var node = this.uncheckedNodes[i], + childKey = node.child.toString() + + if (childKey in this.minimizedNodes) { + node.parent.edges[node.char] = this.minimizedNodes[childKey] + } else { + // Cache the key for this node since + // we know it can't change anymore + node.child._str = childKey + + this.minimizedNodes[childKey] = node.child + } + + this.uncheckedNodes.pop() + } +} +/*! + * lunr.Index + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * An index contains the built index of all documents and provides a query interface + * to the index. + * + * Usually instances of lunr.Index will not be created using this constructor, instead + * lunr.Builder should be used to construct new indexes, or lunr.Index.load should be + * used to load previously built and serialized indexes. + * + * @constructor + * @param {Object} attrs - The attributes of the built search index. + * @param {Object} attrs.invertedIndex - An index of term/field to document reference. + * @param {Object} attrs.fieldVectors - Field vectors + * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens. + * @param {string[]} attrs.fields - The names of indexed document fields. + * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms. + */ +lunr.Index = function (attrs) { + this.invertedIndex = attrs.invertedIndex + this.fieldVectors = attrs.fieldVectors + this.tokenSet = attrs.tokenSet + this.fields = attrs.fields + this.pipeline = attrs.pipeline +} + +/** + * A result contains details of a document matching a search query. + * @typedef {Object} lunr.Index~Result + * @property {string} ref - The reference of the document this result represents. + * @property {number} score - A number between 0 and 1 representing how similar this document is to the query. + * @property {lunr.MatchData} matchData - Contains metadata about this match including which term(s) caused the match. + */ + +/** + * Although lunr provides the ability to create queries using lunr.Query, it also provides a simple + * query language which itself is parsed into an instance of lunr.Query. + * + * For programmatically building queries it is advised to directly use lunr.Query, the query language + * is best used for human entered text rather than program generated text. + * + * At its simplest queries can just be a single term, e.g. `hello`, multiple terms are also supported + * and will be combined with OR, e.g `hello world` will match documents that contain either 'hello' + * or 'world', though those that contain both will rank higher in the results. + * + * Wildcards can be included in terms to match one or more unspecified characters, these wildcards can + * be inserted anywhere within the term, and more than one wildcard can exist in a single term. Adding + * wildcards will increase the number of documents that will be found but can also have a negative + * impact on query performance, especially with wildcards at the beginning of a term. + * + * Terms can be restricted to specific fields, e.g. `title:hello`, only documents with the term + * hello in the title field will match this query. Using a field not present in the index will lead + * to an error being thrown. + * + * Modifiers can also be added to terms, lunr supports edit distance and boost modifiers on terms. A term + * boost will make documents matching that term score higher, e.g. `foo^5`. Edit distance is also supported + * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2. + * Avoid large values for edit distance to improve query performance. + * + * Each term also supports a presence modifier. By default a term's presence in document is optional, however + * this can be changed to either required or prohibited. For a term's presence to be required in a document the + * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and + * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not + * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'. + * + * To escape special characters the backslash character '\' can be used, this allows searches to include + * characters that would normally be considered modifiers, e.g. `foo\~2` will search for a term "foo~2" instead + * of attempting to apply a boost of 2 to the search term "foo". + * + * @typedef {string} lunr.Index~QueryString + * @example Simple single term query + * hello + * @example Multiple term query + * hello world + * @example term scoped to a field + * title:hello + * @example term with a boost of 10 + * hello^10 + * @example term with an edit distance of 2 + * hello~2 + * @example terms with presence modifiers + * -foo +bar baz + */ + +/** + * Performs a search against the index using lunr query syntax. + * + * Results will be returned sorted by their score, the most relevant results + * will be returned first. For details on how the score is calculated, please see + * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}. + * + * For more programmatic querying use lunr.Index#query. + * + * @param {lunr.Index~QueryString} queryString - A string containing a lunr query. + * @throws {lunr.QueryParseError} If the passed query string cannot be parsed. + * @returns {lunr.Index~Result[]} + */ +lunr.Index.prototype.search = function (queryString) { + return this.query(function (query) { + var parser = new lunr.QueryParser(queryString, query) + parser.parse() + }) +} + +/** + * A query builder callback provides a query object to be used to express + * the query to perform on the index. + * + * @callback lunr.Index~queryBuilder + * @param {lunr.Query} query - The query object to build up. + * @this lunr.Query + */ + +/** + * Performs a query against the index using the yielded lunr.Query object. + * + * If performing programmatic queries against the index, this method is preferred + * over lunr.Index#search so as to avoid the additional query parsing overhead. + * + * A query object is yielded to the supplied function which should be used to + * express the query to be run against the index. + * + * Note that although this function takes a callback parameter it is _not_ an + * asynchronous operation, the callback is just yielded a query object to be + * customized. + * + * @param {lunr.Index~queryBuilder} fn - A function that is used to build the query. + * @returns {lunr.Index~Result[]} + */ +lunr.Index.prototype.query = function (fn) { + // for each query clause + // * process terms + // * expand terms from token set + // * find matching documents and metadata + // * get document vectors + // * score documents + + var query = new lunr.Query(this.fields), + matchingFields = Object.create(null), + queryVectors = Object.create(null), + termFieldCache = Object.create(null), + requiredMatches = Object.create(null), + prohibitedMatches = Object.create(null) + + /* + * To support field level boosts a query vector is created per + * field. An empty vector is eagerly created to support negated + * queries. + */ + for (var i = 0; i < this.fields.length; i++) { + queryVectors[this.fields[i]] = new lunr.Vector + } + + fn.call(query, query) + + for (var i = 0; i < query.clauses.length; i++) { + /* + * Unless the pipeline has been disabled for this term, which is + * the case for terms with wildcards, we need to pass the clause + * term through the search pipeline. A pipeline returns an array + * of processed terms. Pipeline functions may expand the passed + * term, which means we may end up performing multiple index lookups + * for a single query term. + */ + var clause = query.clauses[i], + terms = null, + clauseMatches = lunr.Set.empty + + if (clause.usePipeline) { + terms = this.pipeline.runString(clause.term, { + fields: clause.fields + }) + } else { + terms = [clause.term] + } + + for (var m = 0; m < terms.length; m++) { + var term = terms[m] + + /* + * Each term returned from the pipeline needs to use the same query + * clause object, e.g. the same boost and or edit distance. The + * simplest way to do this is to re-use the clause object but mutate + * its term property. + */ + clause.term = term + + /* + * From the term in the clause we create a token set which will then + * be used to intersect the indexes token set to get a list of terms + * to lookup in the inverted index + */ + var termTokenSet = lunr.TokenSet.fromClause(clause), + expandedTerms = this.tokenSet.intersect(termTokenSet).toArray() + + /* + * If a term marked as required does not exist in the tokenSet it is + * impossible for the search to return any matches. We set all the field + * scoped required matches set to empty and stop examining any further + * clauses. + */ + if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) { + for (var k = 0; k < clause.fields.length; k++) { + var field = clause.fields[k] + requiredMatches[field] = lunr.Set.empty + } + + break + } + + for (var j = 0; j < expandedTerms.length; j++) { + /* + * For each term get the posting and termIndex, this is required for + * building the query vector. + */ + var expandedTerm = expandedTerms[j], + posting = this.invertedIndex[expandedTerm], + termIndex = posting._index + + for (var k = 0; k < clause.fields.length; k++) { + /* + * For each field that this query term is scoped by (by default + * all fields are in scope) we need to get all the document refs + * that have this term in that field. + * + * The posting is the entry in the invertedIndex for the matching + * term from above. + */ + var field = clause.fields[k], + fieldPosting = posting[field], + matchingDocumentRefs = Object.keys(fieldPosting), + termField = expandedTerm + "/" + field, + matchingDocumentsSet = new lunr.Set(matchingDocumentRefs) + + /* + * if the presence of this term is required ensure that the matching + * documents are added to the set of required matches for this clause. + * + */ + if (clause.presence == lunr.Query.presence.REQUIRED) { + clauseMatches = clauseMatches.union(matchingDocumentsSet) + + if (requiredMatches[field] === undefined) { + requiredMatches[field] = lunr.Set.complete + } + } + + /* + * if the presence of this term is prohibited ensure that the matching + * documents are added to the set of prohibited matches for this field, + * creating that set if it does not yet exist. + */ + if (clause.presence == lunr.Query.presence.PROHIBITED) { + if (prohibitedMatches[field] === undefined) { + prohibitedMatches[field] = lunr.Set.empty + } + + prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet) + + /* + * Prohibited matches should not be part of the query vector used for + * similarity scoring and no metadata should be extracted so we continue + * to the next field + */ + continue + } + + /* + * The query field vector is populated using the termIndex found for + * the term and a unit value with the appropriate boost applied. + * Using upsert because there could already be an entry in the vector + * for the term we are working with. In that case we just add the scores + * together. + */ + queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b }) + + /** + * If we've already seen this term, field combo then we've already collected + * the matching documents and metadata, no need to go through all that again + */ + if (termFieldCache[termField]) { + continue + } + + for (var l = 0; l < matchingDocumentRefs.length; l++) { + /* + * All metadata for this term/field/document triple + * are then extracted and collected into an instance + * of lunr.MatchData ready to be returned in the query + * results + */ + var matchingDocumentRef = matchingDocumentRefs[l], + matchingFieldRef = new lunr.FieldRef (matchingDocumentRef, field), + metadata = fieldPosting[matchingDocumentRef], + fieldMatch + + if ((fieldMatch = matchingFields[matchingFieldRef]) === undefined) { + matchingFields[matchingFieldRef] = new lunr.MatchData (expandedTerm, field, metadata) + } else { + fieldMatch.add(expandedTerm, field, metadata) + } + + } + + termFieldCache[termField] = true + } + } + } + + /** + * If the presence was required we need to update the requiredMatches field sets. + * We do this after all fields for the term have collected their matches because + * the clause terms presence is required in _any_ of the fields not _all_ of the + * fields. + */ + if (clause.presence === lunr.Query.presence.REQUIRED) { + for (var k = 0; k < clause.fields.length; k++) { + var field = clause.fields[k] + requiredMatches[field] = requiredMatches[field].intersect(clauseMatches) + } + } + } + + /** + * Need to combine the field scoped required and prohibited + * matching documents into a global set of required and prohibited + * matches + */ + var allRequiredMatches = lunr.Set.complete, + allProhibitedMatches = lunr.Set.empty + + for (var i = 0; i < this.fields.length; i++) { + var field = this.fields[i] + + if (requiredMatches[field]) { + allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field]) + } + + if (prohibitedMatches[field]) { + allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field]) + } + } + + var matchingFieldRefs = Object.keys(matchingFields), + results = [], + matches = Object.create(null) + + /* + * If the query is negated (contains only prohibited terms) + * we need to get _all_ fieldRefs currently existing in the + * index. This is only done when we know that the query is + * entirely prohibited terms to avoid any cost of getting all + * fieldRefs unnecessarily. + * + * Additionally, blank MatchData must be created to correctly + * populate the results. + */ + if (query.isNegated()) { + matchingFieldRefs = Object.keys(this.fieldVectors) + + for (var i = 0; i < matchingFieldRefs.length; i++) { + var matchingFieldRef = matchingFieldRefs[i] + var fieldRef = lunr.FieldRef.fromString(matchingFieldRef) + matchingFields[matchingFieldRef] = new lunr.MatchData + } + } + + for (var i = 0; i < matchingFieldRefs.length; i++) { + /* + * Currently we have document fields that match the query, but we + * need to return documents. The matchData and scores are combined + * from multiple fields belonging to the same document. + * + * Scores are calculated by field, using the query vectors created + * above, and combined into a final document score using addition. + */ + var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]), + docRef = fieldRef.docRef + + if (!allRequiredMatches.contains(docRef)) { + continue + } + + if (allProhibitedMatches.contains(docRef)) { + continue + } + + var fieldVector = this.fieldVectors[fieldRef], + score = queryVectors[fieldRef.fieldName].similarity(fieldVector), + docMatch + + if ((docMatch = matches[docRef]) !== undefined) { + docMatch.score += score + docMatch.matchData.combine(matchingFields[fieldRef]) + } else { + var match = { + ref: docRef, + score: score, + matchData: matchingFields[fieldRef] + } + matches[docRef] = match + results.push(match) + } + } + + /* + * Sort the results objects by score, highest first. + */ + return results.sort(function (a, b) { + return b.score - a.score + }) +} + +/** + * Prepares the index for JSON serialization. + * + * The schema for this JSON blob will be described in a + * separate JSON schema file. + * + * @returns {Object} + */ +lunr.Index.prototype.toJSON = function () { + var invertedIndex = Object.keys(this.invertedIndex) + .sort() + .map(function (term) { + return [term, this.invertedIndex[term]] + }, this) + + var fieldVectors = Object.keys(this.fieldVectors) + .map(function (ref) { + return [ref, this.fieldVectors[ref].toJSON()] + }, this) + + return { + version: lunr.version, + fields: this.fields, + fieldVectors: fieldVectors, + invertedIndex: invertedIndex, + pipeline: this.pipeline.toJSON() + } +} + +/** + * Loads a previously serialized lunr.Index + * + * @param {Object} serializedIndex - A previously serialized lunr.Index + * @returns {lunr.Index} + */ +lunr.Index.load = function (serializedIndex) { + var attrs = {}, + fieldVectors = {}, + serializedVectors = serializedIndex.fieldVectors, + invertedIndex = Object.create(null), + serializedInvertedIndex = serializedIndex.invertedIndex, + tokenSetBuilder = new lunr.TokenSet.Builder, + pipeline = lunr.Pipeline.load(serializedIndex.pipeline) + + if (serializedIndex.version != lunr.version) { + lunr.utils.warn("Version mismatch when loading serialised index. Current version of lunr '" + lunr.version + "' does not match serialized index '" + serializedIndex.version + "'") + } + + for (var i = 0; i < serializedVectors.length; i++) { + var tuple = serializedVectors[i], + ref = tuple[0], + elements = tuple[1] + + fieldVectors[ref] = new lunr.Vector(elements) + } + + for (var i = 0; i < serializedInvertedIndex.length; i++) { + var tuple = serializedInvertedIndex[i], + term = tuple[0], + posting = tuple[1] + + tokenSetBuilder.insert(term) + invertedIndex[term] = posting + } + + tokenSetBuilder.finish() + + attrs.fields = serializedIndex.fields + + attrs.fieldVectors = fieldVectors + attrs.invertedIndex = invertedIndex + attrs.tokenSet = tokenSetBuilder.root + attrs.pipeline = pipeline + + return new lunr.Index(attrs) +} +/*! + * lunr.Builder + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.Builder performs indexing on a set of documents and + * returns instances of lunr.Index ready for querying. + * + * All configuration of the index is done via the builder, the + * fields to index, the document reference, the text processing + * pipeline and document scoring parameters are all set on the + * builder before indexing. + * + * @constructor + * @property {string} _ref - Internal reference to the document reference field. + * @property {string[]} _fields - Internal reference to the document fields to index. + * @property {object} invertedIndex - The inverted index maps terms to document fields. + * @property {object} documentTermFrequencies - Keeps track of document term frequencies. + * @property {object} documentLengths - Keeps track of the length of documents added to the index. + * @property {lunr.tokenizer} tokenizer - Function for splitting strings into tokens for indexing. + * @property {lunr.Pipeline} pipeline - The pipeline performs text processing on tokens before indexing. + * @property {lunr.Pipeline} searchPipeline - A pipeline for processing search terms before querying the index. + * @property {number} documentCount - Keeps track of the total number of documents indexed. + * @property {number} _b - A parameter to control field length normalization, setting this to 0 disabled normalization, 1 fully normalizes field lengths, the default value is 0.75. + * @property {number} _k1 - A parameter to control how quickly an increase in term frequency results in term frequency saturation, the default value is 1.2. + * @property {number} termIndex - A counter incremented for each unique term, used to identify a terms position in the vector space. + * @property {array} metadataWhitelist - A list of metadata keys that have been whitelisted for entry in the index. + */ +lunr.Builder = function () { + this._ref = "id" + this._fields = Object.create(null) + this._documents = Object.create(null) + this.invertedIndex = Object.create(null) + this.fieldTermFrequencies = {} + this.fieldLengths = {} + this.tokenizer = lunr.tokenizer + this.pipeline = new lunr.Pipeline + this.searchPipeline = new lunr.Pipeline + this.documentCount = 0 + this._b = 0.75 + this._k1 = 1.2 + this.termIndex = 0 + this.metadataWhitelist = [] +} + +/** + * Sets the document field used as the document reference. Every document must have this field. + * The type of this field in the document should be a string, if it is not a string it will be + * coerced into a string by calling toString. + * + * The default ref is 'id'. + * + * The ref should _not_ be changed during indexing, it should be set before any documents are + * added to the index. Changing it during indexing can lead to inconsistent results. + * + * @param {string} ref - The name of the reference field in the document. + */ +lunr.Builder.prototype.ref = function (ref) { + this._ref = ref +} + +/** + * A function that is used to extract a field from a document. + * + * Lunr expects a field to be at the top level of a document, if however the field + * is deeply nested within a document an extractor function can be used to extract + * the right field for indexing. + * + * @callback fieldExtractor + * @param {object} doc - The document being added to the index. + * @returns {?(string|object|object[])} obj - The object that will be indexed for this field. + * @example Extracting a nested field + * function (doc) { return doc.nested.field } + */ + +/** + * Adds a field to the list of document fields that will be indexed. Every document being + * indexed should have this field. Null values for this field in indexed documents will + * not cause errors but will limit the chance of that document being retrieved by searches. + * + * All fields should be added before adding documents to the index. Adding fields after + * a document has been indexed will have no effect on already indexed documents. + * + * Fields can be boosted at build time. This allows terms within that field to have more + * importance when ranking search results. Use a field boost to specify that matches within + * one field are more important than other fields. + * + * @param {string} fieldName - The name of a field to index in all documents. + * @param {object} attributes - Optional attributes associated with this field. + * @param {number} [attributes.boost=1] - Boost applied to all terms within this field. + * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document. + * @throws {RangeError} fieldName cannot contain unsupported characters '/' + */ +lunr.Builder.prototype.field = function (fieldName, attributes) { + if (/\//.test(fieldName)) { + throw new RangeError ("Field '" + fieldName + "' contains illegal character '/'") + } + + this._fields[fieldName] = attributes || {} +} + +/** + * A parameter to tune the amount of field length normalisation that is applied when + * calculating relevance scores. A value of 0 will completely disable any normalisation + * and a value of 1 will fully normalise field lengths. The default is 0.75. Values of b + * will be clamped to the range 0 - 1. + * + * @param {number} number - The value to set for this tuning parameter. + */ +lunr.Builder.prototype.b = function (number) { + if (number < 0) { + this._b = 0 + } else if (number > 1) { + this._b = 1 + } else { + this._b = number + } +} + +/** + * A parameter that controls the speed at which a rise in term frequency results in term + * frequency saturation. The default value is 1.2. Setting this to a higher value will give + * slower saturation levels, a lower value will result in quicker saturation. + * + * @param {number} number - The value to set for this tuning parameter. + */ +lunr.Builder.prototype.k1 = function (number) { + this._k1 = number +} + +/** + * Adds a document to the index. + * + * Before adding fields to the index the index should have been fully setup, with the document + * ref and all fields to index already having been specified. + * + * The document must have a field name as specified by the ref (by default this is 'id') and + * it should have all fields defined for indexing, though null or undefined values will not + * cause errors. + * + * Entire documents can be boosted at build time. Applying a boost to a document indicates that + * this document should rank higher in search results than other documents. + * + * @param {object} doc - The document to add to the index. + * @param {object} attributes - Optional attributes associated with this document. + * @param {number} [attributes.boost=1] - Boost applied to all terms within this document. + */ +lunr.Builder.prototype.add = function (doc, attributes) { + var docRef = doc[this._ref], + fields = Object.keys(this._fields) + + this._documents[docRef] = attributes || {} + this.documentCount += 1 + + for (var i = 0; i < fields.length; i++) { + var fieldName = fields[i], + extractor = this._fields[fieldName].extractor, + field = extractor ? extractor(doc) : doc[fieldName], + tokens = this.tokenizer(field, { + fields: [fieldName] + }), + terms = this.pipeline.run(tokens), + fieldRef = new lunr.FieldRef (docRef, fieldName), + fieldTerms = Object.create(null) + + this.fieldTermFrequencies[fieldRef] = fieldTerms + this.fieldLengths[fieldRef] = 0 + + // store the length of this field for this document + this.fieldLengths[fieldRef] += terms.length + + // calculate term frequencies for this field + for (var j = 0; j < terms.length; j++) { + var term = terms[j] + + if (fieldTerms[term] == undefined) { + fieldTerms[term] = 0 + } + + fieldTerms[term] += 1 + + // add to inverted index + // create an initial posting if one doesn't exist + if (this.invertedIndex[term] == undefined) { + var posting = Object.create(null) + posting["_index"] = this.termIndex + this.termIndex += 1 + + for (var k = 0; k < fields.length; k++) { + posting[fields[k]] = Object.create(null) + } + + this.invertedIndex[term] = posting + } + + // add an entry for this term/fieldName/docRef to the invertedIndex + if (this.invertedIndex[term][fieldName][docRef] == undefined) { + this.invertedIndex[term][fieldName][docRef] = Object.create(null) + } + + // store all whitelisted metadata about this token in the + // inverted index + for (var l = 0; l < this.metadataWhitelist.length; l++) { + var metadataKey = this.metadataWhitelist[l], + metadata = term.metadata[metadataKey] + + if (this.invertedIndex[term][fieldName][docRef][metadataKey] == undefined) { + this.invertedIndex[term][fieldName][docRef][metadataKey] = [] + } + + this.invertedIndex[term][fieldName][docRef][metadataKey].push(metadata) + } + } + + } +} + +/** + * Calculates the average document length for this index + * + * @private + */ +lunr.Builder.prototype.calculateAverageFieldLengths = function () { + + var fieldRefs = Object.keys(this.fieldLengths), + numberOfFields = fieldRefs.length, + accumulator = {}, + documentsWithField = {} + + for (var i = 0; i < numberOfFields; i++) { + var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), + field = fieldRef.fieldName + + documentsWithField[field] || (documentsWithField[field] = 0) + documentsWithField[field] += 1 + + accumulator[field] || (accumulator[field] = 0) + accumulator[field] += this.fieldLengths[fieldRef] + } + + var fields = Object.keys(this._fields) + + for (var i = 0; i < fields.length; i++) { + var fieldName = fields[i] + accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName] + } + + this.averageFieldLength = accumulator +} + +/** + * Builds a vector space model of every document using lunr.Vector + * + * @private + */ +lunr.Builder.prototype.createFieldVectors = function () { + var fieldVectors = {}, + fieldRefs = Object.keys(this.fieldTermFrequencies), + fieldRefsLength = fieldRefs.length, + termIdfCache = Object.create(null) + + for (var i = 0; i < fieldRefsLength; i++) { + var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), + fieldName = fieldRef.fieldName, + fieldLength = this.fieldLengths[fieldRef], + fieldVector = new lunr.Vector, + termFrequencies = this.fieldTermFrequencies[fieldRef], + terms = Object.keys(termFrequencies), + termsLength = terms.length + + + var fieldBoost = this._fields[fieldName].boost || 1, + docBoost = this._documents[fieldRef.docRef].boost || 1 + + for (var j = 0; j < termsLength; j++) { + var term = terms[j], + tf = termFrequencies[term], + termIndex = this.invertedIndex[term]._index, + idf, score, scoreWithPrecision + + if (termIdfCache[term] === undefined) { + idf = lunr.idf(this.invertedIndex[term], this.documentCount) + termIdfCache[term] = idf + } else { + idf = termIdfCache[term] + } + + score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf) + score *= fieldBoost + score *= docBoost + scoreWithPrecision = Math.round(score * 1000) / 1000 + // Converts 1.23456789 to 1.234. + // Reducing the precision so that the vectors take up less + // space when serialised. Doing it now so that they behave + // the same before and after serialisation. Also, this is + // the fastest approach to reducing a number's precision in + // JavaScript. + + fieldVector.insert(termIndex, scoreWithPrecision) + } + + fieldVectors[fieldRef] = fieldVector + } + + this.fieldVectors = fieldVectors +} + +/** + * Creates a token set of all tokens in the index using lunr.TokenSet + * + * @private + */ +lunr.Builder.prototype.createTokenSet = function () { + this.tokenSet = lunr.TokenSet.fromArray( + Object.keys(this.invertedIndex).sort() + ) +} + +/** + * Builds the index, creating an instance of lunr.Index. + * + * This completes the indexing process and should only be called + * once all documents have been added to the index. + * + * @returns {lunr.Index} + */ +lunr.Builder.prototype.build = function () { + this.calculateAverageFieldLengths() + this.createFieldVectors() + this.createTokenSet() + + return new lunr.Index({ + invertedIndex: this.invertedIndex, + fieldVectors: this.fieldVectors, + tokenSet: this.tokenSet, + fields: Object.keys(this._fields), + pipeline: this.searchPipeline + }) +} + +/** + * Applies a plugin to the index builder. + * + * A plugin is a function that is called with the index builder as its context. + * Plugins can be used to customise or extend the behaviour of the index + * in some way. A plugin is just a function, that encapsulated the custom + * behaviour that should be applied when building the index. + * + * The plugin function will be called with the index builder as its argument, additional + * arguments can also be passed when calling use. The function will be called + * with the index builder as its context. + * + * @param {Function} plugin The plugin to apply. + */ +lunr.Builder.prototype.use = function (fn) { + var args = Array.prototype.slice.call(arguments, 1) + args.unshift(this) + fn.apply(this, args) +} +/** + * Contains and collects metadata about a matching document. + * A single instance of lunr.MatchData is returned as part of every + * lunr.Index~Result. + * + * @constructor + * @param {string} term - The term this match data is associated with + * @param {string} field - The field in which the term was found + * @param {object} metadata - The metadata recorded about this term in this field + * @property {object} metadata - A cloned collection of metadata associated with this document. + * @see {@link lunr.Index~Result} + */ +lunr.MatchData = function (term, field, metadata) { + var clonedMetadata = Object.create(null), + metadataKeys = Object.keys(metadata || {}) + + // Cloning the metadata to prevent the original + // being mutated during match data combination. + // Metadata is kept in an array within the inverted + // index so cloning the data can be done with + // Array#slice + for (var i = 0; i < metadataKeys.length; i++) { + var key = metadataKeys[i] + clonedMetadata[key] = metadata[key].slice() + } + + this.metadata = Object.create(null) + + if (term !== undefined) { + this.metadata[term] = Object.create(null) + this.metadata[term][field] = clonedMetadata + } +} + +/** + * An instance of lunr.MatchData will be created for every term that matches a + * document. However only one instance is required in a lunr.Index~Result. This + * method combines metadata from another instance of lunr.MatchData with this + * objects metadata. + * + * @param {lunr.MatchData} otherMatchData - Another instance of match data to merge with this one. + * @see {@link lunr.Index~Result} + */ +lunr.MatchData.prototype.combine = function (otherMatchData) { + var terms = Object.keys(otherMatchData.metadata) + + for (var i = 0; i < terms.length; i++) { + var term = terms[i], + fields = Object.keys(otherMatchData.metadata[term]) + + if (this.metadata[term] == undefined) { + this.metadata[term] = Object.create(null) + } + + for (var j = 0; j < fields.length; j++) { + var field = fields[j], + keys = Object.keys(otherMatchData.metadata[term][field]) + + if (this.metadata[term][field] == undefined) { + this.metadata[term][field] = Object.create(null) + } + + for (var k = 0; k < keys.length; k++) { + var key = keys[k] + + if (this.metadata[term][field][key] == undefined) { + this.metadata[term][field][key] = otherMatchData.metadata[term][field][key] + } else { + this.metadata[term][field][key] = this.metadata[term][field][key].concat(otherMatchData.metadata[term][field][key]) + } + + } + } + } +} + +/** + * Add metadata for a term/field pair to this instance of match data. + * + * @param {string} term - The term this match data is associated with + * @param {string} field - The field in which the term was found + * @param {object} metadata - The metadata recorded about this term in this field + */ +lunr.MatchData.prototype.add = function (term, field, metadata) { + if (!(term in this.metadata)) { + this.metadata[term] = Object.create(null) + this.metadata[term][field] = metadata + return + } + + if (!(field in this.metadata[term])) { + this.metadata[term][field] = metadata + return + } + + var metadataKeys = Object.keys(metadata) + + for (var i = 0; i < metadataKeys.length; i++) { + var key = metadataKeys[i] + + if (key in this.metadata[term][field]) { + this.metadata[term][field][key] = this.metadata[term][field][key].concat(metadata[key]) + } else { + this.metadata[term][field][key] = metadata[key] + } + } +} +/** + * A lunr.Query provides a programmatic way of defining queries to be performed + * against a {@link lunr.Index}. + * + * Prefer constructing a lunr.Query using the {@link lunr.Index#query} method + * so the query object is pre-initialized with the right index fields. + * + * @constructor + * @property {lunr.Query~Clause[]} clauses - An array of query clauses. + * @property {string[]} allFields - An array of all available fields in a lunr.Index. + */ +lunr.Query = function (allFields) { + this.clauses = [] + this.allFields = allFields +} + +/** + * Constants for indicating what kind of automatic wildcard insertion will be used when constructing a query clause. + * + * This allows wildcards to be added to the beginning and end of a term without having to manually do any string + * concatenation. + * + * The wildcard constants can be bitwise combined to select both leading and trailing wildcards. + * + * @constant + * @default + * @property {number} wildcard.NONE - The term will have no wildcards inserted, this is the default behaviour + * @property {number} wildcard.LEADING - Prepend the term with a wildcard, unless a leading wildcard already exists + * @property {number} wildcard.TRAILING - Append a wildcard to the term, unless a trailing wildcard already exists + * @see lunr.Query~Clause + * @see lunr.Query#clause + * @see lunr.Query#term + * @example query term with trailing wildcard + * query.term('foo', { wildcard: lunr.Query.wildcard.TRAILING }) + * @example query term with leading and trailing wildcard + * query.term('foo', { + * wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING + * }) + */ + +lunr.Query.wildcard = new String ("*") +lunr.Query.wildcard.NONE = 0 +lunr.Query.wildcard.LEADING = 1 +lunr.Query.wildcard.TRAILING = 2 + +/** + * Constants for indicating what kind of presence a term must have in matching documents. + * + * @constant + * @enum {number} + * @see lunr.Query~Clause + * @see lunr.Query#clause + * @see lunr.Query#term + * @example query term with required presence + * query.term('foo', { presence: lunr.Query.presence.REQUIRED }) + */ +lunr.Query.presence = { + /** + * Term's presence in a document is optional, this is the default value. + */ + OPTIONAL: 1, + + /** + * Term's presence in a document is required, documents that do not contain + * this term will not be returned. + */ + REQUIRED: 2, + + /** + * Term's presence in a document is prohibited, documents that do contain + * this term will not be returned. + */ + PROHIBITED: 3 +} + +/** + * A single clause in a {@link lunr.Query} contains a term and details on how to + * match that term against a {@link lunr.Index}. + * + * @typedef {Object} lunr.Query~Clause + * @property {string[]} fields - The fields in an index this clause should be matched against. + * @property {number} [boost=1] - Any boost that should be applied when matching this clause. + * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be. + * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline. + * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended. + * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents. + */ + +/** + * Adds a {@link lunr.Query~Clause} to this query. + * + * Unless the clause contains the fields to be matched all fields will be matched. In addition + * a default boost of 1 is applied to the clause. + * + * @param {lunr.Query~Clause} clause - The clause to add to this query. + * @see lunr.Query~Clause + * @returns {lunr.Query} + */ +lunr.Query.prototype.clause = function (clause) { + if (!('fields' in clause)) { + clause.fields = this.allFields + } + + if (!('boost' in clause)) { + clause.boost = 1 + } + + if (!('usePipeline' in clause)) { + clause.usePipeline = true + } + + if (!('wildcard' in clause)) { + clause.wildcard = lunr.Query.wildcard.NONE + } + + if ((clause.wildcard & lunr.Query.wildcard.LEADING) && (clause.term.charAt(0) != lunr.Query.wildcard)) { + clause.term = "*" + clause.term + } + + if ((clause.wildcard & lunr.Query.wildcard.TRAILING) && (clause.term.slice(-1) != lunr.Query.wildcard)) { + clause.term = "" + clause.term + "*" + } + + if (!('presence' in clause)) { + clause.presence = lunr.Query.presence.OPTIONAL + } + + this.clauses.push(clause) + + return this +} + +/** + * A negated query is one in which every clause has a presence of + * prohibited. These queries require some special processing to return + * the expected results. + * + * @returns boolean + */ +lunr.Query.prototype.isNegated = function () { + for (var i = 0; i < this.clauses.length; i++) { + if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) { + return false + } + } + + return true +} + +/** + * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause} + * to the list of clauses that make up this query. + * + * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion + * to a token or token-like string should be done before calling this method. + * + * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an + * array, each term in the array will share the same options. + * + * @param {object|object[]} term - The term(s) to add to the query. + * @param {object} [options] - Any additional properties to add to the query clause. + * @returns {lunr.Query} + * @see lunr.Query#clause + * @see lunr.Query~Clause + * @example adding a single term to a query + * query.term("foo") + * @example adding a single term to a query and specifying search fields, term boost and automatic trailing wildcard + * query.term("foo", { + * fields: ["title"], + * boost: 10, + * wildcard: lunr.Query.wildcard.TRAILING + * }) + * @example using lunr.tokenizer to convert a string to tokens before using them as terms + * query.term(lunr.tokenizer("foo bar")) + */ +lunr.Query.prototype.term = function (term, options) { + if (Array.isArray(term)) { + term.forEach(function (t) { this.term(t, lunr.utils.clone(options)) }, this) + return this + } + + var clause = options || {} + clause.term = term.toString() + + this.clause(clause) + + return this +} +lunr.QueryParseError = function (message, start, end) { + this.name = "QueryParseError" + this.message = message + this.start = start + this.end = end +} + +lunr.QueryParseError.prototype = new Error +lunr.QueryLexer = function (str) { + this.lexemes = [] + this.str = str + this.length = str.length + this.pos = 0 + this.start = 0 + this.escapeCharPositions = [] +} + +lunr.QueryLexer.prototype.run = function () { + var state = lunr.QueryLexer.lexText + + while (state) { + state = state(this) + } +} + +lunr.QueryLexer.prototype.sliceString = function () { + var subSlices = [], + sliceStart = this.start, + sliceEnd = this.pos + + for (var i = 0; i < this.escapeCharPositions.length; i++) { + sliceEnd = this.escapeCharPositions[i] + subSlices.push(this.str.slice(sliceStart, sliceEnd)) + sliceStart = sliceEnd + 1 + } + + subSlices.push(this.str.slice(sliceStart, this.pos)) + this.escapeCharPositions.length = 0 + + return subSlices.join('') +} + +lunr.QueryLexer.prototype.emit = function (type) { + this.lexemes.push({ + type: type, + str: this.sliceString(), + start: this.start, + end: this.pos + }) + + this.start = this.pos +} + +lunr.QueryLexer.prototype.escapeCharacter = function () { + this.escapeCharPositions.push(this.pos - 1) + this.pos += 1 +} + +lunr.QueryLexer.prototype.next = function () { + if (this.pos >= this.length) { + return lunr.QueryLexer.EOS + } + + var char = this.str.charAt(this.pos) + this.pos += 1 + return char +} + +lunr.QueryLexer.prototype.width = function () { + return this.pos - this.start +} + +lunr.QueryLexer.prototype.ignore = function () { + if (this.start == this.pos) { + this.pos += 1 + } + + this.start = this.pos +} + +lunr.QueryLexer.prototype.backup = function () { + this.pos -= 1 +} + +lunr.QueryLexer.prototype.acceptDigitRun = function () { + var char, charCode + + do { + char = this.next() + charCode = char.charCodeAt(0) + } while (charCode > 47 && charCode < 58) + + if (char != lunr.QueryLexer.EOS) { + this.backup() + } +} + +lunr.QueryLexer.prototype.more = function () { + return this.pos < this.length +} + +lunr.QueryLexer.EOS = 'EOS' +lunr.QueryLexer.FIELD = 'FIELD' +lunr.QueryLexer.TERM = 'TERM' +lunr.QueryLexer.EDIT_DISTANCE = 'EDIT_DISTANCE' +lunr.QueryLexer.BOOST = 'BOOST' +lunr.QueryLexer.PRESENCE = 'PRESENCE' + +lunr.QueryLexer.lexField = function (lexer) { + lexer.backup() + lexer.emit(lunr.QueryLexer.FIELD) + lexer.ignore() + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexTerm = function (lexer) { + if (lexer.width() > 1) { + lexer.backup() + lexer.emit(lunr.QueryLexer.TERM) + } + + lexer.ignore() + + if (lexer.more()) { + return lunr.QueryLexer.lexText + } +} + +lunr.QueryLexer.lexEditDistance = function (lexer) { + lexer.ignore() + lexer.acceptDigitRun() + lexer.emit(lunr.QueryLexer.EDIT_DISTANCE) + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexBoost = function (lexer) { + lexer.ignore() + lexer.acceptDigitRun() + lexer.emit(lunr.QueryLexer.BOOST) + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexEOS = function (lexer) { + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } +} + +// This matches the separator used when tokenising fields +// within a document. These should match otherwise it is +// not possible to search for some tokens within a document. +// +// It is possible for the user to change the separator on the +// tokenizer so it _might_ clash with any other of the special +// characters already used within the search string, e.g. :. +// +// This means that it is possible to change the separator in +// such a way that makes some words unsearchable using a search +// string. +lunr.QueryLexer.termSeparator = lunr.tokenizer.separator + +lunr.QueryLexer.lexText = function (lexer) { + while (true) { + var char = lexer.next() + + if (char == lunr.QueryLexer.EOS) { + return lunr.QueryLexer.lexEOS + } + + // Escape character is '\' + if (char.charCodeAt(0) == 92) { + lexer.escapeCharacter() + continue + } + + if (char == ":") { + return lunr.QueryLexer.lexField + } + + if (char == "~") { + lexer.backup() + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } + return lunr.QueryLexer.lexEditDistance + } + + if (char == "^") { + lexer.backup() + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } + return lunr.QueryLexer.lexBoost + } + + // "+" indicates term presence is required + // checking for length to ensure that only + // leading "+" are considered + if (char == "+" && lexer.width() === 1) { + lexer.emit(lunr.QueryLexer.PRESENCE) + return lunr.QueryLexer.lexText + } + + // "-" indicates term presence is prohibited + // checking for length to ensure that only + // leading "-" are considered + if (char == "-" && lexer.width() === 1) { + lexer.emit(lunr.QueryLexer.PRESENCE) + return lunr.QueryLexer.lexText + } + + if (char.match(lunr.QueryLexer.termSeparator)) { + return lunr.QueryLexer.lexTerm + } + } +} + +lunr.QueryParser = function (str, query) { + this.lexer = new lunr.QueryLexer (str) + this.query = query + this.currentClause = {} + this.lexemeIdx = 0 +} + +lunr.QueryParser.prototype.parse = function () { + this.lexer.run() + this.lexemes = this.lexer.lexemes + + var state = lunr.QueryParser.parseClause + + while (state) { + state = state(this) + } + + return this.query +} + +lunr.QueryParser.prototype.peekLexeme = function () { + return this.lexemes[this.lexemeIdx] +} + +lunr.QueryParser.prototype.consumeLexeme = function () { + var lexeme = this.peekLexeme() + this.lexemeIdx += 1 + return lexeme +} + +lunr.QueryParser.prototype.nextClause = function () { + var completedClause = this.currentClause + this.query.clause(completedClause) + this.currentClause = {} +} + +lunr.QueryParser.parseClause = function (parser) { + var lexeme = parser.peekLexeme() + + if (lexeme == undefined) { + return + } + + switch (lexeme.type) { + case lunr.QueryLexer.PRESENCE: + return lunr.QueryParser.parsePresence + case lunr.QueryLexer.FIELD: + return lunr.QueryParser.parseField + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expected either a field or a term, found " + lexeme.type + + if (lexeme.str.length >= 1) { + errorMessage += " with value '" + lexeme.str + "'" + } + + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } +} + +lunr.QueryParser.parsePresence = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + switch (lexeme.str) { + case "-": + parser.currentClause.presence = lunr.Query.presence.PROHIBITED + break + case "+": + parser.currentClause.presence = lunr.Query.presence.REQUIRED + break + default: + var errorMessage = "unrecognised presence operator'" + lexeme.str + "'" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + var errorMessage = "expecting term or field, found nothing" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.FIELD: + return lunr.QueryParser.parseField + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expecting term or field, found '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseField = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + if (parser.query.allFields.indexOf(lexeme.str) == -1) { + var possibleFields = parser.query.allFields.map(function (f) { return "'" + f + "'" }).join(', '), + errorMessage = "unrecognised field '" + lexeme.str + "', possible fields: " + possibleFields + + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.fields = [lexeme.str] + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + var errorMessage = "expecting term, found nothing" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expecting term, found '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseTerm = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + parser.currentClause.term = lexeme.str.toLowerCase() + + if (lexeme.str.indexOf("*") != -1) { + parser.currentClause.usePipeline = false + } + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseEditDistance = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + var editDistance = parseInt(lexeme.str, 10) + + if (isNaN(editDistance)) { + var errorMessage = "edit distance must be numeric" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.editDistance = editDistance + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseBoost = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + var boost = parseInt(lexeme.str, 10) + + if (isNaN(boost)) { + var errorMessage = "boost must be numeric" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.boost = boost + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + + /** + * export the module via AMD, CommonJS or as a browser global + * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js + */ + ;(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory) + } else if (typeof exports === 'object') { + /** + * Node. Does not work with strict CommonJS, but + * only CommonJS-like environments that support module.exports, + * like Node. + */ + module.exports = factory() + } else { + // Browser globals (root is window) + root.lunr = factory() + } + }(this, function () { + /** + * Just return a value to define the module export. + * This example returns an object, but the module + * can return a function as the exported value. + */ + return lunr + })) +})(); diff --git a/docs/search/main.js b/docs/search/main.js new file mode 100644 index 000000000..a5e469d7c --- /dev/null +++ b/docs/search/main.js @@ -0,0 +1,109 @@ +function getSearchTermFromLocation() { + var sPageURL = window.location.search.substring(1); + var sURLVariables = sPageURL.split('&'); + for (var i = 0; i < sURLVariables.length; i++) { + var sParameterName = sURLVariables[i].split('='); + if (sParameterName[0] == 'q') { + return decodeURIComponent(sParameterName[1].replace(/\+/g, '%20')); + } + } +} + +function joinUrl (base, path) { + if (path.substring(0, 1) === "/") { + // path starts with `/`. Thus it is absolute. + return path; + } + if (base.substring(base.length-1) === "/") { + // base ends with `/` + return base + path; + } + return base + "/" + path; +} + +function escapeHtml (value) { + return value.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); +} + +function formatResult (location, title, summary) { + return ''; +} + +function displayResults (results) { + var search_results = document.getElementById("mkdocs-search-results"); + while (search_results.firstChild) { + search_results.removeChild(search_results.firstChild); + } + if (results.length > 0){ + for (var i=0; i < results.length; i++){ + var result = results[i]; + var html = formatResult(result.location, result.title, result.summary); + search_results.insertAdjacentHTML('beforeend', html); + } + } else { + var noResultsText = search_results.getAttribute('data-no-results-text'); + if (!noResultsText) { + noResultsText = "No results found"; + } + search_results.insertAdjacentHTML('beforeend', '

' + noResultsText + '

'); + } +} + +function doSearch () { + var query = document.getElementById('mkdocs-search-query').value; + if (query.length > min_search_length) { + if (!window.Worker) { + displayResults(search(query)); + } else { + searchWorker.postMessage({query: query}); + } + } else { + // Clear results for short queries + displayResults([]); + } +} + +function initSearch () { + var search_input = document.getElementById('mkdocs-search-query'); + if (search_input) { + search_input.addEventListener("keyup", doSearch); + } + var term = getSearchTermFromLocation(); + if (term) { + search_input.value = term; + doSearch(); + } +} + +function onWorkerMessage (e) { + if (e.data.allowSearch) { + initSearch(); + } else if (e.data.results) { + var results = e.data.results; + displayResults(results); + } else if (e.data.config) { + min_search_length = e.data.config.min_search_length-1; + } +} + +if (!window.Worker) { + console.log('Web Worker API not supported'); + // load index in main thread + $.getScript(joinUrl(base_url, "search/worker.js")).done(function () { + console.log('Loaded worker'); + init(); + window.postMessage = function (msg) { + onWorkerMessage({data: msg}); + }; + }).fail(function (jqxhr, settings, exception) { + console.error('Could not load worker.js'); + }); +} else { + // Wrap search in a web worker + var searchWorker = new Worker(joinUrl(base_url, "search/worker.js")); + searchWorker.postMessage({init: true}); + searchWorker.onmessage = onWorkerMessage; +} diff --git a/docs/search/search_index.json b/docs/search/search_index.json new file mode 100644 index 000000000..636a77f8b --- /dev/null +++ b/docs/search/search_index.json @@ -0,0 +1 @@ +{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"Welcome to the Mercury project The Mercury project is created with one primary objective - to make software easy to write, read, test, deploy, scale and manage. Mercury has been powering mission-critical cloud native production applications for the last few years. In version 3, Mercury has evolved to the next level. It is bringing event-driven design and the best of preemptive and cooperative multitasking as a foundation to build \"composable applications.\" You have low-level control to precisely tune your application for optimal performance and throughput using three function execution strategies: kernel thread pool coroutine suspend function Mercury version 3 achieves virtual threading using coroutine and suspend function. In version 4, it fully embraces Java 21 virtual thread feature that was officially available since 12/2023. August, 2024 Mercury version 3 Mercury version 3 is a software development toolkit for event-driven programming. Event-driven programming allows functional modules to communicate with each other using events instead of direct method calls. This allows the functions to be executed asynchronously, improving overall application performance. IMPORTANT: You should use Mercury version 4 unless you need backward compatibility for your production systems. Please visit Mercury Version 4 using the links below: Mercury v4: https://github.com/Accenture/mercury-composable Documentation: https://accenture.github.io/mercury-composable/ Differences between Mercury version 3 and 4 The key differences of Mercury version 3 and the latest Mercury-Composable version 4 are: Category Mercury 3.0 Mercury 4.x Java version Supports Java 1.8 or higher Requires Java 21 or higher Event management Orchestration by code Event choreography by configuration Multitasking Coroutine and kernel Java 21 virtual threads, coroutine and kernel Functional isolation KernelThreadRunner Virtual Threads and KernelThreadRunner Breaking changes By default, the system runs all functions as \"coroutines\" where previous versions run them using a kernel thread pool. The CoroutineRunner annotation has been removed and replaced with the new KernelThreadRunner annotation. The \"rest.automation.yaml\" key is renamed as \"yaml.rest.automation\" after we unify the parsing behavior of application.properties with application.yml. Write your first composable application To get started with your first application, please refer to the Chapter 4, Developer Guide . Introduction to composable architecture In cloud migration and IT modernization, we evaluate application portfolio and recommend different disposition strategies based on the 7R migration methodology. 7R: Retire, retain, re-host, re-platform, replace, re-architect and re-imagine. The most common observation during IT modernization discovery is that there are many complex monolithic applications that are hard to modernize quickly. IT modernization is like moving into a new home. It would be the opportunity to clean up and to improve for business agility and strategic competitiveness. Composable architecture is gaining visibility recently because it accelerates organization transformation towards a cloud native future. We will discuss how we may reduce modernization risks with this approach. Composability Composability applies to both platform and application levels. We can trace the root of composability to Service Oriented Architecture (SOA) in 2000 or a technical bulletin on \"Flow-Based Programming\" by IBM in 1971. This is the idea that architecture and applications are built using modular building blocks and each block is self-contained with predictable behavior. At the platform level, composable architecture refers to loosely coupled platform services, utilities, and business applications. With modular design, you can assemble platform components and applications to create new use cases or to adjust for ever-changing business environment and requirements. Domain driven design (DDD), Command Query Responsibility Segregation (CQRS) and Microservices patterns are the popular tools that architects use to build composable architecture. You may deploy application in container, serverless or other means. At the application level, a composable application means that an application is assembled from modular software components or functions that are self-contained and pluggable. You can mix-n-match functions to form new applications. You can retire outdated functions without adverse side effect to a production system. Multiple versions of a function can exist, and you can decide how to route user requests to different versions of a function. Applications would be easier to design, develop, maintain, deploy, and scale. Composable architecture and applications contribute to business agility. Building a composable application Microservices Since 2014, microservices architectural pattern helps to decompose a big application into smaller pieces of \u201cself-contained\u201d services. We also apply digital decoupling techniques to services and domains. Smaller is better. However, we are writing code in the same old fashion. One method is calling other methods directly. Functional and reactive programming techniques are means to run code in a non-blocking manner, for example Reactive Streams, Akka, Vertx, Quarkus Multi/Uni and Spring Reactive Flux/Mono. These are excellent tools, but they do not reduce the complexity of business applications. Composable application To make an application composable, the software components within a single application should be loosely coupled where each component has zero or minimal dependencies. Unlike traditional programming approach, composable application is built from the top down. First, we describe a business transaction as an event flow. Second, from the event flow, we identify individual functions for business logic. Third, we write user story for each function and write code in a self-contained manner. Finally, we write orchestration code to coordinate event flow among the functions, so they work together as a single application. The individual functions become the building block for a composable application. We can mix-n-match different sets of functions to address different business use cases. Event is the communication conduit Cloud native applications are deployed as containers or serverless functions. Ideally, they communicate using events. For example, the CQRS design pattern is well accepted for building high performance cloud native applications. Figure 1 - Cloud native applications use event streams to communicate However, within a single application unit, the application is mostly built in a traditional way. i.e. one function is calling other functions and libraries directly, thus making the modules and libraries tightly coupled. As a result, microservices may become smaller monolithic applications. To overcome this limitation, we can employ \u201cevent-driven design\u201d to make the microservices application unit composable. An application unit is a collection of functions in memory and an \u201cevent bus\u201d is the communication conduit to connect the functions together to form a single executable. Figure 2 \u2013 Functions use in-memory event bus to communicate In-memory event bus For a composable application, each function is written using the first principle of \u201cinput-process-output\u201d where input and output payloads are delivered as events. All input and output are immutable to reduce unintended bugs and side effects. Since input and output for each function is well-defined, test-driven development (TDD) can be done naturally. It is also easier to define a user story for each function and the developer does not need to study and integrate multiple levels of dependencies, resulting in higher quality code. Figure 3 - The first principle of a function What is a \u201cfunction\u201d? For example, reading a record from a database and performing some data transformation, doing a calculation with a formula, etc. Figure 4 - Connecting output of one function to input of another As shown in Figure 4, if function-1 wants to send a request to function-2, we can write \u201cevent orchestration code\u201d to put the output from function-1 into an event envelope and send it over an in-memory event bus. The event system will transport the event envelope to function-2, extract the payload and submit it as \u201cinput\u201d to function-2 Function execution strategy In event-driven application design, a function is executed when an event arrives as \u201cinput.\u201d When a function finishes processing, your application can command the event system to route the result set (\u201coutput\u201d) as an event to another function. Figure 5 - Executing function through event flow As shown in Figure 5, functions can send/receive events using an in-memory event bus (aka \"event loop\"). This event-driven architecture provides the foundation to design and implement composable applications. Each function is self-contained and loosely coupled by event flow. A function receiving an event needs to be executed. There are three ways to do that: Kernel thread pool Coroutine Suspend function Kernel thread pool Java supports \u201cpreemptive multitasking\u201d using kernel threads. Multiple functions can execute in parallel. Preemptive multitasking leverages the multiple cores of a CPU to yield higher performance. Preemptive multitasking is performed at the kernel level and the operating system is doing the context switching. As a result, the maximum number of kernel threads is small. As a rule of thumb, a moderately fast computer can support ~200 kernel threads. Figure 6 - Multitasking of kernel threads at the hardware and OS level Coroutine Many modern programming languages such as GoLang, Kotlin, Python and Node.js support \u201ccooperative multitasking\u201d using \u201cevent loop\u201d or \u201ccoroutine.\u201d Instead of context switching at the kernel level, functions are executed orderly by yielding to each other. The order of execution depends on the event flow of each business transaction. Since the functions are running cooperatively, the overheads of context switching are low. \u201cEvent loop\u201d or \u201cCoroutine\u201d technology usually can support tens of thousands of \u201cfunctions\u201d running in \u201cparallel.\u201d Technically, the functions are running sequentially. When each function finishes execution very quickly, they appear as running concurrently. Figure 7 - Cooperative multitasking of coroutines Java 1.8 and higher versions support event loop with open sources libraries such as Lightbend Akka and Eclipse Vertx. A preview \u201cvirtual thread\u201d technology is available in Java version 19. It brings cooperative multitasking by running tens of thousands of \u201cvirtual threads\u201d in a single kernel thread. This is a major technological breakthrough to close the gap with other modern programming languages. \u201cSuspend function\u201d In a typical enterprise application, many functions are waiting for responses most of the time. In preemptive multitasking, these functions are using kernel threads and consuming CPU time. Too many active kernel threads would turn the application into slow motion. \u201cSuspend function\u201d not only avoids overwhelming the CPU with excessive kernel threads but also leverages the synchronous request-response opportunity into high throughput non-blocking operation. As the name indicates, \u201csuspend function\u201d can be suspended and resumed. When it is suspended, it yields control to the event loop so that other coroutines or suspend functions can run. In Node.js and GoLang, coroutine and suspend function are the same. Suspend function refers to the \u201casync/await\u201d keywords or API of coroutine. In Kotlin, the suspend function extends a coroutine to have the suspend/resume ability. A function is suspended when it is waiting for a response from the network, a database or from another function. It is resumed when a response is received. Figure 8 - Improving throughput with suspend function As shown in Figure 8, a \u201csuspend function\u201d can suspend and resume multiple times during its execution. When it suspends, it is not using any CPU time, thus the application has more time to serve other functions. This mechanism is so efficient that it can significantly increase the throughput of the application. i.e. it can handle many concurrent users, and process more requests. Performance and throughput The ability to select an optimal function execution strategy for a function is critical to the success of a composable application. This allows the developer to have low level control of how the application performs and scales. Without an optimal function execution strategy, performance tuning is usually an educated guess. In composable application architecture, each function is self-contained and stateless. We can predict the performance of each function by selecting an optimal function execution strategy and evaluate it with unit tests and observability. Predicting application performance and throughput at design and development time reduces modernization risks. The pros and cons of each function execution strategy are summarized below: Strategy Advantage Disadvantage Kernel threads Highest performance in terms of operations per seconds Lower number of concurrent threads due to high context switching overheads Coroutine Highest throughput in terms of concurrent users served by virtual threads concurrently Not suitable for long running tasks Suspend function Sequential \"non-blocking\" for RPC (request-response) that makes code easier to read and maintain Not suitable for long running tasks As shown in the table above, performance and throughput are determined by function execution strategies. For example, single threaded event driven network proxies such as nginx support twenty times more concurrent connections than multithreaded application servers. On the other hand, Node.js is not suitable for long running tasks. When one function takes more time to execute, all other functions are blocked and thus degrading the overall application performance. The latest Node.js language adds kernel threads using the \u201cweb worker\u201d technology to alleviate this limitation. However, web worker API is more tedious than multithreading in Java and other programming languages. The best of both worlds If we simplify event-driven programming and support all three function execution strategies, we can design and implement composable applications that deliver high performance and high throughput. The \u201cvirtual thread\u201d feature in the upcoming Java version 19 will be a good building block for function execution strategies. Currently it is available as a \u201cpreview\u201d feature. When it becomes available later in 2023, it will have a significant impact on the Java community. It will be at par with other programming languages that support event loop. It supports non-blocking sequential programming without explicitly using the \u201casync\u201d and \u201cawait\u201d keywords. All current open sources libraries that provide event loop functionality would evolve. To accelerate this evolution, we have implemented Mercury version 3.0 as an accelerator to build composable applications. It supports the two pillars of composable application \u2013 In-memory event bus and selection of function execution strategies. It integrates with Eclipse Vertx to hide the complexity of event-driven programming and embraces the three function execution strategies using kernel thread pool, coroutine and suspend function. The default execution strategy is \"coroutine\" unless you specify the function using the \"KernelThreadRunner\" annotation. To simplify writing \u201csuspend function,\u201d you can implement the \u201cKotlinLambdaFunction\u201d class and copy-n-paste your existing Java code into the new Kotlin class, the IDE will automatically convert code for you. With 90% conversion efficiency, you may need minor touch up to finish the rest. We can construct a composable application with self-contained functions that execute when events arrive. There is a simple event API that we call the \u201cPost Office\u201d to support sequential non-blocking RPC, async, drop and forget, callback, workflow, pipeline, streaming and interceptor patterns. Sequential non-blocking RPC reduces the effort in application modernization because we can directly port sequential legacy code from a monolithic application to the new composable cloud native design. What is \"event orchestration\"? In traditional programming, we write code to make calls to different methods and libraries. In event-driven programming, we write code to send events, and this is \u201cevent orchestration.\u201d We can use events to make RPC call just like traditional programming. It is viable to port legacy orchestration logic into event orchestration code. To further reduce coding effort, we can use Event Script to do \u201cevent orchestration.\u201d This would replace code with simple event flow configuration. To use event script, please upgrade to Mercury v4. Mercury v4: https://github.com/Accenture/mercury-composable Documentation: https://accenture.github.io/mercury-composable/ How steep is the learning curve for a developer? The developer can use any coding style to write the individual functions, no matter it is sequential, object-oriented, or reactive. One may use any favorite frameworks or libraries. There are no restrictions. There is a learning curve in writing \u201cevent orchestration.\u201d Since event orchestration supports sequential non-blocking RPC, the developer can port existing legacy code to the modern style with direct mapping. Typically, the learning curve is about two weeks. If you are familiar with event-driven programming, the learning curve would be lower. To eliminate this learning curve, the developer may use Event Script that replaces orchestration code with event flow configuration files. Event Script is designed to have virtually zero API integration for exceptionally low learning curve. Conclusion Composability applies to both platform and application levels. We can design and implement better cloud native applications that are composable using event-driven design and the three function execution strategies. We can deliver application that demonstrates both high performance and high throughput, an objective that has been technically challenging with traditional means. We can scientifically predict application performance and throughput in design and development time, thus saving time and ensuring consistent product quality. Composable approach also facilitates the migration of monolithic application into cloud native by decomposing the application to functional level and assembling them into microservices according to domain boundary. It reduces coding effort and application complexity, meaning less project risks. Java version 19 is introducing a new \u201cvirtual thread\u201d feature in 2023 that will make it at par with other modern programming languages such as GoLang and Node.js. Since Java has the largest enterprise-grade open sources and commercial libraries with easy access to a large pool of trained developers, the availability of virtual thread technology would retain Java as the best option for application modernization and composable applications. Mercury and Event Script version 3.0 bring virtual thread technology with Kotlin coroutine and suspend function before Java version 19 becomes mainstream. This opens a new frontier of cloud native applications that are composable, scalable, and easy to maintain, thus contributing to business agility.","title":"Home"},{"location":"#welcome-to-the-mercury-project","text":"The Mercury project is created with one primary objective - to make software easy to write, read, test, deploy, scale and manage. Mercury has been powering mission-critical cloud native production applications for the last few years. In version 3, Mercury has evolved to the next level. It is bringing event-driven design and the best of preemptive and cooperative multitasking as a foundation to build \"composable applications.\" You have low-level control to precisely tune your application for optimal performance and throughput using three function execution strategies: kernel thread pool coroutine suspend function Mercury version 3 achieves virtual threading using coroutine and suspend function. In version 4, it fully embraces Java 21 virtual thread feature that was officially available since 12/2023. August, 2024","title":"Welcome to the Mercury project"},{"location":"#mercury-version-3","text":"Mercury version 3 is a software development toolkit for event-driven programming. Event-driven programming allows functional modules to communicate with each other using events instead of direct method calls. This allows the functions to be executed asynchronously, improving overall application performance. IMPORTANT: You should use Mercury version 4 unless you need backward compatibility for your production systems. Please visit Mercury Version 4 using the links below: Mercury v4: https://github.com/Accenture/mercury-composable Documentation: https://accenture.github.io/mercury-composable/","title":"Mercury version 3"},{"location":"#differences-between-mercury-version-3-and-4","text":"The key differences of Mercury version 3 and the latest Mercury-Composable version 4 are: Category Mercury 3.0 Mercury 4.x Java version Supports Java 1.8 or higher Requires Java 21 or higher Event management Orchestration by code Event choreography by configuration Multitasking Coroutine and kernel Java 21 virtual threads, coroutine and kernel Functional isolation KernelThreadRunner Virtual Threads and KernelThreadRunner","title":"Differences between Mercury version 3 and 4"},{"location":"#breaking-changes","text":"By default, the system runs all functions as \"coroutines\" where previous versions run them using a kernel thread pool. The CoroutineRunner annotation has been removed and replaced with the new KernelThreadRunner annotation. The \"rest.automation.yaml\" key is renamed as \"yaml.rest.automation\" after we unify the parsing behavior of application.properties with application.yml.","title":"Breaking changes"},{"location":"#write-your-first-composable-application","text":"To get started with your first application, please refer to the Chapter 4, Developer Guide .","title":"Write your first composable application"},{"location":"#introduction-to-composable-architecture","text":"In cloud migration and IT modernization, we evaluate application portfolio and recommend different disposition strategies based on the 7R migration methodology. 7R: Retire, retain, re-host, re-platform, replace, re-architect and re-imagine. The most common observation during IT modernization discovery is that there are many complex monolithic applications that are hard to modernize quickly. IT modernization is like moving into a new home. It would be the opportunity to clean up and to improve for business agility and strategic competitiveness. Composable architecture is gaining visibility recently because it accelerates organization transformation towards a cloud native future. We will discuss how we may reduce modernization risks with this approach.","title":"Introduction to composable architecture"},{"location":"#composability","text":"Composability applies to both platform and application levels. We can trace the root of composability to Service Oriented Architecture (SOA) in 2000 or a technical bulletin on \"Flow-Based Programming\" by IBM in 1971. This is the idea that architecture and applications are built using modular building blocks and each block is self-contained with predictable behavior. At the platform level, composable architecture refers to loosely coupled platform services, utilities, and business applications. With modular design, you can assemble platform components and applications to create new use cases or to adjust for ever-changing business environment and requirements. Domain driven design (DDD), Command Query Responsibility Segregation (CQRS) and Microservices patterns are the popular tools that architects use to build composable architecture. You may deploy application in container, serverless or other means. At the application level, a composable application means that an application is assembled from modular software components or functions that are self-contained and pluggable. You can mix-n-match functions to form new applications. You can retire outdated functions without adverse side effect to a production system. Multiple versions of a function can exist, and you can decide how to route user requests to different versions of a function. Applications would be easier to design, develop, maintain, deploy, and scale. Composable architecture and applications contribute to business agility.","title":"Composability"},{"location":"#building-a-composable-application","text":"","title":"Building a composable application"},{"location":"#microservices","text":"Since 2014, microservices architectural pattern helps to decompose a big application into smaller pieces of \u201cself-contained\u201d services. We also apply digital decoupling techniques to services and domains. Smaller is better. However, we are writing code in the same old fashion. One method is calling other methods directly. Functional and reactive programming techniques are means to run code in a non-blocking manner, for example Reactive Streams, Akka, Vertx, Quarkus Multi/Uni and Spring Reactive Flux/Mono. These are excellent tools, but they do not reduce the complexity of business applications.","title":"Microservices"},{"location":"#composable-application","text":"To make an application composable, the software components within a single application should be loosely coupled where each component has zero or minimal dependencies. Unlike traditional programming approach, composable application is built from the top down. First, we describe a business transaction as an event flow. Second, from the event flow, we identify individual functions for business logic. Third, we write user story for each function and write code in a self-contained manner. Finally, we write orchestration code to coordinate event flow among the functions, so they work together as a single application. The individual functions become the building block for a composable application. We can mix-n-match different sets of functions to address different business use cases.","title":"Composable application"},{"location":"#event-is-the-communication-conduit","text":"Cloud native applications are deployed as containers or serverless functions. Ideally, they communicate using events. For example, the CQRS design pattern is well accepted for building high performance cloud native applications. Figure 1 - Cloud native applications use event streams to communicate However, within a single application unit, the application is mostly built in a traditional way. i.e. one function is calling other functions and libraries directly, thus making the modules and libraries tightly coupled. As a result, microservices may become smaller monolithic applications. To overcome this limitation, we can employ \u201cevent-driven design\u201d to make the microservices application unit composable. An application unit is a collection of functions in memory and an \u201cevent bus\u201d is the communication conduit to connect the functions together to form a single executable. Figure 2 \u2013 Functions use in-memory event bus to communicate","title":"Event is the communication conduit"},{"location":"#in-memory-event-bus","text":"For a composable application, each function is written using the first principle of \u201cinput-process-output\u201d where input and output payloads are delivered as events. All input and output are immutable to reduce unintended bugs and side effects. Since input and output for each function is well-defined, test-driven development (TDD) can be done naturally. It is also easier to define a user story for each function and the developer does not need to study and integrate multiple levels of dependencies, resulting in higher quality code. Figure 3 - The first principle of a function What is a \u201cfunction\u201d? For example, reading a record from a database and performing some data transformation, doing a calculation with a formula, etc. Figure 4 - Connecting output of one function to input of another As shown in Figure 4, if function-1 wants to send a request to function-2, we can write \u201cevent orchestration code\u201d to put the output from function-1 into an event envelope and send it over an in-memory event bus. The event system will transport the event envelope to function-2, extract the payload and submit it as \u201cinput\u201d to function-2","title":"In-memory event bus"},{"location":"#function-execution-strategy","text":"In event-driven application design, a function is executed when an event arrives as \u201cinput.\u201d When a function finishes processing, your application can command the event system to route the result set (\u201coutput\u201d) as an event to another function. Figure 5 - Executing function through event flow As shown in Figure 5, functions can send/receive events using an in-memory event bus (aka \"event loop\"). This event-driven architecture provides the foundation to design and implement composable applications. Each function is self-contained and loosely coupled by event flow. A function receiving an event needs to be executed. There are three ways to do that: Kernel thread pool Coroutine Suspend function","title":"Function execution strategy"},{"location":"#kernel-thread-pool","text":"Java supports \u201cpreemptive multitasking\u201d using kernel threads. Multiple functions can execute in parallel. Preemptive multitasking leverages the multiple cores of a CPU to yield higher performance. Preemptive multitasking is performed at the kernel level and the operating system is doing the context switching. As a result, the maximum number of kernel threads is small. As a rule of thumb, a moderately fast computer can support ~200 kernel threads. Figure 6 - Multitasking of kernel threads at the hardware and OS level","title":"Kernel thread pool"},{"location":"#coroutine","text":"Many modern programming languages such as GoLang, Kotlin, Python and Node.js support \u201ccooperative multitasking\u201d using \u201cevent loop\u201d or \u201ccoroutine.\u201d Instead of context switching at the kernel level, functions are executed orderly by yielding to each other. The order of execution depends on the event flow of each business transaction. Since the functions are running cooperatively, the overheads of context switching are low. \u201cEvent loop\u201d or \u201cCoroutine\u201d technology usually can support tens of thousands of \u201cfunctions\u201d running in \u201cparallel.\u201d Technically, the functions are running sequentially. When each function finishes execution very quickly, they appear as running concurrently. Figure 7 - Cooperative multitasking of coroutines Java 1.8 and higher versions support event loop with open sources libraries such as Lightbend Akka and Eclipse Vertx. A preview \u201cvirtual thread\u201d technology is available in Java version 19. It brings cooperative multitasking by running tens of thousands of \u201cvirtual threads\u201d in a single kernel thread. This is a major technological breakthrough to close the gap with other modern programming languages.","title":"Coroutine"},{"location":"#suspend-function","text":"In a typical enterprise application, many functions are waiting for responses most of the time. In preemptive multitasking, these functions are using kernel threads and consuming CPU time. Too many active kernel threads would turn the application into slow motion. \u201cSuspend function\u201d not only avoids overwhelming the CPU with excessive kernel threads but also leverages the synchronous request-response opportunity into high throughput non-blocking operation. As the name indicates, \u201csuspend function\u201d can be suspended and resumed. When it is suspended, it yields control to the event loop so that other coroutines or suspend functions can run. In Node.js and GoLang, coroutine and suspend function are the same. Suspend function refers to the \u201casync/await\u201d keywords or API of coroutine. In Kotlin, the suspend function extends a coroutine to have the suspend/resume ability. A function is suspended when it is waiting for a response from the network, a database or from another function. It is resumed when a response is received. Figure 8 - Improving throughput with suspend function As shown in Figure 8, a \u201csuspend function\u201d can suspend and resume multiple times during its execution. When it suspends, it is not using any CPU time, thus the application has more time to serve other functions. This mechanism is so efficient that it can significantly increase the throughput of the application. i.e. it can handle many concurrent users, and process more requests.","title":"\u201cSuspend function\u201d"},{"location":"#performance-and-throughput","text":"The ability to select an optimal function execution strategy for a function is critical to the success of a composable application. This allows the developer to have low level control of how the application performs and scales. Without an optimal function execution strategy, performance tuning is usually an educated guess. In composable application architecture, each function is self-contained and stateless. We can predict the performance of each function by selecting an optimal function execution strategy and evaluate it with unit tests and observability. Predicting application performance and throughput at design and development time reduces modernization risks. The pros and cons of each function execution strategy are summarized below: Strategy Advantage Disadvantage Kernel threads Highest performance in terms of operations per seconds Lower number of concurrent threads due to high context switching overheads Coroutine Highest throughput in terms of concurrent users served by virtual threads concurrently Not suitable for long running tasks Suspend function Sequential \"non-blocking\" for RPC (request-response) that makes code easier to read and maintain Not suitable for long running tasks As shown in the table above, performance and throughput are determined by function execution strategies. For example, single threaded event driven network proxies such as nginx support twenty times more concurrent connections than multithreaded application servers. On the other hand, Node.js is not suitable for long running tasks. When one function takes more time to execute, all other functions are blocked and thus degrading the overall application performance. The latest Node.js language adds kernel threads using the \u201cweb worker\u201d technology to alleviate this limitation. However, web worker API is more tedious than multithreading in Java and other programming languages.","title":"Performance and throughput"},{"location":"#the-best-of-both-worlds","text":"If we simplify event-driven programming and support all three function execution strategies, we can design and implement composable applications that deliver high performance and high throughput. The \u201cvirtual thread\u201d feature in the upcoming Java version 19 will be a good building block for function execution strategies. Currently it is available as a \u201cpreview\u201d feature. When it becomes available later in 2023, it will have a significant impact on the Java community. It will be at par with other programming languages that support event loop. It supports non-blocking sequential programming without explicitly using the \u201casync\u201d and \u201cawait\u201d keywords. All current open sources libraries that provide event loop functionality would evolve. To accelerate this evolution, we have implemented Mercury version 3.0 as an accelerator to build composable applications. It supports the two pillars of composable application \u2013 In-memory event bus and selection of function execution strategies. It integrates with Eclipse Vertx to hide the complexity of event-driven programming and embraces the three function execution strategies using kernel thread pool, coroutine and suspend function. The default execution strategy is \"coroutine\" unless you specify the function using the \"KernelThreadRunner\" annotation. To simplify writing \u201csuspend function,\u201d you can implement the \u201cKotlinLambdaFunction\u201d class and copy-n-paste your existing Java code into the new Kotlin class, the IDE will automatically convert code for you. With 90% conversion efficiency, you may need minor touch up to finish the rest. We can construct a composable application with self-contained functions that execute when events arrive. There is a simple event API that we call the \u201cPost Office\u201d to support sequential non-blocking RPC, async, drop and forget, callback, workflow, pipeline, streaming and interceptor patterns. Sequential non-blocking RPC reduces the effort in application modernization because we can directly port sequential legacy code from a monolithic application to the new composable cloud native design.","title":"The best of both worlds"},{"location":"#what-is-event-orchestration","text":"In traditional programming, we write code to make calls to different methods and libraries. In event-driven programming, we write code to send events, and this is \u201cevent orchestration.\u201d We can use events to make RPC call just like traditional programming. It is viable to port legacy orchestration logic into event orchestration code. To further reduce coding effort, we can use Event Script to do \u201cevent orchestration.\u201d This would replace code with simple event flow configuration. To use event script, please upgrade to Mercury v4. Mercury v4: https://github.com/Accenture/mercury-composable Documentation: https://accenture.github.io/mercury-composable/","title":"What is \"event orchestration\"?"},{"location":"#how-steep-is-the-learning-curve-for-a-developer","text":"The developer can use any coding style to write the individual functions, no matter it is sequential, object-oriented, or reactive. One may use any favorite frameworks or libraries. There are no restrictions. There is a learning curve in writing \u201cevent orchestration.\u201d Since event orchestration supports sequential non-blocking RPC, the developer can port existing legacy code to the modern style with direct mapping. Typically, the learning curve is about two weeks. If you are familiar with event-driven programming, the learning curve would be lower. To eliminate this learning curve, the developer may use Event Script that replaces orchestration code with event flow configuration files. Event Script is designed to have virtually zero API integration for exceptionally low learning curve.","title":"How steep is the learning curve for a developer?"},{"location":"#conclusion","text":"Composability applies to both platform and application levels. We can design and implement better cloud native applications that are composable using event-driven design and the three function execution strategies. We can deliver application that demonstrates both high performance and high throughput, an objective that has been technically challenging with traditional means. We can scientifically predict application performance and throughput in design and development time, thus saving time and ensuring consistent product quality. Composable approach also facilitates the migration of monolithic application into cloud native by decomposing the application to functional level and assembling them into microservices according to domain boundary. It reduces coding effort and application complexity, meaning less project risks. Java version 19 is introducing a new \u201cvirtual thread\u201d feature in 2023 that will make it at par with other modern programming languages such as GoLang and Node.js. Since Java has the largest enterprise-grade open sources and commercial libraries with easy access to a large pool of trained developers, the availability of virtual thread technology would retain Java as the best option for application modernization and composable applications. Mercury and Event Script version 3.0 bring virtual thread technology with Kotlin coroutine and suspend function before Java version 19 becomes mainstream. This opens a new frontier of cloud native applications that are composable, scalable, and easy to maintain, thus contributing to business agility.","title":"Conclusion"},{"location":"CHANGELOG/","text":"Changelog Release notes All notable changes to this project will be documented in this file. The format is based on Keep a Changelog , and this project adheres to Semantic Versioning . Version 3.0.16, 8/31/2024 Added N/A Removed N/A Changed Updated documentation AsyncHttpRequest - remove the class type variable in the data model MultiLevelMap - update the removeElement method OSS update - GSON 2.11.0, classpath 4.8.174, guava 33.3.0-jre, vertx 2.5.9, spring boot 3.3.3, spring framework 5.3.39 Version 3.0.15, 5/1/2024 This version supercedes 3.0.13 and 3.0.14 due to updated data structure for static content handling. Added Added optional static-content.no-cache-pages in rest.yaml AsyncHttpClientLoader Removed N/A Changed Updated data structure for static-content section in rest.yaml Fixed bug for setting multiple HTTP cookies Unified configuration file prefix \"yaml.\" Version 3.0.14, 4/28/2024 Added N/A Removed N/A Changed Updated syntax for static-content-filter section in rest.yaml Version 3.0.13, 4/28/2024 Added Added optional static content HTTP-GET request filter in rest.yaml Removed N/A Changed Updated guava to version 33.1.0-jre Version 3.0.12, 4/24/2024 Added N/A Removed N/A Changed Enhanced OptionalService annotation Version 3.0.11, 4/23/2024 Backport KernelThreadRunner from Mercury Composable 3.1 Added KernelThreadRunner annotation Removed CoroutineRunner annotation Changed Set default execution strategy to \"coroutine\". To tell the system to run a function using kernel thread pool, add the KernelThreadRunner annotation. Update PersistentWsClient to use vertx WebSocketClient. Upgrade netty to version 4.1.109.Final. Version 3.0.10, 3/19/2024 Added Added \"app-config-reader.yml\" file in the resources folder so that you can override the default application configuration files. Removed N/A Changed Open sources library update (Spring Boot 3.2.4, Vertx 4.5.5) Improve AppConfigReader and ConfigReader to use the app-config-reader.yml file. Version 3.0.9, 2/9/2024 Added AutoStart to run application as Spring Boot if the rest-spring-3 library is packaged in app Removed Bugfix: removed websocket client connection timeout that causes the first connection to drop after one minute Changed Open sources library update (Spring Boot 3.2.2, Vertx 4.5.3 and MsgPack 0.9.8) Rename application parameter \"event.worker.pool\" to \"kernel.thread.pool\" Support user defined serializer with PreLoad annotation and platform API Version 3.0.8, 1/27/2024 Added N/A Removed ActiveMQ and Tibco cloud connectors Changed Updated AsyncHttpRequest and CryptoApi Version 3.0.7, 12/23/2023 Added Print out basic JVM information before startup for verification of base container image. Removed Removed Maven Shade packager Changed Updated open sources libraries to address security vulnerabilities Spring Boot 2/3 to version 2.7.18 and 3.2.1 respectively Tomcat 9.0.84 Vertx 4.5.1 Classgraph 4.8.165 Netty 4.1.104.Final slf4j API 2.0.9 log4j2 2.22.0 Kotlin 1.9.22 Artemis 2.31.2 Hazelcast 5.3.6 Guava 33.0.0-jre Version 3.0.6, 10/26/2023 Added Enhanced Benchmark tool to support \"Event over HTTP\" protocol to evaluate performance efficiency for commmunication between application containers using HTTP. Removed N/A Changed Updated open sources libraries Spring Boot 2/3 to version 2.7.17 and 3.1.5 respectively Kafka-client 3.6.0 Version 3.0.5, 10/21/2023 Added Support two executable JAR packaging system: 1. Maven Shade packager 2. Spring Boot packager Starting from version 3.0.5, we have replaced Spring Boot packager with Maven Shade. This avoids a classpath edge case for Spring Boot packager when running kafka-client under Java 11 or higher. Maven Shade also results in smaller executable JAR size. Removed N/A Changed Updated open sources libraries Spring-Boot 2.7.16 / 3.1.4 classgraph 4.8.163 snakeyaml 2.2 kotlin 1.9.10 vertx 4.4.6 guava 32.1.3-jre msgpack 0.9.6 slj4j 2.0.9 zookeeper 3.7.2 The \"/info/lib\" admin endpoint has been enhanced to list library dependencies for executable JAR generated by either Maven Shade or Spring Boot Packager. Improved ConfigReader to recognize both \".yml\" and \".yaml\" extensions and their uses are interchangeable. Version 3.0.4, 8/6/2023 Added N/A Removed N/A Changed Updated open sources libraries Spring-Boot 2.7.14 / 3.1.2 Kafka-client 3.5.1 classgraph 4.8.161 guava 32.1.2-jre msgpack 0.9.5 Version 3.0.3, 6/27/2023 Added File extension to MIME type mapping for static HTML file handling Removed N/A Changed Open sources library update - Kotlin version 1.9.0 Version 3.0.2, 6/9/2023 Added N/A Removed N/A Changed Consistent exception handling for Event API endpoint Open sources lib update - Vertx 4.4.4, Spring Boot 2.7.13, Spring Boot 3.1.1, classgraph 4.8.160, guava 32.0.1-jre Version 3.0.1, 6/5/2023 In this release, we have replace Google HTTP Client with vertx non-blocking WebClient. We also tested compatibility up to OpenJDK version 20 and maven 3.9.2. Added When \"x-raw-xml\" HTTP request header is set to \"true\", the AsyncHttpClient will skip the built-in XML serialization so that your application can retrieve the original XML text. Removed Retire Google HTTP client Changed Upgrade maven plugin versions. Version 3.0.0, 4/18/2023 This is a major release with some breaking changes. Please refer to Chapter-10 (Migration guide) for details. This version brings the best of preemptive and cooperating multitasking to Java (version 1.8 to 19) before Java 19 virtual thread feature becomes officially available. Added Function execution engine supporting kernel thread pool, Kotlin coroutine and suspend function \"Event over HTTP\" service for inter-container communication Support for Spring Boot version 3 and WebFlux Sample code for a pre-configured Spring Boot 3 application Removed Remove blocking APIs from platform-core Retire PM2 process manager sample script due to compatibility issue Changed Refactor \"async.http.request\" to use vertx web client for non-blocking operation Update log4j2 version 2.20.0 and slf4j version 2.0.7 in platform-core Update JBoss RestEasy JAX_RS to version 3.15.6.Final in rest-spring Update vertx to 4.4.2 Update Spring Boot parent pom to 2.7.12 and 3.1.0 for spring boot 2 and 3 respectively Remove com.fasterxml.classmate dependency from rest-spring Version 2.8.0, 3/20/2023 Added N/A Removed N/A Changed Improved load balancing in cloud-connector Filter URI to avoid XSS attack Upgrade to SnakeYaml 2.0 and patch Spring Boot 2.6.8 for compatibility with it Upgrade to Vertx 4.4.0, classgraph 4.8.157, tomcat 9.0.73 Version 2.7.1, 12/22/2022 Added standalone benchmark report app client and server benchmark apps add timeout tag to RPC events Removed N/A Changed Updated open sources dependencies Netty 4.1.86.Final Tomcat 9.0.69 Vertx 4.3.6 classgraph 4.8.152 google-http-client 1.42.3 Improved unit tests to use assertThrows to evaluate exception Enhanced AsyncHttpRequest serialization Version 2.7.0, 11/11/2022 In this version, REST automation code is moved to platform-core such that REST and Websocket service can share the same port. Added AsyncObjectStreamReader is added for non-blocking read operation from an object stream. Support of LocalDateTime in SimpleMapper Add \"removeElement\" method to MultiLevelMap Automatically convert a map to a PoJo when the sender does not specify class in event body Removed N/A Changed REST automation becomes part of platform-core and it can co-exist with Spring Web in the rest-spring module Enforce Spring Boot lifecycle management such that user apps will start after Spring Boot has loaded all components Update netty to version 4.1.84.Final Version 2.6.0, 10/13/2022 In this version, websocket notification example code has been removed from the REST automation system. If your application uses this feature, please recover the code from version 2.5.0 and refactor it as a separate library. Added N/A Removed Simplify REST automation system by removing websocket notification example in REST automation. Changed Replace Tomcat websocket server with Vertx non-blocking websocket server library Update netty to version 4.1.79.Final Update kafka client to version 2.8.2 Update snake yaml to version 1.33 Update gson to version 2.9.1 Version 2.5.0, 9/10/2022 Added New Preload annotation class to automate pre-registration of LambdaFunction. Removed Removed Spring framework and Tomcat dependencies from platform-core so that the core library can be applied to legacy J2EE application without library conflict. Changed Bugfix for proper housekeeping of future events. Make Gson and MsgPack handling of integer/long consistent Updated open sources libraries. Eclipse vertx-core version 4.3.4 MsgPack version 0.9.3 Google httpclient version 1.42.2 SnakeYaml version 1.31 Version 2.3.6, 6/21/2022 Added Support more than one event stream cluster. User application can share the same event stream cluster for pub/sub or connect to an alternative cluster for pub/sub use cases. Removed N/A Changed Cloud connector libraries update to Hazelcast 5.1.2 Version 2.3.5, 5/30/2022 Added Add tagging feature to handle language connector's routing and exception handling Removed Remove language pack's pub/sub broadcast feature Changed Update Spring Boot parent to version 2.6.8 to fetch Netty 4.1.77 and Spring Framework 5.3.20 Streamlined language connector transport protocol for compatibility with both Python and Node.js Version 2.3.4, 5/14/2022 Added N/A Removed Remove swagger-ui distribution from api-playground such that developer can clone the latest version Changed Update application.properties (from spring.resources.static-locations to spring.web.resources.static-locations) Update log4j, Tomcat and netty library version using Spring parent 2.6.6 Version 2.3.3, 3/30/2022 Added Enhanced AsyncRequest to handle non-blocking fork-n-join Removed N/A Changed Upgrade Spring Boot from 2.6.3 to 2.6.6 Version 2.3.2, 2/21/2022 Added Add support of queue API in native pub/sub module for improved ESB compatibility Removed N/A Changed N/A Version 2.3.1, 2/19/2022 Added N/A Removed N/A Changed Update Vertx to version 4.2.4 Update Tomcat to version 5.0.58 Use Tomcat websocket server for presence monitors Bugfix - Simple Scheduler's leader election searches peers correctly Version 2.3.0, 1/28/2022 Added N/A Removed N/A Changed Update copyright notice Update Vertx to version 4.2.3 Bugfix - RSA key generator supporting key length from 1024 to 4096 bits CryptoAPI - support different AES algorithms and custom IV Update Spring Boot to version 2.6.3 Version 2.2.3, 12/29/2021 Added Transaction journaling Add parameter distributed.trace.aggregation in application.properties such that trace aggregation may be disabled. Removed N/A Changed Update JBoss RestEasy library to 3.15.3.Final Improved po.search(route) to scan local and remote service registries. Added \"remoteOnly\" selection. Fix bug in releasing presence monitor topic for specific closed user group Update Apache log4j to version 2.17.1 Update Spring Boot parent to version 2.6.1 Update Netty to version 4.1.72.Final Update Vertx to version 4.2.2 Convenient class \"UserNotification\" for backend service to publish events to the UI when REST automation is deployed Version 2.2.2, 11/12/2021 Added User defined API authentication functions can be selected using custom HTTP request header \"Exception chaining\" feature in EventEnvelope New \"deferred.commit.log\" parameter for backward compatibility with older PowerMock in unit tests Removed N/A Changed Improved and streamlined SimpleXmlParser to handle arrays Bugfix for file upload in Service Gateway (REST automation library) Update Tomcat library from 9.0.50 to 9.0.54 Update Spring Boot library to 2.5.6 Update GSON library to 2.8.9 Version 2.2.1, 10/1/2021 Added Callback function can implement ServiceExceptionHandler to catch exception. It adds the onError() method. Removed N/A Changed Open sources library update - Vert.x 4.1.3, Netty 4.1.68-Final Version 2.1.1, 9/10/2021 Added User defined PoJo and Generics mapping Standardized serializers for default case, snake_case and camelCase Support of EventEnvelope as input parameter in TypedLambdaFunction so application function can inspect event's metadata Application can subscribe to life cycle events of other application instances Removed N/A Changed Replace Tomcat websocket server engine with Vertx in presence monitor for higher performance Bugfix for MsgPack transport of integer, long, BigInteger and BigDecimal Version 2.1.0, 7/25/2021 Added Multicast - application can define a multicast.yaml config to relay events to more than one target service. StreamFunction - function that allows the application to control back-pressure Removed \"object.streams.io\" route is removed from platform-core Changed Elastic Queue - Refactored using Oracle Berkeley DB Object stream I/O - simplified design using the new StreamFunction feature Open sources library update - Spring Boot 2.5.2, Tomcat 9.0.50, Vert.x 4.1.1, Netty 4.1.66-Final Version 2.0.0, 5/5/2021 Vert.x is introduced as the in-memory event bus Added ActiveMQ and Tibco connectors Admin endpoints to stop, suspend and resume an application instance Handle edge case to detect stalled application instances Add \"isStreamingPubSub\" method to the PubSub interface Removed Event Node event stream emulator has been retired. You may use standalone Kafka server as a replacement for development and testing in your laptop. Multi-tenancy namespace configuration has been retired. It is replaced by the \"closed user group\" feature. Changed Refactored Kafka and Hazelcast connectors to support virtual topics and closed user groups. Updated ConfigReader to be consistent with Spring value substitution logic for application properties Replace Akka actor system with Vert.x event bus Common code for various cloud connectors consolidated into cloud core libraries Version 1.13.0, 1/15/2021 Version 1.13.0 is the last version that uses Akka as the in-memory event system. Version 1.12.66, 1/15/2021 Added A simple websocket notification service is integrated into the REST automation system Seamless migration feature is added to the REST automation system Removed Legacy websocket notification example application Changed N/A Version 1.12.65, 12/9/2020 Added \"kafka.pubsub\" is added as a cloud service File download example in the lambda-example project \"trace.log.header\" added to application.properties - when tracing is enabled, this inserts the trace-ID of the transaction in the log context. For more details, please refer to the Developer Guide Add API to pub/sub engine to support creation of topic with partitions TypedLambdaFunction is added so that developer can predefine input and output classes in a service without casting Removed N/A Changed Decouple Kafka pub/sub from kafka connector so that native pub/sub can be used when application is running in standalone mode Rename \"relay\" to \"targetHost\" in AsyncHttpRequest data model Enhanced routing table distribution by sending a complete list of route tables, thus reducing network admin traffic. Version 1.12.64, 9/28/2020 Added If predictable topic is set, application instances will report their predictable topics as \"instance ID\" to the presence monitor. This improves visibility when a developer tests their application in \"hybrid\" mode. i.e. running the app locally and connect to the cloud remotely for event streams and cloud resources. Removed N/A Changed N/A Version 1.12.63, 8/27/2020 Added N/A Removed N/A Changed Improved Kafka producer and consumer pairing Version 1.12.62, 8/12/2020 Added New presence monitor's admin endpoint for the operator to force routing table synchronization (\"/api/ping/now\") Removed N/A Changed Improved routing table integrity check Version 1.12.61, 8/8/2020 Added Event stream systems like Kafka assume topic to be used long term. This version adds support to reuse the same topic when an application instance restarts. You can create a predictable topic using unique application name and instance ID. For example, with Kubernetes, you can use the POD name as the unique application instance topic. Removed N/A Changed N/A Version 1.12.56, 8/4/2020 Added Automate trace for fork-n-join use case Removed N/A Changed N/A Version 1.12.55, 7/19/2020 Added N/A Removed N/A Changed Improved distributed trace - set the \"from\" address in EventEnvelope automatically. Version 1.12.54, 7/10/2020 Added N/A Removed N/A Changed Application life-cycle management - User provided main application(s) will be started after Spring Boot declares web application ready. This ensures correct Spring autowiring or dependencies are available. Bugfix for locale - String.format(float) returns comma as decimal point that breaks number parser. Replace with BigDecimal decimal point scaling. Bugfix for Tomcat 9.0.35 - Change Async servlet default timeout from 30 seconds to -1 so the system can handle the whole life-cycle directly. Version 1.12.52, 6/11/2020 Added new \"search\" method in Post Office to return a list of application instances for a service simple \"cron\" job scheduler as an extension project add \"sequence\" to MainApplication annotation for orderly execution when more than one MainApplication is available support \"Optional\" object in EventEnvelope so a LambdaFunction can read and return Optional Removed N/A Changed The rest-spring library has been updated to support both JAR and WAR deployment All pom.xml files updated accordingly PersistentWsClient will back off for 10 seconds when disconnected by remote host Version 1.12.50, 5/20/2020 Added Payload segmentation For large payload in an event, the payload is automatically segmented into 64 KB segments. When there are more than one target application instances, the system ensures that the segments of the same event is delivered to exactly the same target. PersistentWsClient added - generalized persistent websocket client for Event Node, Kafka reporter and Hazelcast reporter. Removed N/A Changed Code cleaning to improve consistency Upgraded to hibernate-validator to v6.1.5.Final and Hazelcast version 4.0.1 REST automation is provided as a library and an application to handle different use cases Version 1.12.40, 5/4/2020 Added N/A Removed N/A Changed For security reason, upgrade log4j to version 2.13.2 Version 1.12.39, 5/3/2020 Added Use RestEasy JAX-RS library Removed For security reason, removed Jersey JAX-RS library Changed Updated RestLoader to initialize RestEasy servlet dispatcher Support nested arrays in MultiLevelMap Version 1.12.36, 4/16/2020 Added N/A Removed For simplicity, retire route-substitution admin endpoint. Route substitution uses a simple static table in route-substitution.yaml. Changed N/A Version 1.12.35, 4/12/2020 Added N/A Removed SimpleRBAC class is retired Changed Improved ConfigReader and AppConfigReader with automatic key-value normalization for YAML and JSON files Improved pub/sub module in kafka-connector Version 1.12.34, 3/28/2020 Added N/A Removed Retired proprietary config manager since we can use the \"BeforeApplication\" approach to load config from Kubernetes configMap or other systems of config record. Changed Added \"isZero\" method to the SimpleMapper class Convert BigDecimal to string without scientific notation (i.e. toPlainString instead of toString) Corresponding unit tests added to verify behavior Version 1.12.32, 3/14/2020 Added N/A Removed N/A Changed Kafka-connector will shutdown application instance when the EventProducer cannot send event to Kafka. This would allow the infrastructure to restart application instance automatically. Version 1.12.31, 2/26/2020 Added N/A Removed N/A Changed Kafka-connector now supports external service provider for Kafka properties and credentials. If your application implements a function with route name \"kafka.properties.provider\" before connecting to cloud, the kafka-connector will retrieve kafka credentials on demand. This addresses case when kafka credentials change after application start-up. Interceptors are designed to forward requests and thus they do not generate replies. However, if you implement a function as an EventInterceptor, your function can throw exception just like a regular function and the exception will be returned to the calling function. This makes it easier to write interceptors. Version 1.12.30, 2/6/2020 Added Expose \"async.http.request\" as a PUBLIC function (\"HttpClient as a service\") Removed N/A Changed Improved Hazelcast client connection stability Improved Kafka native pub/sub Version 1.12.29, 1/10/2020 Added Rest-automation will transport X-Trace-Id from/to Http request/response, therefore extending distributed trace across systems that support the X-Trace-Id HTTP header. Added endpoint and service to shutdown application instance. Removed N/A Changed Updated SimpleXmlParser with XML External Entity (XXE) injection prevention. Bug fix for hazelcast recovery logic - when a hazelcast node is down, the app instance will restart the hazelcast client and reset routing table correctly. HSTS header insertion is optional so that we can disable it to avoid duplicated header when API gateway is doing it. Version 1.12.26, 1/4/2020 Added Feature to disable PoJo deserialization so that caller can decide if the result set should be in PoJo or a Map. Removed N/A Changed Simplified key management for Event Node AsyncHttpRequest case insensitivity for headers, cookies, path parameters and session key-values Make built-in configuration management optional Version 1.12.19, 12/28/2019 Added Added HTTP relay feature in rest-automation project Removed N/A Changed Improved hazelcast retry and peer discovery logic Refactored rest-automation's service gateway module to use AsyncHttpRequest Info endpoint to show routing table of a peer Version 1.12.17, 12/16/2019 Added Simple configuration management is added to event-node, hazelcast-presence and kafka-presence monitors Added BeforeApplication annotation - this allows user application to execute some setup logic before the main application starts. e.g. modifying parameters in application.properties Added API playground as a convenient standalone application to render OpenAPI 2.0 and 3.0 yaml and json files Added argument parser in rest-automation helper app to use a static HTML folder in the local file system if arguments -html file_path is given when starting the JAR file. Removed N/A Changed Kafka publisher timeout value changed from 10 to 20 seconds Log a warning when Kafka takes more than 5 seconds to send an event Version 1.12.14, 11/20/2019 Added getRoute() method is added to PostOffice to facilitate RBAC The route name of the current service is added to an outgoing event when the \"from\" field is not present Simple RBAC using YAML configuration instead of code Removed N/A Changed Updated Spring Boot to v2.2.1 Version 1.12.12, 10/26/2019 Added Multi-tenancy support for event streams (Hazelcast and Kafka). This allows the use of a single event stream cluster for multiple non-prod environments. For production, it must use a separate event stream cluster for security reason. Removed N/A Changed logging framework changed from logback to log4j2 (version 2.12.1) Use JSR-356 websocket annotated ClientEndpoint Improved websocket reconnection logic Version 1.12.9, 9/14/2019 Added Distributed tracing implemented in platform-core and rest-automation Improved HTTP header transformation for rest-automation Removed N/A Changed language pack API key obtained from environment variable Version 1.12.8, 8/15/2019 Added N/A Removed rest-core subproject has been merged with rest-spring Changed N/A Version 1.12.7, 7/15/2019 Added Periodic routing table integrity check (15 minutes) Set kafka read pointer to the beginning for new application instances except presence monitor REST automation helper application in the \"extensions\" project Support service discovery of multiple routes in the updated PostOffice's exists() method logback to set log level based on environment variable LOG_LEVEL (default is INFO) Removed N/A Changed Minor refactoring of kafka-connector and hazelcast-connector to ensure that they can coexist if you want to include both of these dependencies in your project. This is for convenience of dev and testing. In production, please select only one cloud connector library to reduce memory footprint. Version 1.12.4, 6/24/2019 Added Add inactivity expiry timer to ObjectStreamIO so that house-keeper can clean up resources that are idle Removed N/A Changed Disable HTML encape sequence for GSON serializer Bug fix for GSON serialization optimization Bug fix for Object Stream housekeeper By default, GSON serializer converts all numbers to double, resulting in unwanted decimal point for integer and long. To handle custom map serialization for correct representation of numbers, an unintended side effect was introduced in earlier releases. List of inner PoJo would be incorrectly serialized as map, resulting in casting exception. This release resolves this issue. Version 1.12.1, 6/10/2019 Added Store-n-forward pub/sub API will be automatically enabled if the underlying cloud connector supports it. e.g. kafka ObjectStreamIO, a convenient wrapper class, to provide event stream I/O API. Object stream feature is now a standard feature instead of optional. Deferred delivery added to language connector. Removed N/A Changed N/A Version 1.11.40, 5/25/2019 Added Route substitution for simple versioning use case Add \"Strict Transport Security\" header if HTTPS (https://tools.ietf.org/html/rfc6797) Event stream connector for Kafka Distributed housekeeper feature for Hazelcast connector Removed System log service Changed Refactoring of Hazelcast event stream connector library to sync up with the new Kafka connector. Version 1.11.39, 4/30/2019 Added Language-support service application for Python, Node.js and Go, etc. Python language pack project is available at https://github.com/Accenture/mercury-python Removed N/A Changed replace Jackson serialization engine with Gson ( platform-core project) replace Apache HttpClient with Google Http Client ( rest-spring ) remove Jackson dependencies from Spring Boot ( rest-spring ) interceptor improvement Version 1.11.33, 3/25/2019 Added N/A Removed N/A Changed Move safe.data.models validation rules from EventEnvelope to SimpleMapper Apache fluent HTTP client downgraded to version 4.5.6 because the pom file in 4.5.7 is invalid Version 1.11.30, 3/7/2019 Added Added retry logic in persistent queue when OS cannot update local file metadata in real-time for Windows based machine. Removed N/A Changed pom.xml changes - update with latest 3rd party open sources dependencies. Version 1.11.29, 1/25/2019 Added platform-core Support for long running functions so that any long queries will not block the rest of the system. \"safe.data.models\" is available as an option in the application.properties. This is an additional security measure to protect against Jackson deserialization vulnerability. See example below: # # additional security to protect against model injection # comma separated list of model packages that are considered safe to be used for object deserialization # #safe.data.models=com.accenture.models rest-spring \"/env\" endpoint is added. See sample application.properties below: # # environment and system properties to be exposed to the \"/env\" admin endpoint # show.env.variables=USER, TEST show.application.properties=server.port, cloud.connector Removed N/A Changed platform-core Use Java Future and an elastic cached thread pool for executing user functions. Fixed N/A Version 1.11.28, 12/20/2018 Added Hazelcast support is added. This includes two projects (hazelcast-connector and hazelcast-presence). Hazelcast-connector is a cloud connector library. Hazelcast-presence is the \"Presence Monitor\" for monitoring the presence status of each application instance. Removed platform-core The \"fixed resource manager\" feature is removed because the same outcome can be achieved at the application level. e.g. The application can broadcast requests to multiple application instances with the same route name and use a callback function to receive response asynchronously. The services can provide resource metrics so that the caller can decide which is the most available instance to contact. For simplicity, resources management is better left to the cloud platform or the application itself. Changed N/A Fixed N/A","title":"Release notes"},{"location":"CHANGELOG/#changelog","text":"","title":"Changelog"},{"location":"CHANGELOG/#release-notes","text":"All notable changes to this project will be documented in this file. The format is based on Keep a Changelog , and this project adheres to Semantic Versioning .","title":"Release notes"},{"location":"CHANGELOG/#version-3016-8312024","text":"","title":"Version 3.0.16, 8/31/2024"},{"location":"CHANGELOG/#added","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed","text":"Updated documentation AsyncHttpRequest - remove the class type variable in the data model MultiLevelMap - update the removeElement method OSS update - GSON 2.11.0, classpath 4.8.174, guava 33.3.0-jre, vertx 2.5.9, spring boot 3.3.3, spring framework 5.3.39","title":"Changed"},{"location":"CHANGELOG/#version-3015-512024","text":"This version supercedes 3.0.13 and 3.0.14 due to updated data structure for static content handling.","title":"Version 3.0.15, 5/1/2024"},{"location":"CHANGELOG/#added_1","text":"Added optional static-content.no-cache-pages in rest.yaml AsyncHttpClientLoader","title":"Added"},{"location":"CHANGELOG/#removed_1","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_1","text":"Updated data structure for static-content section in rest.yaml Fixed bug for setting multiple HTTP cookies Unified configuration file prefix \"yaml.\"","title":"Changed"},{"location":"CHANGELOG/#version-3014-4282024","text":"","title":"Version 3.0.14, 4/28/2024"},{"location":"CHANGELOG/#added_2","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_2","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_2","text":"Updated syntax for static-content-filter section in rest.yaml","title":"Changed"},{"location":"CHANGELOG/#version-3013-4282024","text":"","title":"Version 3.0.13, 4/28/2024"},{"location":"CHANGELOG/#added_3","text":"Added optional static content HTTP-GET request filter in rest.yaml","title":"Added"},{"location":"CHANGELOG/#removed_3","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_3","text":"Updated guava to version 33.1.0-jre","title":"Changed"},{"location":"CHANGELOG/#version-3012-4242024","text":"","title":"Version 3.0.12, 4/24/2024"},{"location":"CHANGELOG/#added_4","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_4","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_4","text":"Enhanced OptionalService annotation","title":"Changed"},{"location":"CHANGELOG/#version-3011-4232024","text":"Backport KernelThreadRunner from Mercury Composable 3.1","title":"Version 3.0.11, 4/23/2024"},{"location":"CHANGELOG/#added_5","text":"KernelThreadRunner annotation","title":"Added"},{"location":"CHANGELOG/#removed_5","text":"CoroutineRunner annotation","title":"Removed"},{"location":"CHANGELOG/#changed_5","text":"Set default execution strategy to \"coroutine\". To tell the system to run a function using kernel thread pool, add the KernelThreadRunner annotation. Update PersistentWsClient to use vertx WebSocketClient. Upgrade netty to version 4.1.109.Final.","title":"Changed"},{"location":"CHANGELOG/#version-3010-3192024","text":"","title":"Version 3.0.10, 3/19/2024"},{"location":"CHANGELOG/#added_6","text":"Added \"app-config-reader.yml\" file in the resources folder so that you can override the default application configuration files.","title":"Added"},{"location":"CHANGELOG/#removed_6","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_6","text":"Open sources library update (Spring Boot 3.2.4, Vertx 4.5.5) Improve AppConfigReader and ConfigReader to use the app-config-reader.yml file.","title":"Changed"},{"location":"CHANGELOG/#version-309-292024","text":"","title":"Version 3.0.9, 2/9/2024"},{"location":"CHANGELOG/#added_7","text":"AutoStart to run application as Spring Boot if the rest-spring-3 library is packaged in app","title":"Added"},{"location":"CHANGELOG/#removed_7","text":"Bugfix: removed websocket client connection timeout that causes the first connection to drop after one minute","title":"Removed"},{"location":"CHANGELOG/#changed_7","text":"Open sources library update (Spring Boot 3.2.2, Vertx 4.5.3 and MsgPack 0.9.8) Rename application parameter \"event.worker.pool\" to \"kernel.thread.pool\" Support user defined serializer with PreLoad annotation and platform API","title":"Changed"},{"location":"CHANGELOG/#version-308-1272024","text":"","title":"Version 3.0.8, 1/27/2024"},{"location":"CHANGELOG/#added_8","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_8","text":"ActiveMQ and Tibco cloud connectors","title":"Removed"},{"location":"CHANGELOG/#changed_8","text":"Updated AsyncHttpRequest and CryptoApi","title":"Changed"},{"location":"CHANGELOG/#version-307-12232023","text":"","title":"Version 3.0.7, 12/23/2023"},{"location":"CHANGELOG/#added_9","text":"Print out basic JVM information before startup for verification of base container image.","title":"Added"},{"location":"CHANGELOG/#removed_9","text":"Removed Maven Shade packager","title":"Removed"},{"location":"CHANGELOG/#changed_9","text":"Updated open sources libraries to address security vulnerabilities Spring Boot 2/3 to version 2.7.18 and 3.2.1 respectively Tomcat 9.0.84 Vertx 4.5.1 Classgraph 4.8.165 Netty 4.1.104.Final slf4j API 2.0.9 log4j2 2.22.0 Kotlin 1.9.22 Artemis 2.31.2 Hazelcast 5.3.6 Guava 33.0.0-jre","title":"Changed"},{"location":"CHANGELOG/#version-306-10262023","text":"","title":"Version 3.0.6, 10/26/2023"},{"location":"CHANGELOG/#added_10","text":"Enhanced Benchmark tool to support \"Event over HTTP\" protocol to evaluate performance efficiency for commmunication between application containers using HTTP.","title":"Added"},{"location":"CHANGELOG/#removed_10","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_10","text":"Updated open sources libraries Spring Boot 2/3 to version 2.7.17 and 3.1.5 respectively Kafka-client 3.6.0","title":"Changed"},{"location":"CHANGELOG/#version-305-10212023","text":"","title":"Version 3.0.5, 10/21/2023"},{"location":"CHANGELOG/#added_11","text":"Support two executable JAR packaging system: 1. Maven Shade packager 2. Spring Boot packager Starting from version 3.0.5, we have replaced Spring Boot packager with Maven Shade. This avoids a classpath edge case for Spring Boot packager when running kafka-client under Java 11 or higher. Maven Shade also results in smaller executable JAR size.","title":"Added"},{"location":"CHANGELOG/#removed_11","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_11","text":"Updated open sources libraries Spring-Boot 2.7.16 / 3.1.4 classgraph 4.8.163 snakeyaml 2.2 kotlin 1.9.10 vertx 4.4.6 guava 32.1.3-jre msgpack 0.9.6 slj4j 2.0.9 zookeeper 3.7.2 The \"/info/lib\" admin endpoint has been enhanced to list library dependencies for executable JAR generated by either Maven Shade or Spring Boot Packager. Improved ConfigReader to recognize both \".yml\" and \".yaml\" extensions and their uses are interchangeable.","title":"Changed"},{"location":"CHANGELOG/#version-304-862023","text":"","title":"Version 3.0.4, 8/6/2023"},{"location":"CHANGELOG/#added_12","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_12","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_12","text":"Updated open sources libraries Spring-Boot 2.7.14 / 3.1.2 Kafka-client 3.5.1 classgraph 4.8.161 guava 32.1.2-jre msgpack 0.9.5","title":"Changed"},{"location":"CHANGELOG/#version-303-6272023","text":"","title":"Version 3.0.3, 6/27/2023"},{"location":"CHANGELOG/#added_13","text":"File extension to MIME type mapping for static HTML file handling","title":"Added"},{"location":"CHANGELOG/#removed_13","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_13","text":"Open sources library update - Kotlin version 1.9.0","title":"Changed"},{"location":"CHANGELOG/#version-302-692023","text":"","title":"Version 3.0.2, 6/9/2023"},{"location":"CHANGELOG/#added_14","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_14","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_14","text":"Consistent exception handling for Event API endpoint Open sources lib update - Vertx 4.4.4, Spring Boot 2.7.13, Spring Boot 3.1.1, classgraph 4.8.160, guava 32.0.1-jre","title":"Changed"},{"location":"CHANGELOG/#version-301-652023","text":"In this release, we have replace Google HTTP Client with vertx non-blocking WebClient. We also tested compatibility up to OpenJDK version 20 and maven 3.9.2.","title":"Version 3.0.1, 6/5/2023"},{"location":"CHANGELOG/#added_15","text":"When \"x-raw-xml\" HTTP request header is set to \"true\", the AsyncHttpClient will skip the built-in XML serialization so that your application can retrieve the original XML text.","title":"Added"},{"location":"CHANGELOG/#removed_15","text":"Retire Google HTTP client","title":"Removed"},{"location":"CHANGELOG/#changed_15","text":"Upgrade maven plugin versions.","title":"Changed"},{"location":"CHANGELOG/#version-300-4182023","text":"This is a major release with some breaking changes. Please refer to Chapter-10 (Migration guide) for details. This version brings the best of preemptive and cooperating multitasking to Java (version 1.8 to 19) before Java 19 virtual thread feature becomes officially available.","title":"Version 3.0.0, 4/18/2023"},{"location":"CHANGELOG/#added_16","text":"Function execution engine supporting kernel thread pool, Kotlin coroutine and suspend function \"Event over HTTP\" service for inter-container communication Support for Spring Boot version 3 and WebFlux Sample code for a pre-configured Spring Boot 3 application","title":"Added"},{"location":"CHANGELOG/#removed_16","text":"Remove blocking APIs from platform-core Retire PM2 process manager sample script due to compatibility issue","title":"Removed"},{"location":"CHANGELOG/#changed_16","text":"Refactor \"async.http.request\" to use vertx web client for non-blocking operation Update log4j2 version 2.20.0 and slf4j version 2.0.7 in platform-core Update JBoss RestEasy JAX_RS to version 3.15.6.Final in rest-spring Update vertx to 4.4.2 Update Spring Boot parent pom to 2.7.12 and 3.1.0 for spring boot 2 and 3 respectively Remove com.fasterxml.classmate dependency from rest-spring","title":"Changed"},{"location":"CHANGELOG/#version-280-3202023","text":"","title":"Version 2.8.0, 3/20/2023"},{"location":"CHANGELOG/#added_17","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_17","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_17","text":"Improved load balancing in cloud-connector Filter URI to avoid XSS attack Upgrade to SnakeYaml 2.0 and patch Spring Boot 2.6.8 for compatibility with it Upgrade to Vertx 4.4.0, classgraph 4.8.157, tomcat 9.0.73","title":"Changed"},{"location":"CHANGELOG/#version-271-12222022","text":"","title":"Version 2.7.1, 12/22/2022"},{"location":"CHANGELOG/#added_18","text":"standalone benchmark report app client and server benchmark apps add timeout tag to RPC events","title":"Added"},{"location":"CHANGELOG/#removed_18","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_18","text":"Updated open sources dependencies Netty 4.1.86.Final Tomcat 9.0.69 Vertx 4.3.6 classgraph 4.8.152 google-http-client 1.42.3 Improved unit tests to use assertThrows to evaluate exception Enhanced AsyncHttpRequest serialization","title":"Changed"},{"location":"CHANGELOG/#version-270-11112022","text":"In this version, REST automation code is moved to platform-core such that REST and Websocket service can share the same port.","title":"Version 2.7.0, 11/11/2022"},{"location":"CHANGELOG/#added_19","text":"AsyncObjectStreamReader is added for non-blocking read operation from an object stream. Support of LocalDateTime in SimpleMapper Add \"removeElement\" method to MultiLevelMap Automatically convert a map to a PoJo when the sender does not specify class in event body","title":"Added"},{"location":"CHANGELOG/#removed_19","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_19","text":"REST automation becomes part of platform-core and it can co-exist with Spring Web in the rest-spring module Enforce Spring Boot lifecycle management such that user apps will start after Spring Boot has loaded all components Update netty to version 4.1.84.Final","title":"Changed"},{"location":"CHANGELOG/#version-260-10132022","text":"In this version, websocket notification example code has been removed from the REST automation system. If your application uses this feature, please recover the code from version 2.5.0 and refactor it as a separate library.","title":"Version 2.6.0, 10/13/2022"},{"location":"CHANGELOG/#added_20","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_20","text":"Simplify REST automation system by removing websocket notification example in REST automation.","title":"Removed"},{"location":"CHANGELOG/#changed_20","text":"Replace Tomcat websocket server with Vertx non-blocking websocket server library Update netty to version 4.1.79.Final Update kafka client to version 2.8.2 Update snake yaml to version 1.33 Update gson to version 2.9.1","title":"Changed"},{"location":"CHANGELOG/#version-250-9102022","text":"","title":"Version 2.5.0, 9/10/2022"},{"location":"CHANGELOG/#added_21","text":"New Preload annotation class to automate pre-registration of LambdaFunction.","title":"Added"},{"location":"CHANGELOG/#removed_21","text":"Removed Spring framework and Tomcat dependencies from platform-core so that the core library can be applied to legacy J2EE application without library conflict.","title":"Removed"},{"location":"CHANGELOG/#changed_21","text":"Bugfix for proper housekeeping of future events. Make Gson and MsgPack handling of integer/long consistent Updated open sources libraries. Eclipse vertx-core version 4.3.4 MsgPack version 0.9.3 Google httpclient version 1.42.2 SnakeYaml version 1.31","title":"Changed"},{"location":"CHANGELOG/#version-236-6212022","text":"","title":"Version 2.3.6, 6/21/2022"},{"location":"CHANGELOG/#added_22","text":"Support more than one event stream cluster. User application can share the same event stream cluster for pub/sub or connect to an alternative cluster for pub/sub use cases.","title":"Added"},{"location":"CHANGELOG/#removed_22","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_22","text":"Cloud connector libraries update to Hazelcast 5.1.2","title":"Changed"},{"location":"CHANGELOG/#version-235-5302022","text":"","title":"Version 2.3.5, 5/30/2022"},{"location":"CHANGELOG/#added_23","text":"Add tagging feature to handle language connector's routing and exception handling","title":"Added"},{"location":"CHANGELOG/#removed_23","text":"Remove language pack's pub/sub broadcast feature","title":"Removed"},{"location":"CHANGELOG/#changed_23","text":"Update Spring Boot parent to version 2.6.8 to fetch Netty 4.1.77 and Spring Framework 5.3.20 Streamlined language connector transport protocol for compatibility with both Python and Node.js","title":"Changed"},{"location":"CHANGELOG/#version-234-5142022","text":"","title":"Version 2.3.4, 5/14/2022"},{"location":"CHANGELOG/#added_24","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_24","text":"Remove swagger-ui distribution from api-playground such that developer can clone the latest version","title":"Removed"},{"location":"CHANGELOG/#changed_24","text":"Update application.properties (from spring.resources.static-locations to spring.web.resources.static-locations) Update log4j, Tomcat and netty library version using Spring parent 2.6.6","title":"Changed"},{"location":"CHANGELOG/#version-233-3302022","text":"","title":"Version 2.3.3, 3/30/2022"},{"location":"CHANGELOG/#added_25","text":"Enhanced AsyncRequest to handle non-blocking fork-n-join","title":"Added"},{"location":"CHANGELOG/#removed_25","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_25","text":"Upgrade Spring Boot from 2.6.3 to 2.6.6","title":"Changed"},{"location":"CHANGELOG/#version-232-2212022","text":"","title":"Version 2.3.2, 2/21/2022"},{"location":"CHANGELOG/#added_26","text":"Add support of queue API in native pub/sub module for improved ESB compatibility","title":"Added"},{"location":"CHANGELOG/#removed_26","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_26","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-231-2192022","text":"","title":"Version 2.3.1, 2/19/2022"},{"location":"CHANGELOG/#added_27","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_27","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_27","text":"Update Vertx to version 4.2.4 Update Tomcat to version 5.0.58 Use Tomcat websocket server for presence monitors Bugfix - Simple Scheduler's leader election searches peers correctly","title":"Changed"},{"location":"CHANGELOG/#version-230-1282022","text":"","title":"Version 2.3.0, 1/28/2022"},{"location":"CHANGELOG/#added_28","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_28","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_28","text":"Update copyright notice Update Vertx to version 4.2.3 Bugfix - RSA key generator supporting key length from 1024 to 4096 bits CryptoAPI - support different AES algorithms and custom IV Update Spring Boot to version 2.6.3","title":"Changed"},{"location":"CHANGELOG/#version-223-12292021","text":"","title":"Version 2.2.3, 12/29/2021"},{"location":"CHANGELOG/#added_29","text":"Transaction journaling Add parameter distributed.trace.aggregation in application.properties such that trace aggregation may be disabled.","title":"Added"},{"location":"CHANGELOG/#removed_29","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_29","text":"Update JBoss RestEasy library to 3.15.3.Final Improved po.search(route) to scan local and remote service registries. Added \"remoteOnly\" selection. Fix bug in releasing presence monitor topic for specific closed user group Update Apache log4j to version 2.17.1 Update Spring Boot parent to version 2.6.1 Update Netty to version 4.1.72.Final Update Vertx to version 4.2.2 Convenient class \"UserNotification\" for backend service to publish events to the UI when REST automation is deployed","title":"Changed"},{"location":"CHANGELOG/#version-222-11122021","text":"","title":"Version 2.2.2, 11/12/2021"},{"location":"CHANGELOG/#added_30","text":"User defined API authentication functions can be selected using custom HTTP request header \"Exception chaining\" feature in EventEnvelope New \"deferred.commit.log\" parameter for backward compatibility with older PowerMock in unit tests","title":"Added"},{"location":"CHANGELOG/#removed_30","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_30","text":"Improved and streamlined SimpleXmlParser to handle arrays Bugfix for file upload in Service Gateway (REST automation library) Update Tomcat library from 9.0.50 to 9.0.54 Update Spring Boot library to 2.5.6 Update GSON library to 2.8.9","title":"Changed"},{"location":"CHANGELOG/#version-221-1012021","text":"","title":"Version 2.2.1, 10/1/2021"},{"location":"CHANGELOG/#added_31","text":"Callback function can implement ServiceExceptionHandler to catch exception. It adds the onError() method.","title":"Added"},{"location":"CHANGELOG/#removed_31","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_31","text":"Open sources library update - Vert.x 4.1.3, Netty 4.1.68-Final","title":"Changed"},{"location":"CHANGELOG/#version-211-9102021","text":"","title":"Version 2.1.1, 9/10/2021"},{"location":"CHANGELOG/#added_32","text":"User defined PoJo and Generics mapping Standardized serializers for default case, snake_case and camelCase Support of EventEnvelope as input parameter in TypedLambdaFunction so application function can inspect event's metadata Application can subscribe to life cycle events of other application instances","title":"Added"},{"location":"CHANGELOG/#removed_32","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_32","text":"Replace Tomcat websocket server engine with Vertx in presence monitor for higher performance Bugfix for MsgPack transport of integer, long, BigInteger and BigDecimal","title":"Changed"},{"location":"CHANGELOG/#version-210-7252021","text":"","title":"Version 2.1.0, 7/25/2021"},{"location":"CHANGELOG/#added_33","text":"Multicast - application can define a multicast.yaml config to relay events to more than one target service. StreamFunction - function that allows the application to control back-pressure","title":"Added"},{"location":"CHANGELOG/#removed_33","text":"\"object.streams.io\" route is removed from platform-core","title":"Removed"},{"location":"CHANGELOG/#changed_33","text":"Elastic Queue - Refactored using Oracle Berkeley DB Object stream I/O - simplified design using the new StreamFunction feature Open sources library update - Spring Boot 2.5.2, Tomcat 9.0.50, Vert.x 4.1.1, Netty 4.1.66-Final","title":"Changed"},{"location":"CHANGELOG/#version-200-552021","text":"Vert.x is introduced as the in-memory event bus","title":"Version 2.0.0, 5/5/2021"},{"location":"CHANGELOG/#added_34","text":"ActiveMQ and Tibco connectors Admin endpoints to stop, suspend and resume an application instance Handle edge case to detect stalled application instances Add \"isStreamingPubSub\" method to the PubSub interface","title":"Added"},{"location":"CHANGELOG/#removed_34","text":"Event Node event stream emulator has been retired. You may use standalone Kafka server as a replacement for development and testing in your laptop. Multi-tenancy namespace configuration has been retired. It is replaced by the \"closed user group\" feature.","title":"Removed"},{"location":"CHANGELOG/#changed_34","text":"Refactored Kafka and Hazelcast connectors to support virtual topics and closed user groups. Updated ConfigReader to be consistent with Spring value substitution logic for application properties Replace Akka actor system with Vert.x event bus Common code for various cloud connectors consolidated into cloud core libraries","title":"Changed"},{"location":"CHANGELOG/#version-1130-1152021","text":"Version 1.13.0 is the last version that uses Akka as the in-memory event system.","title":"Version 1.13.0, 1/15/2021"},{"location":"CHANGELOG/#version-11266-1152021","text":"","title":"Version 1.12.66, 1/15/2021"},{"location":"CHANGELOG/#added_35","text":"A simple websocket notification service is integrated into the REST automation system Seamless migration feature is added to the REST automation system","title":"Added"},{"location":"CHANGELOG/#removed_35","text":"Legacy websocket notification example application","title":"Removed"},{"location":"CHANGELOG/#changed_35","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-11265-1292020","text":"","title":"Version 1.12.65, 12/9/2020"},{"location":"CHANGELOG/#added_36","text":"\"kafka.pubsub\" is added as a cloud service File download example in the lambda-example project \"trace.log.header\" added to application.properties - when tracing is enabled, this inserts the trace-ID of the transaction in the log context. For more details, please refer to the Developer Guide Add API to pub/sub engine to support creation of topic with partitions TypedLambdaFunction is added so that developer can predefine input and output classes in a service without casting","title":"Added"},{"location":"CHANGELOG/#removed_36","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_36","text":"Decouple Kafka pub/sub from kafka connector so that native pub/sub can be used when application is running in standalone mode Rename \"relay\" to \"targetHost\" in AsyncHttpRequest data model Enhanced routing table distribution by sending a complete list of route tables, thus reducing network admin traffic.","title":"Changed"},{"location":"CHANGELOG/#version-11264-9282020","text":"","title":"Version 1.12.64, 9/28/2020"},{"location":"CHANGELOG/#added_37","text":"If predictable topic is set, application instances will report their predictable topics as \"instance ID\" to the presence monitor. This improves visibility when a developer tests their application in \"hybrid\" mode. i.e. running the app locally and connect to the cloud remotely for event streams and cloud resources.","title":"Added"},{"location":"CHANGELOG/#removed_37","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_37","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-11263-8272020","text":"","title":"Version 1.12.63, 8/27/2020"},{"location":"CHANGELOG/#added_38","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_38","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_38","text":"Improved Kafka producer and consumer pairing","title":"Changed"},{"location":"CHANGELOG/#version-11262-8122020","text":"","title":"Version 1.12.62, 8/12/2020"},{"location":"CHANGELOG/#added_39","text":"New presence monitor's admin endpoint for the operator to force routing table synchronization (\"/api/ping/now\")","title":"Added"},{"location":"CHANGELOG/#removed_39","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_39","text":"Improved routing table integrity check","title":"Changed"},{"location":"CHANGELOG/#version-11261-882020","text":"","title":"Version 1.12.61, 8/8/2020"},{"location":"CHANGELOG/#added_40","text":"Event stream systems like Kafka assume topic to be used long term. This version adds support to reuse the same topic when an application instance restarts. You can create a predictable topic using unique application name and instance ID. For example, with Kubernetes, you can use the POD name as the unique application instance topic.","title":"Added"},{"location":"CHANGELOG/#removed_40","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_40","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-11256-842020","text":"","title":"Version 1.12.56, 8/4/2020"},{"location":"CHANGELOG/#added_41","text":"Automate trace for fork-n-join use case","title":"Added"},{"location":"CHANGELOG/#removed_41","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_41","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-11255-7192020","text":"","title":"Version 1.12.55, 7/19/2020"},{"location":"CHANGELOG/#added_42","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_42","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_42","text":"Improved distributed trace - set the \"from\" address in EventEnvelope automatically.","title":"Changed"},{"location":"CHANGELOG/#version-11254-7102020","text":"","title":"Version 1.12.54, 7/10/2020"},{"location":"CHANGELOG/#added_43","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_43","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_43","text":"Application life-cycle management - User provided main application(s) will be started after Spring Boot declares web application ready. This ensures correct Spring autowiring or dependencies are available. Bugfix for locale - String.format(float) returns comma as decimal point that breaks number parser. Replace with BigDecimal decimal point scaling. Bugfix for Tomcat 9.0.35 - Change Async servlet default timeout from 30 seconds to -1 so the system can handle the whole life-cycle directly.","title":"Changed"},{"location":"CHANGELOG/#version-11252-6112020","text":"","title":"Version 1.12.52, 6/11/2020"},{"location":"CHANGELOG/#added_44","text":"new \"search\" method in Post Office to return a list of application instances for a service simple \"cron\" job scheduler as an extension project add \"sequence\" to MainApplication annotation for orderly execution when more than one MainApplication is available support \"Optional\" object in EventEnvelope so a LambdaFunction can read and return Optional","title":"Added"},{"location":"CHANGELOG/#removed_44","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_44","text":"The rest-spring library has been updated to support both JAR and WAR deployment All pom.xml files updated accordingly PersistentWsClient will back off for 10 seconds when disconnected by remote host","title":"Changed"},{"location":"CHANGELOG/#version-11250-5202020","text":"","title":"Version 1.12.50, 5/20/2020"},{"location":"CHANGELOG/#added_45","text":"Payload segmentation For large payload in an event, the payload is automatically segmented into 64 KB segments. When there are more than one target application instances, the system ensures that the segments of the same event is delivered to exactly the same target. PersistentWsClient added - generalized persistent websocket client for Event Node, Kafka reporter and Hazelcast reporter.","title":"Added"},{"location":"CHANGELOG/#removed_45","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_45","text":"Code cleaning to improve consistency Upgraded to hibernate-validator to v6.1.5.Final and Hazelcast version 4.0.1 REST automation is provided as a library and an application to handle different use cases","title":"Changed"},{"location":"CHANGELOG/#version-11240-542020","text":"","title":"Version 1.12.40, 5/4/2020"},{"location":"CHANGELOG/#added_46","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_46","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_46","text":"For security reason, upgrade log4j to version 2.13.2","title":"Changed"},{"location":"CHANGELOG/#version-11239-532020","text":"","title":"Version 1.12.39, 5/3/2020"},{"location":"CHANGELOG/#added_47","text":"Use RestEasy JAX-RS library","title":"Added"},{"location":"CHANGELOG/#removed_47","text":"For security reason, removed Jersey JAX-RS library","title":"Removed"},{"location":"CHANGELOG/#changed_47","text":"Updated RestLoader to initialize RestEasy servlet dispatcher Support nested arrays in MultiLevelMap","title":"Changed"},{"location":"CHANGELOG/#version-11236-4162020","text":"","title":"Version 1.12.36, 4/16/2020"},{"location":"CHANGELOG/#added_48","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_48","text":"For simplicity, retire route-substitution admin endpoint. Route substitution uses a simple static table in route-substitution.yaml.","title":"Removed"},{"location":"CHANGELOG/#changed_48","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-11235-4122020","text":"","title":"Version 1.12.35, 4/12/2020"},{"location":"CHANGELOG/#added_49","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_49","text":"SimpleRBAC class is retired","title":"Removed"},{"location":"CHANGELOG/#changed_49","text":"Improved ConfigReader and AppConfigReader with automatic key-value normalization for YAML and JSON files Improved pub/sub module in kafka-connector","title":"Changed"},{"location":"CHANGELOG/#version-11234-3282020","text":"","title":"Version 1.12.34, 3/28/2020"},{"location":"CHANGELOG/#added_50","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_50","text":"Retired proprietary config manager since we can use the \"BeforeApplication\" approach to load config from Kubernetes configMap or other systems of config record.","title":"Removed"},{"location":"CHANGELOG/#changed_50","text":"Added \"isZero\" method to the SimpleMapper class Convert BigDecimal to string without scientific notation (i.e. toPlainString instead of toString) Corresponding unit tests added to verify behavior","title":"Changed"},{"location":"CHANGELOG/#version-11232-3142020","text":"","title":"Version 1.12.32, 3/14/2020"},{"location":"CHANGELOG/#added_51","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_51","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_51","text":"Kafka-connector will shutdown application instance when the EventProducer cannot send event to Kafka. This would allow the infrastructure to restart application instance automatically.","title":"Changed"},{"location":"CHANGELOG/#version-11231-2262020","text":"","title":"Version 1.12.31, 2/26/2020"},{"location":"CHANGELOG/#added_52","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_52","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_52","text":"Kafka-connector now supports external service provider for Kafka properties and credentials. If your application implements a function with route name \"kafka.properties.provider\" before connecting to cloud, the kafka-connector will retrieve kafka credentials on demand. This addresses case when kafka credentials change after application start-up. Interceptors are designed to forward requests and thus they do not generate replies. However, if you implement a function as an EventInterceptor, your function can throw exception just like a regular function and the exception will be returned to the calling function. This makes it easier to write interceptors.","title":"Changed"},{"location":"CHANGELOG/#version-11230-262020","text":"","title":"Version 1.12.30, 2/6/2020"},{"location":"CHANGELOG/#added_53","text":"Expose \"async.http.request\" as a PUBLIC function (\"HttpClient as a service\")","title":"Added"},{"location":"CHANGELOG/#removed_53","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_53","text":"Improved Hazelcast client connection stability Improved Kafka native pub/sub","title":"Changed"},{"location":"CHANGELOG/#version-11229-1102020","text":"","title":"Version 1.12.29, 1/10/2020"},{"location":"CHANGELOG/#added_54","text":"Rest-automation will transport X-Trace-Id from/to Http request/response, therefore extending distributed trace across systems that support the X-Trace-Id HTTP header. Added endpoint and service to shutdown application instance.","title":"Added"},{"location":"CHANGELOG/#removed_54","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_54","text":"Updated SimpleXmlParser with XML External Entity (XXE) injection prevention. Bug fix for hazelcast recovery logic - when a hazelcast node is down, the app instance will restart the hazelcast client and reset routing table correctly. HSTS header insertion is optional so that we can disable it to avoid duplicated header when API gateway is doing it.","title":"Changed"},{"location":"CHANGELOG/#version-11226-142020","text":"","title":"Version 1.12.26, 1/4/2020"},{"location":"CHANGELOG/#added_55","text":"Feature to disable PoJo deserialization so that caller can decide if the result set should be in PoJo or a Map.","title":"Added"},{"location":"CHANGELOG/#removed_55","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_55","text":"Simplified key management for Event Node AsyncHttpRequest case insensitivity for headers, cookies, path parameters and session key-values Make built-in configuration management optional","title":"Changed"},{"location":"CHANGELOG/#version-11219-12282019","text":"","title":"Version 1.12.19, 12/28/2019"},{"location":"CHANGELOG/#added_56","text":"Added HTTP relay feature in rest-automation project","title":"Added"},{"location":"CHANGELOG/#removed_56","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_56","text":"Improved hazelcast retry and peer discovery logic Refactored rest-automation's service gateway module to use AsyncHttpRequest Info endpoint to show routing table of a peer","title":"Changed"},{"location":"CHANGELOG/#version-11217-12162019","text":"","title":"Version 1.12.17, 12/16/2019"},{"location":"CHANGELOG/#added_57","text":"Simple configuration management is added to event-node, hazelcast-presence and kafka-presence monitors Added BeforeApplication annotation - this allows user application to execute some setup logic before the main application starts. e.g. modifying parameters in application.properties Added API playground as a convenient standalone application to render OpenAPI 2.0 and 3.0 yaml and json files Added argument parser in rest-automation helper app to use a static HTML folder in the local file system if arguments -html file_path is given when starting the JAR file.","title":"Added"},{"location":"CHANGELOG/#removed_57","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_57","text":"Kafka publisher timeout value changed from 10 to 20 seconds Log a warning when Kafka takes more than 5 seconds to send an event","title":"Changed"},{"location":"CHANGELOG/#version-11214-11202019","text":"","title":"Version 1.12.14, 11/20/2019"},{"location":"CHANGELOG/#added_58","text":"getRoute() method is added to PostOffice to facilitate RBAC The route name of the current service is added to an outgoing event when the \"from\" field is not present Simple RBAC using YAML configuration instead of code","title":"Added"},{"location":"CHANGELOG/#removed_58","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_58","text":"Updated Spring Boot to v2.2.1","title":"Changed"},{"location":"CHANGELOG/#version-11212-10262019","text":"","title":"Version 1.12.12, 10/26/2019"},{"location":"CHANGELOG/#added_59","text":"Multi-tenancy support for event streams (Hazelcast and Kafka). This allows the use of a single event stream cluster for multiple non-prod environments. For production, it must use a separate event stream cluster for security reason.","title":"Added"},{"location":"CHANGELOG/#removed_59","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_59","text":"logging framework changed from logback to log4j2 (version 2.12.1) Use JSR-356 websocket annotated ClientEndpoint Improved websocket reconnection logic","title":"Changed"},{"location":"CHANGELOG/#version-1129-9142019","text":"","title":"Version 1.12.9, 9/14/2019"},{"location":"CHANGELOG/#added_60","text":"Distributed tracing implemented in platform-core and rest-automation Improved HTTP header transformation for rest-automation","title":"Added"},{"location":"CHANGELOG/#removed_60","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_60","text":"language pack API key obtained from environment variable","title":"Changed"},{"location":"CHANGELOG/#version-1128-8152019","text":"","title":"Version 1.12.8, 8/15/2019"},{"location":"CHANGELOG/#added_61","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_61","text":"rest-core subproject has been merged with rest-spring","title":"Removed"},{"location":"CHANGELOG/#changed_61","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-1127-7152019","text":"","title":"Version 1.12.7, 7/15/2019"},{"location":"CHANGELOG/#added_62","text":"Periodic routing table integrity check (15 minutes) Set kafka read pointer to the beginning for new application instances except presence monitor REST automation helper application in the \"extensions\" project Support service discovery of multiple routes in the updated PostOffice's exists() method logback to set log level based on environment variable LOG_LEVEL (default is INFO)","title":"Added"},{"location":"CHANGELOG/#removed_62","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_62","text":"Minor refactoring of kafka-connector and hazelcast-connector to ensure that they can coexist if you want to include both of these dependencies in your project. This is for convenience of dev and testing. In production, please select only one cloud connector library to reduce memory footprint.","title":"Changed"},{"location":"CHANGELOG/#version-1124-6242019","text":"","title":"Version 1.12.4, 6/24/2019"},{"location":"CHANGELOG/#added_63","text":"Add inactivity expiry timer to ObjectStreamIO so that house-keeper can clean up resources that are idle","title":"Added"},{"location":"CHANGELOG/#removed_63","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_63","text":"Disable HTML encape sequence for GSON serializer Bug fix for GSON serialization optimization Bug fix for Object Stream housekeeper By default, GSON serializer converts all numbers to double, resulting in unwanted decimal point for integer and long. To handle custom map serialization for correct representation of numbers, an unintended side effect was introduced in earlier releases. List of inner PoJo would be incorrectly serialized as map, resulting in casting exception. This release resolves this issue.","title":"Changed"},{"location":"CHANGELOG/#version-1121-6102019","text":"","title":"Version 1.12.1, 6/10/2019"},{"location":"CHANGELOG/#added_64","text":"Store-n-forward pub/sub API will be automatically enabled if the underlying cloud connector supports it. e.g. kafka ObjectStreamIO, a convenient wrapper class, to provide event stream I/O API. Object stream feature is now a standard feature instead of optional. Deferred delivery added to language connector.","title":"Added"},{"location":"CHANGELOG/#removed_64","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_64","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-11140-5252019","text":"","title":"Version 1.11.40, 5/25/2019"},{"location":"CHANGELOG/#added_65","text":"Route substitution for simple versioning use case Add \"Strict Transport Security\" header if HTTPS (https://tools.ietf.org/html/rfc6797) Event stream connector for Kafka Distributed housekeeper feature for Hazelcast connector","title":"Added"},{"location":"CHANGELOG/#removed_65","text":"System log service","title":"Removed"},{"location":"CHANGELOG/#changed_65","text":"Refactoring of Hazelcast event stream connector library to sync up with the new Kafka connector.","title":"Changed"},{"location":"CHANGELOG/#version-11139-4302019","text":"","title":"Version 1.11.39, 4/30/2019"},{"location":"CHANGELOG/#added_66","text":"Language-support service application for Python, Node.js and Go, etc. Python language pack project is available at https://github.com/Accenture/mercury-python","title":"Added"},{"location":"CHANGELOG/#removed_66","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_66","text":"replace Jackson serialization engine with Gson ( platform-core project) replace Apache HttpClient with Google Http Client ( rest-spring ) remove Jackson dependencies from Spring Boot ( rest-spring ) interceptor improvement","title":"Changed"},{"location":"CHANGELOG/#version-11133-3252019","text":"","title":"Version 1.11.33, 3/25/2019"},{"location":"CHANGELOG/#added_67","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_67","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_67","text":"Move safe.data.models validation rules from EventEnvelope to SimpleMapper Apache fluent HTTP client downgraded to version 4.5.6 because the pom file in 4.5.7 is invalid","title":"Changed"},{"location":"CHANGELOG/#version-11130-372019","text":"","title":"Version 1.11.30, 3/7/2019"},{"location":"CHANGELOG/#added_68","text":"Added retry logic in persistent queue when OS cannot update local file metadata in real-time for Windows based machine.","title":"Added"},{"location":"CHANGELOG/#removed_68","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_68","text":"pom.xml changes - update with latest 3rd party open sources dependencies.","title":"Changed"},{"location":"CHANGELOG/#version-11129-1252019","text":"","title":"Version 1.11.29, 1/25/2019"},{"location":"CHANGELOG/#added_69","text":"platform-core Support for long running functions so that any long queries will not block the rest of the system. \"safe.data.models\" is available as an option in the application.properties. This is an additional security measure to protect against Jackson deserialization vulnerability. See example below: # # additional security to protect against model injection # comma separated list of model packages that are considered safe to be used for object deserialization # #safe.data.models=com.accenture.models rest-spring \"/env\" endpoint is added. See sample application.properties below: # # environment and system properties to be exposed to the \"/env\" admin endpoint # show.env.variables=USER, TEST show.application.properties=server.port, cloud.connector","title":"Added"},{"location":"CHANGELOG/#removed_69","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_69","text":"platform-core Use Java Future and an elastic cached thread pool for executing user functions.","title":"Changed"},{"location":"CHANGELOG/#fixed","text":"N/A","title":"Fixed"},{"location":"CHANGELOG/#version-11128-12202018","text":"","title":"Version 1.11.28, 12/20/2018"},{"location":"CHANGELOG/#added_70","text":"Hazelcast support is added. This includes two projects (hazelcast-connector and hazelcast-presence). Hazelcast-connector is a cloud connector library. Hazelcast-presence is the \"Presence Monitor\" for monitoring the presence status of each application instance.","title":"Added"},{"location":"CHANGELOG/#removed_70","text":"platform-core The \"fixed resource manager\" feature is removed because the same outcome can be achieved at the application level. e.g. The application can broadcast requests to multiple application instances with the same route name and use a callback function to receive response asynchronously. The services can provide resource metrics so that the caller can decide which is the most available instance to contact. For simplicity, resources management is better left to the cloud platform or the application itself.","title":"Removed"},{"location":"CHANGELOG/#changed_70","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#fixed_1","text":"N/A","title":"Fixed"},{"location":"CODE_OF_CONDUCT/","text":"Contributor Covenant Code of Conduct Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. Our Standards Examples of behavior that contributes to creating a positive environment include: Using welcoming and inclusive language Being respectful of differing viewpoints and experiences Gracefully accepting constructive criticism Focusing on what is best for the community Showing empathy towards other community members Examples of unacceptable behavior by participants include: The use of sexualized language or imagery and unwelcome sexual attention or advances Trolling, insulting/derogatory comments, and personal or political attacks Public or private harassment Publishing others' private information, such as a physical or electronic address, without explicit permission Other conduct which could reasonably be considered inappropriate in a professional setting Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting Kevin Bader (the current project maintainer). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. Attribution This Code of Conduct is adapted from the Contributor Covenant , version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html","title":"Code of Conduct"},{"location":"CODE_OF_CONDUCT/#contributor-covenant-code-of-conduct","text":"","title":"Contributor Covenant Code of Conduct"},{"location":"CODE_OF_CONDUCT/#our-pledge","text":"In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.","title":"Our Pledge"},{"location":"CODE_OF_CONDUCT/#our-standards","text":"Examples of behavior that contributes to creating a positive environment include: Using welcoming and inclusive language Being respectful of differing viewpoints and experiences Gracefully accepting constructive criticism Focusing on what is best for the community Showing empathy towards other community members Examples of unacceptable behavior by participants include: The use of sexualized language or imagery and unwelcome sexual attention or advances Trolling, insulting/derogatory comments, and personal or political attacks Public or private harassment Publishing others' private information, such as a physical or electronic address, without explicit permission Other conduct which could reasonably be considered inappropriate in a professional setting","title":"Our Standards"},{"location":"CODE_OF_CONDUCT/#our-responsibilities","text":"Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.","title":"Our Responsibilities"},{"location":"CODE_OF_CONDUCT/#scope","text":"This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.","title":"Scope"},{"location":"CODE_OF_CONDUCT/#enforcement","text":"Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting Kevin Bader (the current project maintainer). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.","title":"Enforcement"},{"location":"CODE_OF_CONDUCT/#attribution","text":"This Code of Conduct is adapted from the Contributor Covenant , version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html","title":"Attribution"},{"location":"CONTRIBUTING/","text":"Contributing to the Mercury framework Thanks for taking the time to contribute! The following is a set of guidelines for contributing to Mercury and its packages, which are hosted in the Accenture Organization on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. Code of Conduct This project and everyone participating in it is governed by our Code of Conduct . By participating, you are expected to uphold this code. Please report unacceptable behavior to Kevin Bader, who is the current project maintainer. What should I know before I get started? We follow the standard GitHub workflow . Before submitting a Pull Request: Please write tests. Make sure you run all tests and check for warnings. Think about whether it makes sense to document the change in some way. For smaller, internal changes, inline documentation might be sufficient, while more visible ones might warrant a change to the developer's guide or the README . Update CHANGELOG.md file with your current change in form of [Type of change e.g. Config, Kafka, .etc] with a short description of what it is all about and a link to issue or pull request, and choose a suitable section (i.e., changed, added, fixed, removed, deprecated). Design Decisions When we make a significant decision in how to write code, or how to maintain the project and what we can or cannot support, we will document it using Architecture Decision Records (ADR) . Take a look at the design notes for existing ADRs. If you have a question around how we do things, check to see if it is documented there. If it is not documented there, please ask us - chances are you're not the only one wondering. Of course, also feel free to challenge the decisions by starting a discussion on the mailing list.","title":"Contribution"},{"location":"CONTRIBUTING/#contributing-to-the-mercury-framework","text":"Thanks for taking the time to contribute! The following is a set of guidelines for contributing to Mercury and its packages, which are hosted in the Accenture Organization on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.","title":"Contributing to the Mercury framework"},{"location":"CONTRIBUTING/#code-of-conduct","text":"This project and everyone participating in it is governed by our Code of Conduct . By participating, you are expected to uphold this code. Please report unacceptable behavior to Kevin Bader, who is the current project maintainer.","title":"Code of Conduct"},{"location":"CONTRIBUTING/#what-should-i-know-before-i-get-started","text":"We follow the standard GitHub workflow . Before submitting a Pull Request: Please write tests. Make sure you run all tests and check for warnings. Think about whether it makes sense to document the change in some way. For smaller, internal changes, inline documentation might be sufficient, while more visible ones might warrant a change to the developer's guide or the README . Update CHANGELOG.md file with your current change in form of [Type of change e.g. Config, Kafka, .etc] with a short description of what it is all about and a link to issue or pull request, and choose a suitable section (i.e., changed, added, fixed, removed, deprecated).","title":"What should I know before I get started?"},{"location":"CONTRIBUTING/#design-decisions","text":"When we make a significant decision in how to write code, or how to maintain the project and what we can or cannot support, we will document it using Architecture Decision Records (ADR) . Take a look at the design notes for existing ADRs. If you have a question around how we do things, check to see if it is documented there. If it is not documented there, please ask us - chances are you're not the only one wondering. Of course, also feel free to challenge the decisions by starting a discussion on the mailing list.","title":"Design Decisions"},{"location":"INCLUSIVITY/","text":"TECHNOLOGY INCLUSIVE LANGUAGE GUIDEBOOK As an organization, Accenture believes in building an inclusive workplace and contributing to a world where equality thrives. Certain terms or expressions can unintentionally harm, perpetuate damaging stereotypes, and insult people. Inclusive language avoids bias, slang terms, and word choices which express derision of groups of people based on race, gender, sexuality, or socioeconomic status. The Accenture North America Technology team created this guidebook to provide Accenture employees with a view into inclusive language and guidance for working to avoid its use\u2014helping to ensure that we communicate with respect, dignity and fairness. How to use this guide? As of 8/2023, Accenture has over 730,000 employees from diverse backgrounds, who perform consulting and delivery work for an equally diverse set of clients and partners. When communicating with your colleagues and representing Accenture, consider the connotation, however unintended, of certain terms in your written and verbal communication. The guidelines are intended to help you recognize non-inclusive words and understand potential meanings that these words might convey. Our goal with these recommendations is not to require you to use specific words, but to ask you to take a moment to consider how your audience may be affected by the language you choose. Inclusive Categories Non-inclusive term Replacement Explanation Race, Ethnicity & National Origin master primary client source leader Using the terms \u201cmaster/slave\u201d in this context inappropriately normalizes and minimizes the very large magnitude that slavery and its effects have had in our history. slave secondary replica follower blacklist deny list block list The term \u201cblacklist\u201d was first used in the early 1600s to describe a list of those who were under suspicion and thus not to be trusted, whereas \u201cwhitelist\u201d referred to those considered acceptable. Accenture does not want to promote the association of \u201cblack\u201d and negative, nor the connotation of \u201cwhite\u201d being the inverse, or positive. whitelist allow list approved list native original core feature Referring to \u201cnative\u201d vs \u201cnon-native\u201d to describe technology platforms carries overtones of minimizing the impact of colonialism on native people, and thus minimizes the negative associations the terminology has in the latter context. non-native non-original non-core feature Gender & Sexuality man-hours work-hours business-hours When people read the words \u2018man\u2019 or \u2018he,\u2019 people often picture males only. Usage of the male terminology subtly suggests that only males can perform certain work or hold certain jobs. Gender-neutral terms include the whole audience, and thus using terms such as \u201cbusiness executive\u201d instead of \u201cbusinessman,\u201d or informally, \u201cfolks\u201d instead of \u201cguys\u201d is preferable because it is inclusive. man-days work-days business-days Ability Status & (Dis)abilities sanity check insanity check confidence check quality check rationality check Using the \u201cHuman Engagement, People First\u2019 approach, putting people - all people - at the center is important. Denoting ability status in the context of inferior or problematic work implies that people with mental illnesses are inferior, wrong, or incorrect. dummy variables indicator variables Violence STONITH, kill, hit conclude cease discontinue Using the \u201cHuman Engagement, People First\u2019 approach, putting people - all people - at the center is important. Denoting ability status in the context of inferior or problematic work implies that people with mental illnesses are inferior, wrong, or incorrect. one throat to choke single point of contact primary contact This guidebook is a living document and will be updated as terminology evolves. We encourage our users to provide feedback on the effectiveness of this document and we welcome additional suggestions. Contact us at Technology_ProjectElevate@accenture.com .","title":"Inclusivity"},{"location":"INCLUSIVITY/#technology-inclusive-language-guidebook","text":"As an organization, Accenture believes in building an inclusive workplace and contributing to a world where equality thrives. Certain terms or expressions can unintentionally harm, perpetuate damaging stereotypes, and insult people. Inclusive language avoids bias, slang terms, and word choices which express derision of groups of people based on race, gender, sexuality, or socioeconomic status. The Accenture North America Technology team created this guidebook to provide Accenture employees with a view into inclusive language and guidance for working to avoid its use\u2014helping to ensure that we communicate with respect, dignity and fairness. How to use this guide? As of 8/2023, Accenture has over 730,000 employees from diverse backgrounds, who perform consulting and delivery work for an equally diverse set of clients and partners. When communicating with your colleagues and representing Accenture, consider the connotation, however unintended, of certain terms in your written and verbal communication. The guidelines are intended to help you recognize non-inclusive words and understand potential meanings that these words might convey. Our goal with these recommendations is not to require you to use specific words, but to ask you to take a moment to consider how your audience may be affected by the language you choose. Inclusive Categories Non-inclusive term Replacement Explanation Race, Ethnicity & National Origin master primary client source leader Using the terms \u201cmaster/slave\u201d in this context inappropriately normalizes and minimizes the very large magnitude that slavery and its effects have had in our history. slave secondary replica follower blacklist deny list block list The term \u201cblacklist\u201d was first used in the early 1600s to describe a list of those who were under suspicion and thus not to be trusted, whereas \u201cwhitelist\u201d referred to those considered acceptable. Accenture does not want to promote the association of \u201cblack\u201d and negative, nor the connotation of \u201cwhite\u201d being the inverse, or positive. whitelist allow list approved list native original core feature Referring to \u201cnative\u201d vs \u201cnon-native\u201d to describe technology platforms carries overtones of minimizing the impact of colonialism on native people, and thus minimizes the negative associations the terminology has in the latter context. non-native non-original non-core feature Gender & Sexuality man-hours work-hours business-hours When people read the words \u2018man\u2019 or \u2018he,\u2019 people often picture males only. Usage of the male terminology subtly suggests that only males can perform certain work or hold certain jobs. Gender-neutral terms include the whole audience, and thus using terms such as \u201cbusiness executive\u201d instead of \u201cbusinessman,\u201d or informally, \u201cfolks\u201d instead of \u201cguys\u201d is preferable because it is inclusive. man-days work-days business-days Ability Status & (Dis)abilities sanity check insanity check confidence check quality check rationality check Using the \u201cHuman Engagement, People First\u2019 approach, putting people - all people - at the center is important. Denoting ability status in the context of inferior or problematic work implies that people with mental illnesses are inferior, wrong, or incorrect. dummy variables indicator variables Violence STONITH, kill, hit conclude cease discontinue Using the \u201cHuman Engagement, People First\u2019 approach, putting people - all people - at the center is important. Denoting ability status in the context of inferior or problematic work implies that people with mental illnesses are inferior, wrong, or incorrect. one throat to choke single point of contact primary contact This guidebook is a living document and will be updated as terminology evolves. We encourage our users to provide feedback on the effectiveness of this document and we welcome additional suggestions. Contact us at Technology_ProjectElevate@accenture.com .","title":"TECHNOLOGY INCLUSIVE LANGUAGE GUIDEBOOK"},{"location":"ROADMAP/","text":"Technology roadmap for 2023 Language Packs Upgrade Mercury python and node.js projects to the Mercury 3.0 specifications. Java \"virtual thread\" feature Integrate with Java virtual thread feature when it becomes officially available in Java 19 around Q4, 2023.","title":"Roadmap"},{"location":"ROADMAP/#technology-roadmap-for-2023","text":"","title":"Technology roadmap for 2023"},{"location":"ROADMAP/#language-packs","text":"Upgrade Mercury python and node.js projects to the Mercury 3.0 specifications.","title":"Language Packs"},{"location":"ROADMAP/#java-virtual-thread-feature","text":"Integrate with Java virtual thread feature when it becomes officially available in Java 19 around Q4, 2023.","title":"Java \"virtual thread\" feature"},{"location":"arch-decisions/DESIGN-NOTES/","text":"Design notes Non-blocking design The foundation library (platform-core) has been integrated with Kotlin coroutine and suspend function features. We have also removed all blocking APIs from the platform-core library. Applications using Mercury version 2 blocking RPC calls should be refactored to \"suspend function\". This would reduce memory footprint and increase throughput. i.e. support more concurrent users and requests. Since many functions in an application may be waiting for responses from a database or from a REST endpoint, \"suspend function\" approach releases CPU resources during the \"wait\" state, thus contributing to higher application throughput. Low level control of function execution strategies You can precisely control how your functions execute, using kernel thread pool, coroutine or suspend function to address various use cases to yield the highest performance and throughput. Kernel threads provide the highest performance in terms of operations per second when the number of threads is smaller. As a rule of thumb, do not set \"kernel.thread.pool\" higher than 200. Coroutine is ideal for functions that execute very quickly to yield control to other coroutines. Suspend function should be used to support \"sequential non-blocking\" RPC or logic that requires artificial delay. You can use the \"awaitRequest\" and \"delay\" APIs respectively. Serialization Gson We are using Gson for its minimalist design. We have customized the serialization behavior to be similar to Jackson and other serializers. i.e. Integer and long values are kept without decimal points. For backward compatibility with Jackson, we have added the writeValueAsString, writeValueAsBytes and readValue methods. The convertValue method has been consolidated into the readValue method. For simplicity, custom serialization annotations are discouraged. MsgPack For efficient and serialization performance, we use MsgPack as schemaless binary transport for EventEnvelope that contains event metadata, headers and payload. Custom JSON and XML serializers For consistency, we have customized JAX-RS, Spring Boot and Servlet serialization and exception handlers. Reactive design Mercury uses the temporary local file system ( /tmp ) as an overflow area for events when the consumer is slower than the producer. This event buffering design means that user application does not have to handle back-pressure logic directly. However, it does not restrict you from implementing your flow-control logic. Vertx In Mercury version 1, the Akka actor system is used as the in-memory event bus. Since Mercury version 2, we have migrated from Akka to Eclipse Vertx. In Mercury version 3, we extend the engine to be fully non-blocking with low-level control of application performance and throughput. Java versions The platform-core library is backward compatible to Java 1.8 so that it can be used for IT modernization of legacy Java projects. However, the codebase has been tested with Java 1.8 to 19 for projects, and you can apply the platform-core library in your projects without JVM constraints. Spring Boot The platform-core includes a non-blocking HTTP and websocket server for standalone operation without Spring Boot or similar application server. You may also use the platform-core with Spring Boot or other frameworks. Customized Spring Boot The rest-spring-2-example project demonstrates the use of the rest-spring-2 library to build a Spring Boot 2 executable. The rest-spring-2 is a convenient library with customized Spring Boot serializers and exception handlers. A corresponding library and example application for Spring Boot version 3 is rest-spring-3 and rest-spring-3-example . Regular Spring Boot You can also use the platform-core library with a regular Spring Boot application if you prefer.","title":"Design notes"},{"location":"arch-decisions/DESIGN-NOTES/#design-notes","text":"","title":"Design notes"},{"location":"arch-decisions/DESIGN-NOTES/#non-blocking-design","text":"The foundation library (platform-core) has been integrated with Kotlin coroutine and suspend function features. We have also removed all blocking APIs from the platform-core library. Applications using Mercury version 2 blocking RPC calls should be refactored to \"suspend function\". This would reduce memory footprint and increase throughput. i.e. support more concurrent users and requests. Since many functions in an application may be waiting for responses from a database or from a REST endpoint, \"suspend function\" approach releases CPU resources during the \"wait\" state, thus contributing to higher application throughput.","title":"Non-blocking design"},{"location":"arch-decisions/DESIGN-NOTES/#low-level-control-of-function-execution-strategies","text":"You can precisely control how your functions execute, using kernel thread pool, coroutine or suspend function to address various use cases to yield the highest performance and throughput. Kernel threads provide the highest performance in terms of operations per second when the number of threads is smaller. As a rule of thumb, do not set \"kernel.thread.pool\" higher than 200. Coroutine is ideal for functions that execute very quickly to yield control to other coroutines. Suspend function should be used to support \"sequential non-blocking\" RPC or logic that requires artificial delay. You can use the \"awaitRequest\" and \"delay\" APIs respectively.","title":"Low level control of function execution strategies"},{"location":"arch-decisions/DESIGN-NOTES/#serialization","text":"","title":"Serialization"},{"location":"arch-decisions/DESIGN-NOTES/#gson","text":"We are using Gson for its minimalist design. We have customized the serialization behavior to be similar to Jackson and other serializers. i.e. Integer and long values are kept without decimal points. For backward compatibility with Jackson, we have added the writeValueAsString, writeValueAsBytes and readValue methods. The convertValue method has been consolidated into the readValue method. For simplicity, custom serialization annotations are discouraged.","title":"Gson"},{"location":"arch-decisions/DESIGN-NOTES/#msgpack","text":"For efficient and serialization performance, we use MsgPack as schemaless binary transport for EventEnvelope that contains event metadata, headers and payload.","title":"MsgPack"},{"location":"arch-decisions/DESIGN-NOTES/#custom-json-and-xml-serializers","text":"For consistency, we have customized JAX-RS, Spring Boot and Servlet serialization and exception handlers.","title":"Custom JSON and XML serializers"},{"location":"arch-decisions/DESIGN-NOTES/#reactive-design","text":"Mercury uses the temporary local file system ( /tmp ) as an overflow area for events when the consumer is slower than the producer. This event buffering design means that user application does not have to handle back-pressure logic directly. However, it does not restrict you from implementing your flow-control logic.","title":"Reactive design"},{"location":"arch-decisions/DESIGN-NOTES/#vertx","text":"In Mercury version 1, the Akka actor system is used as the in-memory event bus. Since Mercury version 2, we have migrated from Akka to Eclipse Vertx. In Mercury version 3, we extend the engine to be fully non-blocking with low-level control of application performance and throughput.","title":"Vertx"},{"location":"arch-decisions/DESIGN-NOTES/#java-versions","text":"The platform-core library is backward compatible to Java 1.8 so that it can be used for IT modernization of legacy Java projects. However, the codebase has been tested with Java 1.8 to 19 for projects, and you can apply the platform-core library in your projects without JVM constraints.","title":"Java versions"},{"location":"arch-decisions/DESIGN-NOTES/#spring-boot","text":"The platform-core includes a non-blocking HTTP and websocket server for standalone operation without Spring Boot or similar application server. You may also use the platform-core with Spring Boot or other frameworks.","title":"Spring Boot"},{"location":"arch-decisions/DESIGN-NOTES/#customized-spring-boot","text":"The rest-spring-2-example project demonstrates the use of the rest-spring-2 library to build a Spring Boot 2 executable. The rest-spring-2 is a convenient library with customized Spring Boot serializers and exception handlers. A corresponding library and example application for Spring Boot version 3 is rest-spring-3 and rest-spring-3-example .","title":"Customized Spring Boot"},{"location":"arch-decisions/DESIGN-NOTES/#regular-spring-boot","text":"You can also use the platform-core library with a regular Spring Boot application if you prefer.","title":"Regular Spring Boot"},{"location":"guides/APPENDIX-I/","text":"Application Configuration The following parameters are used by the system. You can define them in either the application.properties or application.yml file. When you use both application.properties and application.yml, the parameters in application.properties will take precedence. Key Value (example) Required application.name Application name Yes spring.application.name Alias for application name Yes*1 info.app.version major.minor.build (e.g. 1.0.0) Yes info.app.description Something about your application Yes web.component.scan your own package path or parent path Yes server.port e.g. 8083 Yes*1 rest.server.port e.g. 8085 Optional websocket.server.port Alias for rest.server.port Optional rest.automation true if you want to enable automation Optional yaml.rest.automation Config location. e.g. classpath:/rest.yaml Optional yaml.event.over.http Config location classpath:/event-over-http.yaml Optional yaml.multicast Config location classpath:/multicast.yaml Optional yaml.journal Config location classpath:/journal.yaml Optional yaml.route.substitution Config location Optional yaml.topic.substitution Config location Optional yaml.cron Config location Optional yaml.flow.automation Config location. e.g. classpath:/flows.yaml EventScript static.html.folder classpath:/public/ Yes spring.web.resources.static-locations (alias for static.html.folder) Yes*1 mime.types Map of file extensions to MIME types (application.yml only) Optional spring.mvc.static-path-pattern /** Yes*1 jax.rs.application.path /api Optional* show.env.variables comma separated list of variable names Optional show.application.properties comma separated list of property names Optional cloud.connector kafka, none, etc. Optional cloud.services e.g. some.interesting.service Optional snake.case.serialization true (recommended) Optional safe.data.models packages pointing to your PoJo classes Optional protect.info.endpoints true to disable actuators. Default: true Optional trace.http.header comma separated list. Default \"X-Trace-Id\" Optional index.redirection comma separated list of URI paths Optional* index.page default is index.html Optional* hsts.feature default is true Optional* application.feature.route.substitution default is false Optional application.feature.topic.substitution default is false Optional kafka.replication.factor 3 Kafka cloud.client.properties e.g. classpath:/kafka.properties Connector user.cloud.client.properties e.g. classpath:/second-kafka.properties Connector default.app.group.id groupId for the app instance. Default: appGroup Connector default.monitor.group.id groupId for the presence-monitor. Default: monitorGroup Connector monitor.topic topic for the presence-monitor. Default: service.monitor Connector app.topic.prefix Default: multiplex (DO NOT change) Connector app.partitions.per.topic Max Kafka partitions per topic. Default: 32 Connector max.virtual.topics Max virtual topics = partitions * topics. Default: 288 Connector max.closed.user.groups Number of closed user groups. Default: 10, range: 3 - 30 Connector closed.user.group Closed user group. Default: 1 Connector transient.data.store Default is \"/tmp/reactive\" Optional running.in.cloud Default is false (set to true if containerized) Optional deferred.commit.log Default is false (for unit tests only) Optional kernel.thread.pool Default 100. Not more than 200. Optional * - when using the \"rest-spring\" library Base configuration files By default, the system assumes the following application configuration files: application.properties application.yml You can change this behavior by adding the app-config-reader.yml in your project's resources folder. resources: - application.properties - application.yml You can tell the system to load application configuration from different set of files. You can use either PROPERTIES or YAML files. YAML files can use \"yml\" or \"yaml\" extension. For example, you may use only \"application.yml\" file without scanning application.properties. Special handling for PROPERTIES file Since application.properties and application.yml can be used together, the system must enforce keyspace uniqueness because YAML keyspaces are hierarchical. For example, if you have x.y and x.y.z, x.y is the parent of x.y.z. Therefore, you cannot set a value for the parent key since the parent is a key-value container. This hierarchical rule is enforced for PROPERTIES files. If you have x.y=3 and x.y.z=2 in the same PROPERTIES file, x.y will become a parent of x.y.z and its intended value of 3 will be lost. Optional Service The OptionalService annotation may be used with the following class annotations: BeforeApplication MainApplication PreLoad WebSocketService When the OptionalService annotation is available, the system will evaluate the annotation value as a conditional statement where it supports one or more simple condition using a key-value in the application configuration. For examples: OptionalService(\"rest.automation\") - the class will be loaded when rest.automation=true OptionalService(\"!rest.automation\") - the class will be loaded when rest.automation is false or non-exist OptionalService(\"interesting.key=100\") - the system will load the class when \"interesting.key\" is set to 100 in application configuration. To specify more than one condition, use a comma separated list as the value like this: OptionalService(\"web.socket.enabled, rest.automation\") - this tells the system to load the class when either web.socket.enabled or rest.automation is true. Static HTML contents You can place static HTML files (e.g. the HTML bundle for a UI program) in the \"resources/public\" folder or in the local file system using the \"static.html.folder\" parameter. The system supports a bare minimal list of file extensions to MIME types. If your use case requires additional MIME type mapping, you may define them in the application.yml configuration file under the mime.types section like this: mime.types: pdf: 'application/pdf' doc: 'application/msword' Note that application.properties file cannot be used for the \"mime.types\" section because it only supports text key-values. HTTP and websocket port assignment If rest.automation=true and rest.server.port or server.port are configured, the system will start a lightweight non-blocking HTTP server. If rest.server.port is not available, it will fall back to server.port . If rest.automation=false and you have a websocket server endpoint annotated as WebsocketService , the system will start a non-blocking Websocket server with a minimalist HTTP server that provides actuator services. If websocket.server.port is not available, it will fall back to rest.server.port or server.port . If you add Spring Boot dependency, Spring Boot will use server.port to start Tomcat or similar HTTP server. The built-in lightweight non-blocking HTTP server and Spring Boot can co-exist when you configure rest.server.port and server.port to use different ports. Note that the websocket.server.port parameter is an alias of rest.server.port . Transient data store The system handles back-pressure automatically by overflowing events from memory to a transient data store. As a cloud native best practice, the folder must be under \"/tmp\". The default is \"/tmp/reactive\". The \"running.in.cloud\" parameter must be set to false when your apps are running in IDE or in your laptop. When running in kubernetes, it can be set to true. The safe.data.models parameter PoJo may contain Java code. As a result, it is possible to inject malicious code that does harm when deserializing a PoJo. This security risk applies to any JSON serialization engine. For added security and peace of mind, you may want to protect your PoJo package paths. When the safe.data.models parameter is configured, the underlying serializers for JAX-RS, Spring RestController and Servlets will respect this setting and enforce PoJo filtering. If there is a genuine need to programmatically perform serialization, you may use the pre-configured serializer so that the serialization behavior is consistent. You can get an instance of the serializer with SimpleMapper.getInstance().getMapper() . The serializer may perform snake case or camel serialization depending on the parameter snake.case.serialization . If you want to ensure snake case or camel, you can select the serializer like this: SimpleObjectMapper snakeCaseMapper = SimpleMapper.getInstance().getSnakeCaseMapper(); SimpleObjectMapper camelCaseMapper = SimpleMapper.getInstance().getCamelCaseMapper(); The trace.http.header parameter The trace.http.header parameter sets the HTTP header for trace ID. When configured with more than one label, the system will retrieve trace ID from the corresponding HTTP header and propagate it through the transaction that may be served by multiple services. If trace ID is presented in an HTTP request, the system will use the same label to set HTTP response traceId header. X-Trace-Id: a9a4e1ec-1663-4c52-b4c3-7b34b3e33697 or X-Correlation-Id: a9a4e1ec-1663-4c52-b4c3-7b34b3e33697 Kafka specific configuration If you use the kafka-connector (cloud connector) and kafka-presence (presence monitor), you may want to externalize kafka.properties like this: cloud.client.properties=file:/tmp/config/kafka.properties Note that \"classpath\" refers to embedded config file in the \"resources\" folder in your source code and \"file\" refers to an external config file. You want also use the embedded config file as a backup like this: cloud.client.properties=file:/tmp/config/kafka.properties,classpath:/kafka.properties Distributed trace To enable distributed trace logging, please set this in log4j2.xml: Built-in XML serializer The platform-core includes built-in serializers for JSON and XML in the AsyncHttpClient, JAX-RS and Spring RestController. The XML serializer is designed for simple use cases. If you need to handle more complex XML data structure, you can disable the XML serializer by adding the following HTTP request header. X-Raw-Xml=true Chapter-10 Home Appendix-II Migration Guide Table of Contents Reserved Route Names","title":"Appendix-I"},{"location":"guides/APPENDIX-I/#application-configuration","text":"The following parameters are used by the system. You can define them in either the application.properties or application.yml file. When you use both application.properties and application.yml, the parameters in application.properties will take precedence. Key Value (example) Required application.name Application name Yes spring.application.name Alias for application name Yes*1 info.app.version major.minor.build (e.g. 1.0.0) Yes info.app.description Something about your application Yes web.component.scan your own package path or parent path Yes server.port e.g. 8083 Yes*1 rest.server.port e.g. 8085 Optional websocket.server.port Alias for rest.server.port Optional rest.automation true if you want to enable automation Optional yaml.rest.automation Config location. e.g. classpath:/rest.yaml Optional yaml.event.over.http Config location classpath:/event-over-http.yaml Optional yaml.multicast Config location classpath:/multicast.yaml Optional yaml.journal Config location classpath:/journal.yaml Optional yaml.route.substitution Config location Optional yaml.topic.substitution Config location Optional yaml.cron Config location Optional yaml.flow.automation Config location. e.g. classpath:/flows.yaml EventScript static.html.folder classpath:/public/ Yes spring.web.resources.static-locations (alias for static.html.folder) Yes*1 mime.types Map of file extensions to MIME types (application.yml only) Optional spring.mvc.static-path-pattern /** Yes*1 jax.rs.application.path /api Optional* show.env.variables comma separated list of variable names Optional show.application.properties comma separated list of property names Optional cloud.connector kafka, none, etc. Optional cloud.services e.g. some.interesting.service Optional snake.case.serialization true (recommended) Optional safe.data.models packages pointing to your PoJo classes Optional protect.info.endpoints true to disable actuators. Default: true Optional trace.http.header comma separated list. Default \"X-Trace-Id\" Optional index.redirection comma separated list of URI paths Optional* index.page default is index.html Optional* hsts.feature default is true Optional* application.feature.route.substitution default is false Optional application.feature.topic.substitution default is false Optional kafka.replication.factor 3 Kafka cloud.client.properties e.g. classpath:/kafka.properties Connector user.cloud.client.properties e.g. classpath:/second-kafka.properties Connector default.app.group.id groupId for the app instance. Default: appGroup Connector default.monitor.group.id groupId for the presence-monitor. Default: monitorGroup Connector monitor.topic topic for the presence-monitor. Default: service.monitor Connector app.topic.prefix Default: multiplex (DO NOT change) Connector app.partitions.per.topic Max Kafka partitions per topic. Default: 32 Connector max.virtual.topics Max virtual topics = partitions * topics. Default: 288 Connector max.closed.user.groups Number of closed user groups. Default: 10, range: 3 - 30 Connector closed.user.group Closed user group. Default: 1 Connector transient.data.store Default is \"/tmp/reactive\" Optional running.in.cloud Default is false (set to true if containerized) Optional deferred.commit.log Default is false (for unit tests only) Optional kernel.thread.pool Default 100. Not more than 200. Optional * - when using the \"rest-spring\" library","title":"Application Configuration"},{"location":"guides/APPENDIX-I/#base-configuration-files","text":"By default, the system assumes the following application configuration files: application.properties application.yml You can change this behavior by adding the app-config-reader.yml in your project's resources folder. resources: - application.properties - application.yml You can tell the system to load application configuration from different set of files. You can use either PROPERTIES or YAML files. YAML files can use \"yml\" or \"yaml\" extension. For example, you may use only \"application.yml\" file without scanning application.properties.","title":"Base configuration files"},{"location":"guides/APPENDIX-I/#special-handling-for-properties-file","text":"Since application.properties and application.yml can be used together, the system must enforce keyspace uniqueness because YAML keyspaces are hierarchical. For example, if you have x.y and x.y.z, x.y is the parent of x.y.z. Therefore, you cannot set a value for the parent key since the parent is a key-value container. This hierarchical rule is enforced for PROPERTIES files. If you have x.y=3 and x.y.z=2 in the same PROPERTIES file, x.y will become a parent of x.y.z and its intended value of 3 will be lost.","title":"Special handling for PROPERTIES file"},{"location":"guides/APPENDIX-I/#optional-service","text":"The OptionalService annotation may be used with the following class annotations: BeforeApplication MainApplication PreLoad WebSocketService When the OptionalService annotation is available, the system will evaluate the annotation value as a conditional statement where it supports one or more simple condition using a key-value in the application configuration. For examples: OptionalService(\"rest.automation\") - the class will be loaded when rest.automation=true OptionalService(\"!rest.automation\") - the class will be loaded when rest.automation is false or non-exist OptionalService(\"interesting.key=100\") - the system will load the class when \"interesting.key\" is set to 100 in application configuration. To specify more than one condition, use a comma separated list as the value like this: OptionalService(\"web.socket.enabled, rest.automation\") - this tells the system to load the class when either web.socket.enabled or rest.automation is true.","title":"Optional Service"},{"location":"guides/APPENDIX-I/#static-html-contents","text":"You can place static HTML files (e.g. the HTML bundle for a UI program) in the \"resources/public\" folder or in the local file system using the \"static.html.folder\" parameter. The system supports a bare minimal list of file extensions to MIME types. If your use case requires additional MIME type mapping, you may define them in the application.yml configuration file under the mime.types section like this: mime.types: pdf: 'application/pdf' doc: 'application/msword' Note that application.properties file cannot be used for the \"mime.types\" section because it only supports text key-values.","title":"Static HTML contents"},{"location":"guides/APPENDIX-I/#http-and-websocket-port-assignment","text":"If rest.automation=true and rest.server.port or server.port are configured, the system will start a lightweight non-blocking HTTP server. If rest.server.port is not available, it will fall back to server.port . If rest.automation=false and you have a websocket server endpoint annotated as WebsocketService , the system will start a non-blocking Websocket server with a minimalist HTTP server that provides actuator services. If websocket.server.port is not available, it will fall back to rest.server.port or server.port . If you add Spring Boot dependency, Spring Boot will use server.port to start Tomcat or similar HTTP server. The built-in lightweight non-blocking HTTP server and Spring Boot can co-exist when you configure rest.server.port and server.port to use different ports. Note that the websocket.server.port parameter is an alias of rest.server.port .","title":"HTTP and websocket port assignment"},{"location":"guides/APPENDIX-I/#transient-data-store","text":"The system handles back-pressure automatically by overflowing events from memory to a transient data store. As a cloud native best practice, the folder must be under \"/tmp\". The default is \"/tmp/reactive\". The \"running.in.cloud\" parameter must be set to false when your apps are running in IDE or in your laptop. When running in kubernetes, it can be set to true.","title":"Transient data store"},{"location":"guides/APPENDIX-I/#the-safedatamodels-parameter","text":"PoJo may contain Java code. As a result, it is possible to inject malicious code that does harm when deserializing a PoJo. This security risk applies to any JSON serialization engine. For added security and peace of mind, you may want to protect your PoJo package paths. When the safe.data.models parameter is configured, the underlying serializers for JAX-RS, Spring RestController and Servlets will respect this setting and enforce PoJo filtering. If there is a genuine need to programmatically perform serialization, you may use the pre-configured serializer so that the serialization behavior is consistent. You can get an instance of the serializer with SimpleMapper.getInstance().getMapper() . The serializer may perform snake case or camel serialization depending on the parameter snake.case.serialization . If you want to ensure snake case or camel, you can select the serializer like this: SimpleObjectMapper snakeCaseMapper = SimpleMapper.getInstance().getSnakeCaseMapper(); SimpleObjectMapper camelCaseMapper = SimpleMapper.getInstance().getCamelCaseMapper();","title":"The safe.data.models parameter"},{"location":"guides/APPENDIX-I/#the-tracehttpheader-parameter","text":"The trace.http.header parameter sets the HTTP header for trace ID. When configured with more than one label, the system will retrieve trace ID from the corresponding HTTP header and propagate it through the transaction that may be served by multiple services. If trace ID is presented in an HTTP request, the system will use the same label to set HTTP response traceId header. X-Trace-Id: a9a4e1ec-1663-4c52-b4c3-7b34b3e33697 or X-Correlation-Id: a9a4e1ec-1663-4c52-b4c3-7b34b3e33697","title":"The trace.http.header parameter"},{"location":"guides/APPENDIX-I/#kafka-specific-configuration","text":"If you use the kafka-connector (cloud connector) and kafka-presence (presence monitor), you may want to externalize kafka.properties like this: cloud.client.properties=file:/tmp/config/kafka.properties Note that \"classpath\" refers to embedded config file in the \"resources\" folder in your source code and \"file\" refers to an external config file. You want also use the embedded config file as a backup like this: cloud.client.properties=file:/tmp/config/kafka.properties,classpath:/kafka.properties","title":"Kafka specific configuration"},{"location":"guides/APPENDIX-I/#distributed-trace","text":"To enable distributed trace logging, please set this in log4j2.xml: ","title":"Distributed trace"},{"location":"guides/APPENDIX-I/#built-in-xml-serializer","text":"The platform-core includes built-in serializers for JSON and XML in the AsyncHttpClient, JAX-RS and Spring RestController. The XML serializer is designed for simple use cases. If you need to handle more complex XML data structure, you can disable the XML serializer by adding the following HTTP request header. X-Raw-Xml=true Chapter-10 Home Appendix-II Migration Guide Table of Contents Reserved Route Names","title":"Built-in XML serializer"},{"location":"guides/APPENDIX-II/","text":"Reserved Route Names The Mercury foundation code is written using the same core API and each function has a route name. The following route names are reserved. Please DO NOT use them in your application functions to avoid breaking the system unintentionally. Route Purpose Modules actuator.services Actuator endpoint services platform-core elastic.queue.cleanup Elastic event buffer clean up task platform-core distributed.tracing Distributed tracing logger platform-core system.ws.server.cleanup Websocket server cleanup service platform-core http.auth.handler REST automation authentication router platform-core event.api.service Event API service platform-core stream.to.bytes Event API helper function platform-core system.service.registry Distributed routing registry Connector system.service.query Distributed routing query Connector cloud.connector.health Cloud connector health service Connector cloud.manager Cloud manager service Connector presence.service Presence signal service Connector presence.housekeeper Presence keep-alive service Connector cloud.connector Cloud event emitter Connector init.multiplex.* reserved for event stream startup Connector completion.multiplex.* reserved for event stream clean up Connector async.http.request HTTP request event handler REST automation async.http.response HTTP response event handler REST automation cron.scheduler Cron job scheduler Simple Scheduler init.service.monitor.* reserved for event stream startup Service monitor completion.service.monitor.* reserved for event stream clean up Service monitor Optional user defined functions The following optional route names will be detected by the system for additional user defined features. Route Purpose additional.info User application function to return information about your application status distributed.trace.forwarder Custom function to forward performance metrics to a telemetry system transaction.journal.recorder Custom function to record transaction request-response payloads into an audit DB The additional.info function, if implemented, will be invoked from the \"/info\" endpoint and its response will be merged into the \"/info\" response. For distributed.trace.forwarder and transaction.journal.recorder , please refer to Chapter-5 for details. Reserved event header names The following event headers are injected by the system as READ only metadata. They are available from the input \"headers\". However, they are not part of the EventEnvelope. Header Purpose my_route route name of your function my_trace_id trace ID, if any, for the incoming event my_trace_path trace path, if any, for the incoming event You can create a trackable PostOffice using the \"headers\" and the \"instance\" parameters in the input arguments of your function. The FastRPC instance requires only the \"headers\" parameters. // Java PostOffice po = new PostOffice(headers, instance); // Kotlin val fastRPC = FastRPC(headers); Appendix-I Home Appendix-III Application Configuration Table of Contents Actuator and HTTP client","title":"Appendix-II"},{"location":"guides/APPENDIX-II/#reserved-route-names","text":"The Mercury foundation code is written using the same core API and each function has a route name. The following route names are reserved. Please DO NOT use them in your application functions to avoid breaking the system unintentionally. Route Purpose Modules actuator.services Actuator endpoint services platform-core elastic.queue.cleanup Elastic event buffer clean up task platform-core distributed.tracing Distributed tracing logger platform-core system.ws.server.cleanup Websocket server cleanup service platform-core http.auth.handler REST automation authentication router platform-core event.api.service Event API service platform-core stream.to.bytes Event API helper function platform-core system.service.registry Distributed routing registry Connector system.service.query Distributed routing query Connector cloud.connector.health Cloud connector health service Connector cloud.manager Cloud manager service Connector presence.service Presence signal service Connector presence.housekeeper Presence keep-alive service Connector cloud.connector Cloud event emitter Connector init.multiplex.* reserved for event stream startup Connector completion.multiplex.* reserved for event stream clean up Connector async.http.request HTTP request event handler REST automation async.http.response HTTP response event handler REST automation cron.scheduler Cron job scheduler Simple Scheduler init.service.monitor.* reserved for event stream startup Service monitor completion.service.monitor.* reserved for event stream clean up Service monitor","title":"Reserved Route Names"},{"location":"guides/APPENDIX-II/#optional-user-defined-functions","text":"The following optional route names will be detected by the system for additional user defined features. Route Purpose additional.info User application function to return information about your application status distributed.trace.forwarder Custom function to forward performance metrics to a telemetry system transaction.journal.recorder Custom function to record transaction request-response payloads into an audit DB The additional.info function, if implemented, will be invoked from the \"/info\" endpoint and its response will be merged into the \"/info\" response. For distributed.trace.forwarder and transaction.journal.recorder , please refer to Chapter-5 for details.","title":"Optional user defined functions"},{"location":"guides/APPENDIX-II/#reserved-event-header-names","text":"The following event headers are injected by the system as READ only metadata. They are available from the input \"headers\". However, they are not part of the EventEnvelope. Header Purpose my_route route name of your function my_trace_id trace ID, if any, for the incoming event my_trace_path trace path, if any, for the incoming event You can create a trackable PostOffice using the \"headers\" and the \"instance\" parameters in the input arguments of your function. The FastRPC instance requires only the \"headers\" parameters. // Java PostOffice po = new PostOffice(headers, instance); // Kotlin val fastRPC = FastRPC(headers); Appendix-I Home Appendix-III Application Configuration Table of Contents Actuator and HTTP client","title":"Reserved event header names"},{"location":"guides/APPENDIX-III/","text":"Actuators and HTTP client Actuator endpoints The following admin endpoints are available. GET /info GET /info/routes GET /info/lib GET /env GET /health GET /livenessprobe POST /shutdown Endpoint Purpose /info Describe the application /info/routes Show public routing table /info/lib List libraries packed with this executable /env List all private and public function route names and selected environment variables /health Application health check endpoint /livenessprobe Check if application is running normally /shutdown Operator may use this endpoint to do a POST command to stop the application For the shutdown endpoint, you must provide an X-App-Instance HTTP header where the value is the \"origin ID\" of the application. You can get the value from the \"/info\" endpoint. Custom health services You can extend the \"/health\" endpoint by implementing and registering lambda functions to be added to the \"health check\" dependencies. mandatory.health.dependencies=cloud.connector.health, demo.health optional.health.dependencies=other.service.health Your custom health service must respond to the following requests: Info request (type=info) - it should return a map that includes service name and href (protocol, hostname and port) Health check (type=health) - it should return a text string of the health check. e.g. read/write test result. It can throw AppException with status code and error message if health check fails. A sample health service is available in the DemoHealth class of the lambda-example project as follows: @PreLoad(route=\"demo.health\", instances=5) public class DemoHealth implements LambdaFunction { private static final String TYPE = \"type\"; private static final String INFO = \"info\"; private static final String HEALTH = \"health\"; @Override public Object handleEvent(Map headers, Object input, int instance) { /* * The interface contract for a health check service includes both INFO and HEALTH responses */ if (INFO.equals(headers.get(TYPE))) { Map result = new HashMap<>(); result.put(\"service\", \"demo.service\"); result.put(\"href\", \"http://127.0.0.1\"); return result; } if (HEALTH.equals(headers.get(TYPE))) { /* * This is a place-holder for checking a downstream service. * * You may implement your own logic to test if a downstream service is running fine. * If running, just return a health status message. * Otherwise, * throw new AppException(status, message) */ return \"demo.service is running fine\"; } throw new IllegalArgumentException(\"type must be info or health\"); } } AsyncHttpClient service The \"async.http.request\" function can be used as a non-blocking HTTP client. To make an HTTP request to an external REST endpoint, you can create an HTTP request object using the AsyncHttpRequest class and make an async RPC call to the \"async.http.request\" function like this: PostOffice po = new PostOffice(headers, instance); AsyncHttpRequest req = new AsyncHttpRequest(); req.setMethod(\"GET\"); req.setHeader(\"accept\", \"application/json\"); req.setUrl(\"/api/hello/world?hello world=abc\"); req.setQueryParameter(\"x1\", \"y\"); List list = new ArrayList<>(); list.add(\"a\"); list.add(\"b\"); req.setQueryParameter(\"x2\", list); req.setTargetHost(\"http://127.0.0.1:8083\"); EventEnvelope request = new EventEnvelope().setTo(\"async.http.request\").setBody(req); Future res = po.asyncRequest(request, 5000); res.onSuccess(response -> { // do something with the result }); In a suspend function using KotlinLambdaFunction, the same logic may look like this: val fastRPC = FastRPC(headers) val req = AsyncHttpRequest() req.setMethod(\"GET\") req.setHeader(\"accept\", \"application/json\") req.setUrl(\"/api/hello/world?hello world=abc\") req.setQueryParameter(\"x1\", \"y\") val list: MutableList = ArrayList() list.add(\"a\") list.add(\"b\") req.setQueryParameter(\"x2\", list) req.setTargetHost(\"http://127.0.0.1:8083\") val request = EventEnvelope().setTo(\"async.http.request\").setBody(req) val response = fastRPC.awaitRequest(request, 5000) // do something with the result Send HTTP request body for HTTP PUT, POST and PATCH methods For most cases, you can just set a HashMap into the request body and specify content-type as JSON or XML. The system will perform serialization properly. Example code may look like this: AsyncHttpRequest req = new AsyncHttpRequest(); req.setMethod(\"POST\"); req.setHeader(\"accept\", \"application/json\"); req.setHeader(\"content-type\", \"application/json\"); req.setUrl(\"/api/book\"); req.setTargetHost(\"https://service_provider_host\"); req.setBody(mapOfKeyValues); // where keyValues is a HashMap Send HTTP request body as a stream For larger payload, you may use the streaming method. See sample code below: int len; byte[] buffer = new byte[4096]; FileInputStream in = new FileInputStream(myFile); ObjectStreamIO stream = new ObjectStreamIO(timeoutInSeconds); ObjectStreamWriter out = stream.getOutputStream(); while ((len = in.read(buffer, 0, buffer.length)) != -1) { out.write(buffer, 0, len); } // closing the output stream would send a EOF signal to the stream out.close(); // tell the HTTP client to read the input stream req.setStreamRoute(stream.getInputStreamId()); Read HTTP response body stream If content length is not given, the response body will be received as a stream. Your application should check if the HTTP response header \"stream\" exists. Its value is an input \"stream ID\". For simplicity and readability, we recommend using \"suspend function\" to read the input byte-array stream. It may look like this: val po = PostOffice(headers, instance) val fastRPC = FastRPC(headers) val req = EventEnvelope().setTo(streamId).setHeader(\"type\", \"read\") while (true) { val event = fastRPC.awaitRequest(req, 5000) if (event.status == 408) { // handle input stream timeout break } if (\"eof\" == event.headers[\"type\"]) { po.send(streamId, Kv(\"type\", \"close\")) break } if (\"data\" == event.headers[\"type\"]) { val block = event.body if (block is ByteArray) { // handle the data block from the input stream } } } Content length for HTTP request IMPORTANT: Do not set the \"content-length\" HTTP header because the system will automatically compute the correct content-length for small payload. For large payload, it will use the chunking method. Appendix-II Home Reserved Route Names Table of Contents","title":"Appendix-III"},{"location":"guides/APPENDIX-III/#actuators-and-http-client","text":"","title":"Actuators and HTTP client"},{"location":"guides/APPENDIX-III/#actuator-endpoints","text":"The following admin endpoints are available. GET /info GET /info/routes GET /info/lib GET /env GET /health GET /livenessprobe POST /shutdown Endpoint Purpose /info Describe the application /info/routes Show public routing table /info/lib List libraries packed with this executable /env List all private and public function route names and selected environment variables /health Application health check endpoint /livenessprobe Check if application is running normally /shutdown Operator may use this endpoint to do a POST command to stop the application For the shutdown endpoint, you must provide an X-App-Instance HTTP header where the value is the \"origin ID\" of the application. You can get the value from the \"/info\" endpoint.","title":"Actuator endpoints"},{"location":"guides/APPENDIX-III/#custom-health-services","text":"You can extend the \"/health\" endpoint by implementing and registering lambda functions to be added to the \"health check\" dependencies. mandatory.health.dependencies=cloud.connector.health, demo.health optional.health.dependencies=other.service.health Your custom health service must respond to the following requests: Info request (type=info) - it should return a map that includes service name and href (protocol, hostname and port) Health check (type=health) - it should return a text string of the health check. e.g. read/write test result. It can throw AppException with status code and error message if health check fails. A sample health service is available in the DemoHealth class of the lambda-example project as follows: @PreLoad(route=\"demo.health\", instances=5) public class DemoHealth implements LambdaFunction { private static final String TYPE = \"type\"; private static final String INFO = \"info\"; private static final String HEALTH = \"health\"; @Override public Object handleEvent(Map headers, Object input, int instance) { /* * The interface contract for a health check service includes both INFO and HEALTH responses */ if (INFO.equals(headers.get(TYPE))) { Map result = new HashMap<>(); result.put(\"service\", \"demo.service\"); result.put(\"href\", \"http://127.0.0.1\"); return result; } if (HEALTH.equals(headers.get(TYPE))) { /* * This is a place-holder for checking a downstream service. * * You may implement your own logic to test if a downstream service is running fine. * If running, just return a health status message. * Otherwise, * throw new AppException(status, message) */ return \"demo.service is running fine\"; } throw new IllegalArgumentException(\"type must be info or health\"); } }","title":"Custom health services"},{"location":"guides/APPENDIX-III/#asynchttpclient-service","text":"The \"async.http.request\" function can be used as a non-blocking HTTP client. To make an HTTP request to an external REST endpoint, you can create an HTTP request object using the AsyncHttpRequest class and make an async RPC call to the \"async.http.request\" function like this: PostOffice po = new PostOffice(headers, instance); AsyncHttpRequest req = new AsyncHttpRequest(); req.setMethod(\"GET\"); req.setHeader(\"accept\", \"application/json\"); req.setUrl(\"/api/hello/world?hello world=abc\"); req.setQueryParameter(\"x1\", \"y\"); List list = new ArrayList<>(); list.add(\"a\"); list.add(\"b\"); req.setQueryParameter(\"x2\", list); req.setTargetHost(\"http://127.0.0.1:8083\"); EventEnvelope request = new EventEnvelope().setTo(\"async.http.request\").setBody(req); Future res = po.asyncRequest(request, 5000); res.onSuccess(response -> { // do something with the result }); In a suspend function using KotlinLambdaFunction, the same logic may look like this: val fastRPC = FastRPC(headers) val req = AsyncHttpRequest() req.setMethod(\"GET\") req.setHeader(\"accept\", \"application/json\") req.setUrl(\"/api/hello/world?hello world=abc\") req.setQueryParameter(\"x1\", \"y\") val list: MutableList = ArrayList() list.add(\"a\") list.add(\"b\") req.setQueryParameter(\"x2\", list) req.setTargetHost(\"http://127.0.0.1:8083\") val request = EventEnvelope().setTo(\"async.http.request\").setBody(req) val response = fastRPC.awaitRequest(request, 5000) // do something with the result","title":"AsyncHttpClient service"},{"location":"guides/APPENDIX-III/#send-http-request-body-for-http-put-post-and-patch-methods","text":"For most cases, you can just set a HashMap into the request body and specify content-type as JSON or XML. The system will perform serialization properly. Example code may look like this: AsyncHttpRequest req = new AsyncHttpRequest(); req.setMethod(\"POST\"); req.setHeader(\"accept\", \"application/json\"); req.setHeader(\"content-type\", \"application/json\"); req.setUrl(\"/api/book\"); req.setTargetHost(\"https://service_provider_host\"); req.setBody(mapOfKeyValues); // where keyValues is a HashMap","title":"Send HTTP request body for HTTP PUT, POST and PATCH methods"},{"location":"guides/APPENDIX-III/#send-http-request-body-as-a-stream","text":"For larger payload, you may use the streaming method. See sample code below: int len; byte[] buffer = new byte[4096]; FileInputStream in = new FileInputStream(myFile); ObjectStreamIO stream = new ObjectStreamIO(timeoutInSeconds); ObjectStreamWriter out = stream.getOutputStream(); while ((len = in.read(buffer, 0, buffer.length)) != -1) { out.write(buffer, 0, len); } // closing the output stream would send a EOF signal to the stream out.close(); // tell the HTTP client to read the input stream req.setStreamRoute(stream.getInputStreamId());","title":"Send HTTP request body as a stream"},{"location":"guides/APPENDIX-III/#read-http-response-body-stream","text":"If content length is not given, the response body will be received as a stream. Your application should check if the HTTP response header \"stream\" exists. Its value is an input \"stream ID\". For simplicity and readability, we recommend using \"suspend function\" to read the input byte-array stream. It may look like this: val po = PostOffice(headers, instance) val fastRPC = FastRPC(headers) val req = EventEnvelope().setTo(streamId).setHeader(\"type\", \"read\") while (true) { val event = fastRPC.awaitRequest(req, 5000) if (event.status == 408) { // handle input stream timeout break } if (\"eof\" == event.headers[\"type\"]) { po.send(streamId, Kv(\"type\", \"close\")) break } if (\"data\" == event.headers[\"type\"]) { val block = event.body if (block is ByteArray) { // handle the data block from the input stream } } }","title":"Read HTTP response body stream"},{"location":"guides/APPENDIX-III/#content-length-for-http-request","text":"IMPORTANT: Do not set the \"content-length\" HTTP header because the system will automatically compute the correct content-length for small payload. For large payload, it will use the chunking method. Appendix-II Home Reserved Route Names Table of Contents","title":"Content length for HTTP request"},{"location":"guides/CHAPTER-1/","text":"Introduction Mercury version 3 is a toolkit for event-driven programming that is the foundation for composable application. At the platform level, composable architecture refers to loosely coupled platform services, utilities, and business applications. With modular design, you can assemble platform components and applications to create new use cases or to adjust for ever-changing business environment and requirements. Domain driven design (DDD), Command Query Responsibility Segregation (CQRS) and Microservices patterns are the popular tools that architects use to build composable architecture. You may deploy application in container, serverless or other means. At the application level, a composable application means that an application is assembled from modular software components or functions that are self-contained and pluggable. You can mix-n-match functions to form new applications. You can retire outdated functions without adverse side effect to a production system. Multiple versions of a function can exist, and you can decide how to route user requests to different versions of a function. Applications would be easier to design, develop, maintain, deploy, and scale. While you can write a composable application using event-driven programming, the best way to build a composable application is a declarative approach where event choreography of self-contained functions is performed by an event manager. Declarative approach for building composable applications is shown in: Mercury v4: https://github.com/Accenture/mercury-composable Documentation: https://accenture.github.io/mercury-composable/ Composable application architecture Figure 1 - Composable application architecture As shown in Figure 1, a minimalist composable application consists of three user defined components: Main modules that provides an entry point to your application One or more business logic modules (shown as \"function-1\" to \"function-3\" in the diagram) An event orchestration module to command the business logic modules to work together as an application and a composable event engine that provides: REST automation An in-memory event system (aka \"event loop\") Local pub/sub system Main module Each application has an entry point. You may implement an entry point in a main application like this: @MainApplication public class MainApp implements EntryPoint { public static void main(String[] args) { AutoStart.main(args); } @Override public void start(String[] args) { // your startup logic here log.info(\"Started\"); } } For a command line use case, your main application (\"MainApp\") module would get command line arguments and send the request as an event to a business logic function for processing. For a backend application, the MainApp is usually used to do some \"initialization\" or setup steps for your services. Business logic modules Your user function module may look like this: @PreLoad(route = \"hello.simple\", instances = 10) public class SimpleDemoEndpoint implements TypedLambdaFunction { @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { // business logic here return result; } } Each function in a composable application should be implemented in the first principle of \"input-process-output\". It should be stateless and self-contained. i.e. it has no direct dependencies with any other functions in the composable application. Each function is addressable by a unique \"route name\" and you can use PoJo for input and output. In the above example, the function is called \"hello.simple\". The input is an AsyncHttpRequest object, meaning that this function is a \"Backend for Frontend (BFF)\" module that is invoked by a REST endpoint. When a function finishes processing, its output will be delivered to the next function. Writing code in the first principle of \"input-process-output\" promotes Test Driven Development (TDD) because interface contact is clearly defined. Self-containment means code is more readable too. Event orchestration A transaction can pass through one or more user functions. In this case, you can write a user function to receive request from a user, make requests to some user functions, and consolidate the responses before responding to the user. Note that event orchestration is optional. In the most basic REST application, the REST automation system can send the user request to a function directly. When the function finishes processing, its output will be routed as a HTTP response to the user. The in-memory event system Event routing is done behind the curtain by the composable engine which consists of the REST automation service, an in-memory event system (\"event loop\") and an optional localized pub/sub system. REST automation REST automation creates REST endpoints by configuration rather than code. You can define a REST endpoint like this: - service: \"hello.world\" methods: ['GET'] url: \"/api/hello/world\" timeout: 10s In this example, when a HTTP request is received at the URL path \"/api/hello/world\", the REST automation system will convert the HTTP request into an event for onward delivery to the user defined function \"hello.world\". Your function will receive the HTTP request as input and return a result set that will be sent as a HTTP response to the user. For more sophisticated business logic, you can write a function to receive the HTTP request and do \"event orchestration\". i.e. you can do data transformation and send \"events\" to other user functions to process the request. In-memory event system The composable engine encapsulates the Eclipse vertx event bus library for event routing. It exposes the \"PostOffice\" API for your orchestration function to send async or RPC events. Local pub/sub system The in-memory event system is designed for point-to-point delivery. In some use cases, you may like to have a broadcast channel so that more than one function can receive the same event. For example, sending notification events to multiple functions. The optional local pub/sub system provides this multicast capability. Other user facing channels While REST is the most popular user facing interface, there are other communication means such as event triggers in a serverless environment. You can write a function to listen to these external event triggers and send the events to your user defined functions. This custom \"adapter\" pattern is illustrated as the dotted line path in Figure 1. Build the platform libraries The first step is to build Mercury libraries from source. To simplify the process, you may publish the libraries to your enterprise artifactory. mkdir sandbox cd sandox git clone https://github.com/Accenture/mercury.git cd mercury mvn clean install The above sample script clones the Mercury open sources project and builds the libraries from source. The pre-requisite is maven 3.8.6 and openjdk 1.8 or higher. We have tested mercury with Java version 1.8 to 19. This will build the mercury libraries and the sample applications. The platform-core project is the foundation library for writing composable application. Run the lambda-example application Assuming you follow the suggested project directory above, you can run a sample composable application called \"lambda-example\" like this: cd sandbox/mercury/examples/lambda-example java -jar target/lambda-example-3.0.9.jar You will find the following console output when the app starts Exact API paths [/api/event, /api/hello/download, /api/hello/upload, /api/hello/world] Wildcard API paths [/api/hello/download/{filename}, /api/hello/generic/{id}] Application parameters are defined in the resources/application.properties file (or application.yml if you prefer). When rest.automation=true is defined, the system will parse the \"rest.yaml\" configuration for REST endpoints. Light-weight non-blocking HTTP server When REST automation is turned on, the system will start a lightweight non-blocking HTTP server. By default, it will search for the \"rest.yaml\" file from \"/tmp/config/rest.yaml\" and then from \"classpath:/rest.yaml\". Classpath refers to configuration files under the \"resources\" folder in your source code project. To instruct the system to load from a specific path. You can add the yaml.rest.automation parameter. To select another server port, change the rest.server.port parameter. rest.server.port=8085 rest.automation=true yaml.rest.automation=classpath:/rest.yaml To create a REST endpoint, you can add an entry in the \"rest\" section of the \"rest.yaml\" config file like this: - service: \"hello.download\" methods: [ 'GET' ] url: \"/api/hello/download\" timeout: 20s cors: cors_1 headers: header_1 tracing: true The above example creates the \"/api/hello/download\" endpoint to route requests to the \"hello.download\" function. We will elaborate more about REST automation in Chapter-3 . Function is an event handler A function is executed when an event arrives. You can define a \"route name\" for each function. It is created by a class implementing one of the following interfaces: TypedLambdaFunction allows you to use PoJo or HashMap as input and output LambdaFunction is untyped, but it will transport PoJo from the caller to the input of your function KotlinLambdaFunction is a typed lambda function using Kotlin suspend function Execute the \"hello.world\" function With the application started in a command terminal, please use a browser to point to: http://127.0.0.1:8085/api/hello/world It will echo the HTTP headers from the browser like this: { \"headers\": {}, \"instance\": 1, \"origin\": \"20230324b709495174a649f1b36d401f43167ba9\", \"body\": { \"headers\": { \"sec-fetch-mode\": \"navigate\", \"sec-fetch-site\": \"none\", \"sec-ch-ua-mobile\": \"?0\", \"accept-language\": \"en-US,en;q=0.9\", \"sec-ch-ua-platform\": \"\\\"Windows\\\"\", \"upgrade-insecure-requests\": \"1\", \"sec-fetch-user\": \"?1\", \"accept\": \"text/html,application/xhtml+xml,application/xml,*/*\", \"sec-fetch-dest\": \"document\", \"user-agent\": \"Mozilla/5.0 Chrome/111.0.0.0\" }, \"method\": \"GET\", \"ip\": \"127.0.0.1\", \"https\": false, \"url\": \"/api/hello/world\", \"timeout\": 10 } } Where is the \"hello.world\" function? The function is defined in the MainApp class in the source project with the following segment of code: LambdaFunction echo = (headers, input, instance) -> { log.info(\"echo #{} got a request\", instance); Map result = new HashMap<>(); result.put(\"headers\", headers); result.put(\"body\", input); result.put(\"instance\", instance); result.put(\"origin\", platform.getOrigin()); return result; }; // Register the above inline lambda function platform.register(\"hello.world\", echo, 10); The Hello World function is written as an \"inline lambda function\". It is registered programmatically using the platform.register API. The rest of the functions are written using regular classes implementing the LambdaFunction, TypedLambdaFunction and KotlinLambdaFunction interfaces. TypedLambdaFunction Let's examine the SimpleDemoEndpoint example under the \"services\" folder. It may look like this: @PreLoad(route = \"hello.simple\", instances = 10) public class SimpleDemoEndpoint implements TypedLambdaFunction { @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { // business logic here } } The PreLoad annotation assigns a route name to the Java class and registers it with an in-memory event system. The instances parameter tells the system to create a number of workers to serve concurrent requests. Note that you don't need a lot of workers to handle a larger number of users and requests provided that your function can finish execution very quickly. By default, functions are executed as \"coroutine\" unless you specify the KernelThreadRunner annotation to tell the system to run the function using kernel thread pool. There are three function execution strategies (Kernel thread pool, coroutine and suspend function). We will explain the concept in Chapter-2 In a composable application, a function is designed using the first principle of \"input-process-output\". In the \"hello.simple\" function, the input is an HTTP request expressed as a class of AsyncHttpRequest . You can ignore headers input argument for the moment. We will cover it later. The output is declared as \"Object\" so that the function can return any data structure using a HashMap or PoJo. You may want to review the REST endpoint /api/simple/{task}/* in the rest.yaml config file to see how it is connected to the \"hello.simple\" function. We take a minimalist approach for the rest.yaml syntax. The parser will detect any syntax errors. Please check application log to ensure all REST endpoint entries in rest.yaml file are valid. Write your first function Using the lambda-example as a template, let's create your first function by adding a function in the \"services\" package folder. You will give it the route name \"my.first.function\" in the \"PreLoad\" annotation. Note that route name must use lower case letters and numbers separated by the period character. @PreLoad(route = \"my.first.function\", instances = 10) public class MyFirstFunction implements TypedLambdaFunction { @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { // your business logic here return input; } } To connect this function with a REST endpoint, you can declare a new REST endpoint in the rest.yaml like this: - service: \"my.first.function\" methods: [ 'GET' ] url: \"/api/hello/my/function\" timeout: 20s cors: cors_1 headers: header_1 tracing: true If you do not put any business logic, the above function will echo the incoming HTTP request object back to the browser. Now you can examine the input HTTP request object and perform some data transformation before returning a result. The AsyncHttpRequest class allows you to access data structure such as HTTP method, URL, path parameters, query parameters, cookies, etc. When you click the \"rebuild\" button in IDE and run the \"MainApp\", the new function will be available in the application. Alternatively, you can also do mvn clean package to generate a new executable JAR and run the JAR from command line. To test your new function, visit http://127.0.0.1:8085/api/hello/my/function Event driven design Your function automatically uses an in-memory event bus. The HTTP request from the browser is converted to an event by the system for delivery to your function as the \"input\" argument. The underlying HTTP server is asynchronous and non-blocking. i.e. it does not consume CPU resources while waiting for a response. This composable architecture allows you to design and implement applications so that you have precise control of performance and throughput. Performance tuning is much easier. Deploy your new application You can assemble related functions in a single composable application, and it can be compiled and built into a single \"executable\" for deployment using mvn clean package . The executable JAR is in the target folder. Composable application is by definition cloud native. It is designed to be deployable using Kubernetes or serverless. A sample Dockerfile for your executable JAR may look like this: FROM adoptopenjdk/openjdk11:jre-11.0.11_9-alpine EXPOSE 8083 WORKDIR /app COPY target/your-app-name.jar . ENTRYPOINT [\"java\",\"-jar\",\"your-app-name.jar\"] Home Chapter-2 Table of Contents Function Execution Strategy","title":"Chapter-1"},{"location":"guides/CHAPTER-1/#introduction","text":"Mercury version 3 is a toolkit for event-driven programming that is the foundation for composable application. At the platform level, composable architecture refers to loosely coupled platform services, utilities, and business applications. With modular design, you can assemble platform components and applications to create new use cases or to adjust for ever-changing business environment and requirements. Domain driven design (DDD), Command Query Responsibility Segregation (CQRS) and Microservices patterns are the popular tools that architects use to build composable architecture. You may deploy application in container, serverless or other means. At the application level, a composable application means that an application is assembled from modular software components or functions that are self-contained and pluggable. You can mix-n-match functions to form new applications. You can retire outdated functions without adverse side effect to a production system. Multiple versions of a function can exist, and you can decide how to route user requests to different versions of a function. Applications would be easier to design, develop, maintain, deploy, and scale. While you can write a composable application using event-driven programming, the best way to build a composable application is a declarative approach where event choreography of self-contained functions is performed by an event manager. Declarative approach for building composable applications is shown in: Mercury v4: https://github.com/Accenture/mercury-composable Documentation: https://accenture.github.io/mercury-composable/","title":"Introduction"},{"location":"guides/CHAPTER-1/#composable-application-architecture","text":"Figure 1 - Composable application architecture As shown in Figure 1, a minimalist composable application consists of three user defined components: Main modules that provides an entry point to your application One or more business logic modules (shown as \"function-1\" to \"function-3\" in the diagram) An event orchestration module to command the business logic modules to work together as an application and a composable event engine that provides: REST automation An in-memory event system (aka \"event loop\") Local pub/sub system","title":"Composable application architecture"},{"location":"guides/CHAPTER-1/#main-module","text":"Each application has an entry point. You may implement an entry point in a main application like this: @MainApplication public class MainApp implements EntryPoint { public static void main(String[] args) { AutoStart.main(args); } @Override public void start(String[] args) { // your startup logic here log.info(\"Started\"); } } For a command line use case, your main application (\"MainApp\") module would get command line arguments and send the request as an event to a business logic function for processing. For a backend application, the MainApp is usually used to do some \"initialization\" or setup steps for your services.","title":"Main module"},{"location":"guides/CHAPTER-1/#business-logic-modules","text":"Your user function module may look like this: @PreLoad(route = \"hello.simple\", instances = 10) public class SimpleDemoEndpoint implements TypedLambdaFunction { @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { // business logic here return result; } } Each function in a composable application should be implemented in the first principle of \"input-process-output\". It should be stateless and self-contained. i.e. it has no direct dependencies with any other functions in the composable application. Each function is addressable by a unique \"route name\" and you can use PoJo for input and output. In the above example, the function is called \"hello.simple\". The input is an AsyncHttpRequest object, meaning that this function is a \"Backend for Frontend (BFF)\" module that is invoked by a REST endpoint. When a function finishes processing, its output will be delivered to the next function. Writing code in the first principle of \"input-process-output\" promotes Test Driven Development (TDD) because interface contact is clearly defined. Self-containment means code is more readable too.","title":"Business logic modules"},{"location":"guides/CHAPTER-1/#event-orchestration","text":"A transaction can pass through one or more user functions. In this case, you can write a user function to receive request from a user, make requests to some user functions, and consolidate the responses before responding to the user. Note that event orchestration is optional. In the most basic REST application, the REST automation system can send the user request to a function directly. When the function finishes processing, its output will be routed as a HTTP response to the user.","title":"Event orchestration"},{"location":"guides/CHAPTER-1/#the-in-memory-event-system","text":"Event routing is done behind the curtain by the composable engine which consists of the REST automation service, an in-memory event system (\"event loop\") and an optional localized pub/sub system.","title":"The in-memory event system"},{"location":"guides/CHAPTER-1/#rest-automation","text":"REST automation creates REST endpoints by configuration rather than code. You can define a REST endpoint like this: - service: \"hello.world\" methods: ['GET'] url: \"/api/hello/world\" timeout: 10s In this example, when a HTTP request is received at the URL path \"/api/hello/world\", the REST automation system will convert the HTTP request into an event for onward delivery to the user defined function \"hello.world\". Your function will receive the HTTP request as input and return a result set that will be sent as a HTTP response to the user. For more sophisticated business logic, you can write a function to receive the HTTP request and do \"event orchestration\". i.e. you can do data transformation and send \"events\" to other user functions to process the request.","title":"REST automation"},{"location":"guides/CHAPTER-1/#in-memory-event-system","text":"The composable engine encapsulates the Eclipse vertx event bus library for event routing. It exposes the \"PostOffice\" API for your orchestration function to send async or RPC events.","title":"In-memory event system"},{"location":"guides/CHAPTER-1/#local-pubsub-system","text":"The in-memory event system is designed for point-to-point delivery. In some use cases, you may like to have a broadcast channel so that more than one function can receive the same event. For example, sending notification events to multiple functions. The optional local pub/sub system provides this multicast capability.","title":"Local pub/sub system"},{"location":"guides/CHAPTER-1/#other-user-facing-channels","text":"While REST is the most popular user facing interface, there are other communication means such as event triggers in a serverless environment. You can write a function to listen to these external event triggers and send the events to your user defined functions. This custom \"adapter\" pattern is illustrated as the dotted line path in Figure 1.","title":"Other user facing channels"},{"location":"guides/CHAPTER-1/#build-the-platform-libraries","text":"The first step is to build Mercury libraries from source. To simplify the process, you may publish the libraries to your enterprise artifactory. mkdir sandbox cd sandox git clone https://github.com/Accenture/mercury.git cd mercury mvn clean install The above sample script clones the Mercury open sources project and builds the libraries from source. The pre-requisite is maven 3.8.6 and openjdk 1.8 or higher. We have tested mercury with Java version 1.8 to 19. This will build the mercury libraries and the sample applications. The platform-core project is the foundation library for writing composable application.","title":"Build the platform libraries"},{"location":"guides/CHAPTER-1/#run-the-lambda-example-application","text":"Assuming you follow the suggested project directory above, you can run a sample composable application called \"lambda-example\" like this: cd sandbox/mercury/examples/lambda-example java -jar target/lambda-example-3.0.9.jar You will find the following console output when the app starts Exact API paths [/api/event, /api/hello/download, /api/hello/upload, /api/hello/world] Wildcard API paths [/api/hello/download/{filename}, /api/hello/generic/{id}] Application parameters are defined in the resources/application.properties file (or application.yml if you prefer). When rest.automation=true is defined, the system will parse the \"rest.yaml\" configuration for REST endpoints.","title":"Run the lambda-example application"},{"location":"guides/CHAPTER-1/#light-weight-non-blocking-http-server","text":"When REST automation is turned on, the system will start a lightweight non-blocking HTTP server. By default, it will search for the \"rest.yaml\" file from \"/tmp/config/rest.yaml\" and then from \"classpath:/rest.yaml\". Classpath refers to configuration files under the \"resources\" folder in your source code project. To instruct the system to load from a specific path. You can add the yaml.rest.automation parameter. To select another server port, change the rest.server.port parameter. rest.server.port=8085 rest.automation=true yaml.rest.automation=classpath:/rest.yaml To create a REST endpoint, you can add an entry in the \"rest\" section of the \"rest.yaml\" config file like this: - service: \"hello.download\" methods: [ 'GET' ] url: \"/api/hello/download\" timeout: 20s cors: cors_1 headers: header_1 tracing: true The above example creates the \"/api/hello/download\" endpoint to route requests to the \"hello.download\" function. We will elaborate more about REST automation in Chapter-3 .","title":"Light-weight non-blocking HTTP server"},{"location":"guides/CHAPTER-1/#function-is-an-event-handler","text":"A function is executed when an event arrives. You can define a \"route name\" for each function. It is created by a class implementing one of the following interfaces: TypedLambdaFunction allows you to use PoJo or HashMap as input and output LambdaFunction is untyped, but it will transport PoJo from the caller to the input of your function KotlinLambdaFunction is a typed lambda function using Kotlin suspend function","title":"Function is an event handler"},{"location":"guides/CHAPTER-1/#execute-the-helloworld-function","text":"With the application started in a command terminal, please use a browser to point to: http://127.0.0.1:8085/api/hello/world It will echo the HTTP headers from the browser like this: { \"headers\": {}, \"instance\": 1, \"origin\": \"20230324b709495174a649f1b36d401f43167ba9\", \"body\": { \"headers\": { \"sec-fetch-mode\": \"navigate\", \"sec-fetch-site\": \"none\", \"sec-ch-ua-mobile\": \"?0\", \"accept-language\": \"en-US,en;q=0.9\", \"sec-ch-ua-platform\": \"\\\"Windows\\\"\", \"upgrade-insecure-requests\": \"1\", \"sec-fetch-user\": \"?1\", \"accept\": \"text/html,application/xhtml+xml,application/xml,*/*\", \"sec-fetch-dest\": \"document\", \"user-agent\": \"Mozilla/5.0 Chrome/111.0.0.0\" }, \"method\": \"GET\", \"ip\": \"127.0.0.1\", \"https\": false, \"url\": \"/api/hello/world\", \"timeout\": 10 } }","title":"Execute the \"hello.world\" function"},{"location":"guides/CHAPTER-1/#where-is-the-helloworld-function","text":"The function is defined in the MainApp class in the source project with the following segment of code: LambdaFunction echo = (headers, input, instance) -> { log.info(\"echo #{} got a request\", instance); Map result = new HashMap<>(); result.put(\"headers\", headers); result.put(\"body\", input); result.put(\"instance\", instance); result.put(\"origin\", platform.getOrigin()); return result; }; // Register the above inline lambda function platform.register(\"hello.world\", echo, 10); The Hello World function is written as an \"inline lambda function\". It is registered programmatically using the platform.register API. The rest of the functions are written using regular classes implementing the LambdaFunction, TypedLambdaFunction and KotlinLambdaFunction interfaces.","title":"Where is the \"hello.world\" function?"},{"location":"guides/CHAPTER-1/#typedlambdafunction","text":"Let's examine the SimpleDemoEndpoint example under the \"services\" folder. It may look like this: @PreLoad(route = \"hello.simple\", instances = 10) public class SimpleDemoEndpoint implements TypedLambdaFunction { @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { // business logic here } } The PreLoad annotation assigns a route name to the Java class and registers it with an in-memory event system. The instances parameter tells the system to create a number of workers to serve concurrent requests. Note that you don't need a lot of workers to handle a larger number of users and requests provided that your function can finish execution very quickly. By default, functions are executed as \"coroutine\" unless you specify the KernelThreadRunner annotation to tell the system to run the function using kernel thread pool. There are three function execution strategies (Kernel thread pool, coroutine and suspend function). We will explain the concept in Chapter-2 In a composable application, a function is designed using the first principle of \"input-process-output\". In the \"hello.simple\" function, the input is an HTTP request expressed as a class of AsyncHttpRequest . You can ignore headers input argument for the moment. We will cover it later. The output is declared as \"Object\" so that the function can return any data structure using a HashMap or PoJo. You may want to review the REST endpoint /api/simple/{task}/* in the rest.yaml config file to see how it is connected to the \"hello.simple\" function. We take a minimalist approach for the rest.yaml syntax. The parser will detect any syntax errors. Please check application log to ensure all REST endpoint entries in rest.yaml file are valid.","title":"TypedLambdaFunction"},{"location":"guides/CHAPTER-1/#write-your-first-function","text":"Using the lambda-example as a template, let's create your first function by adding a function in the \"services\" package folder. You will give it the route name \"my.first.function\" in the \"PreLoad\" annotation. Note that route name must use lower case letters and numbers separated by the period character. @PreLoad(route = \"my.first.function\", instances = 10) public class MyFirstFunction implements TypedLambdaFunction { @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { // your business logic here return input; } } To connect this function with a REST endpoint, you can declare a new REST endpoint in the rest.yaml like this: - service: \"my.first.function\" methods: [ 'GET' ] url: \"/api/hello/my/function\" timeout: 20s cors: cors_1 headers: header_1 tracing: true If you do not put any business logic, the above function will echo the incoming HTTP request object back to the browser. Now you can examine the input HTTP request object and perform some data transformation before returning a result. The AsyncHttpRequest class allows you to access data structure such as HTTP method, URL, path parameters, query parameters, cookies, etc. When you click the \"rebuild\" button in IDE and run the \"MainApp\", the new function will be available in the application. Alternatively, you can also do mvn clean package to generate a new executable JAR and run the JAR from command line. To test your new function, visit http://127.0.0.1:8085/api/hello/my/function","title":"Write your first function"},{"location":"guides/CHAPTER-1/#event-driven-design","text":"Your function automatically uses an in-memory event bus. The HTTP request from the browser is converted to an event by the system for delivery to your function as the \"input\" argument. The underlying HTTP server is asynchronous and non-blocking. i.e. it does not consume CPU resources while waiting for a response. This composable architecture allows you to design and implement applications so that you have precise control of performance and throughput. Performance tuning is much easier.","title":"Event driven design"},{"location":"guides/CHAPTER-1/#deploy-your-new-application","text":"You can assemble related functions in a single composable application, and it can be compiled and built into a single \"executable\" for deployment using mvn clean package . The executable JAR is in the target folder. Composable application is by definition cloud native. It is designed to be deployable using Kubernetes or serverless. A sample Dockerfile for your executable JAR may look like this: FROM adoptopenjdk/openjdk11:jre-11.0.11_9-alpine EXPOSE 8083 WORKDIR /app COPY target/your-app-name.jar . ENTRYPOINT [\"java\",\"-jar\",\"your-app-name.jar\"] Home Chapter-2 Table of Contents Function Execution Strategy","title":"Deploy your new application"},{"location":"guides/CHAPTER-10/","text":"Migration Guide Let's discuss some migration tips from Mercury version 2 to 3. Breaking changes Mercury version 3 is a fully non-blocking event system. If you are using Mercury version 2 for production, please note that version 2 codebase has been archived to the \"release/v2-8-0\" branch. To enjoy rapid software development and higher application performance and throughput, we recommend porting your application to Mercury version 3 as soon as possible. The following are the breaking changes that require some code refactoring: Retired blocking APIs - the \"po.request\" methods for RPC have been replaced by the new \"FastRPC\" APIs. Distributed tracing - a new \"PostOffice\" class is available for compatibility with coroutine. Support three function execution strategies - kernel thread pool, coroutine and suspend function. We understand the inconvenience of a major release upgrade in production environment. We believe that the benefits would out-weight the refactoring effort. Your new code will be composable, easier to read and faster. Writing code with Mercury version 3 platform-core is straight forward. By default, all functions run as coroutines. To tell the system to execute the function in a kernel thread pool, you may use the KernelThreadRunner annotation. To write a suspend function, you can use IDE (JetBrains Intellij) automated code conversion to copy-n-paste Java statements into a KotlinLambdaFunction. This is the easiest way to port code. The conversion accuracy is high. With some minor touch up, you would get your new functions up and running quickly. Step-by-step upgrade Global replace of \"PostOffice\" to \"EventEmitter\" The old PostOffice has been renamed as \"EventEmitter\". You can do a \"global search and replace\" to change the class name. Fix broken code for RPC calls Since blocking APIs have been removed, the original PostOffice's \"request\" methods are no longer available. There are two ways to refactor the RPC calls. Convert code to asynchronous RPC calls You can use the \"asyncRequest\" methods for RPC and fork-n-join. Since the asyncRequest's result is a \"Future\" object. You must implement the \"onSuccess\" and optionally the \"onFailure\" logic blocks. Since your new code is asynchronous, the function will immediately return before a future response arrives. If your function may be called by another function, this would break your code. For this use case, you can annotate your function as an \"EventInterceptor\" and return a dummy \"null\" value. As an EventInterceptor, you can inspect metadata of the incoming event to retrieve the \"replyTo\" and \"correlationId\". @EventInterceptor @PreLoad(route=\"my.function\", instances=10) public class MyFunction implements TypedLambdaFunction { @Override public Void handleEvent(Map headers, EventEnvelope input, int instance) { PostOffice po = new PostOffice(headers, instance); // make asyncRequest RPC call EventEnvelope request = new EventEnvelope().setTo(\"some.target.service\") .setBody(input.getBody()); po.asyncRequest(request, 5000) .onSuccess(result -> { String replyTo = input.getReplyTo(); String correlationId = input.getCorrelationId(); if (replyTo != null && correlationId != null) { EventEnvelope response = new EventEnvelope(); response.setTo(replyTo).setBody(result.getBody()) .setCorrelationId(correlationId); po.send(response); } }); return null; } } In the above example, \"my.function\" will immediately return a dummy \"null\" value which will be ignored by the event system. When it receives a response from a downstream service, it can return result to the upstream service by asynchronously sending a response. Convert RPC code to a suspend function You can convert your function containing RPC calls to a suspend function using the KotlinLambdaFunction interface. It may look like this: @PreLoad(route=\"my.function\", instances=10) class MyFunction: KotlinLambdaFunction { override suspend fun handleEvent(headers: Map, input: EventEnvelope, instance: Int): Any { val fastRPC = FastRPC(headers) val request = EventEnvelope().setTo(\"some.target.service\").setBody(input.body) return fastRPC.awaitRequest(request, 5000) } } The above example serves the same purpose as the asynchronous \"my.function\" earlier. The code is much easier to read because it is expressed in a sequential manner. Sequential non-blocking code communicates the intent clearly, and we highly recommend this coding style. If you are new to Kotlin, you may want to leverage the Intellij IDE automated code conversion feature. Just create a dummy Java class as a sketchpad. Write your code in Java and copy-n-paste the Java statements into the new Kotlin class. The IDE will convert the code automatically. The code conversion is highly accurate. With some minor touch up, your new code will be up and running quickly. The new PostOffice API The new PostOffice class is backward compatible with the original asynchronous RPC and fork-n-join methods. You can obtain an instance of the PostOffice API in the \"handleEvent\" method of your function. The PostOffice constructor takes function route name, optional trace ID and path from the headers of the incoming event. These are READ only metadata inserted by the event system. It also needs the worker instance number to track the current transaction. @Override public Map handleEvent(Map headers, EventEnvelope event, int instance) { PostOffice po = new PostOffice(headers, instance); // your business logic here } When you use the PostOffice to send events or make RPC calls to other functions, the system can propagate distributed tracing information along the transaction flow automatically. The new FastRPC API The non-blocking \"awaitRequest\" methods for RPC and fork-n-join are available in a new FastRPC kotlin class. The constructor is similar to the PostOffice. val fastRPC = FastRPC(headers) Distributed tracing The new PostOffice and FastRPC will propagate distributed tracing information along multiple functions in a transaction path. It will automatically detect if \"tracing\" is enabled for a transaction. AsyncHttpClient service In Mercury version 3, the \"async.http.request\" function can be used as a non-blocking HTTP client. To make an HTTP request to an external REST endpoint, you can create an HTTP request object using the AsyncHttpRequest class and make an async RPC call to the \"async.http.request\" function like this: PostOffice po = new PostOffice(headers, instance); AsyncHttpRequest req = new AsyncHttpRequest(); req.setMethod(\"GET\"); req.setHeader(\"accept\", \"application/json\"); req.setUrl(\"/api/hello/world?hello world=abc\"); req.setQueryParameter(\"x1\", \"y\"); List list = new ArrayList<>(); list.add(\"a\"); list.add(\"b\"); req.setQueryParameter(\"x2\", list); req.setTargetHost(\"http://127.0.0.1:8083\"); EventEnvelope request = new EventEnvelope().setTo(\"async.http.request\").setBody(req); Future res = po.asyncRequest(request, RPC_TIMEOUT); res.onSuccess(response -> { // do something with the result }); In a suspend function using KotlinLambdaFunction, the same logic may look like this: val req = AsyncHttpRequest() req.setMethod(\"GET\") req.setHeader(\"accept\", \"application/json\") req.setUrl(\"/api/hello/world?hello world=abc\") req.setQueryParameter(\"x1\", \"y\") val list: MutableList = ArrayList() list.add(\"a\") list.add(\"b\") req.setQueryParameter(\"x2\", list) req.setTargetHost(\"http://127.0.0.1:8083\") val request = EventEnvelope().setTo(\"some.target.service\").setBody(req) val response = fastRPC.awaitRequest(request, 5000) // do something with the result There is virtually no performance difference between the asynchronous approach and sequential non-blocking style. However, the latter demands less CPU resources and yields higher throughput. Kernel thread pool A Java function implementing the LambdaFunction or TypedLambdaFunction will be executed as a coroutine. To tell the system to run a function using kernel thread pool, you can add the KernelThreadRunner annotation. When using a kernel thread pool, please reduce the number of concurrent worker instances when you register your function. You can register your function using the PreLoad annotation. For on-demand functions, you can programmatically register the function using the platform APIs. Java provides preemptive multitasking using kernel threads. It offers the highest performance in terms of operations per second. If your function is computational intensive and long-running, this function execution strategy is ideal. However, please be reminded that kernel thread pool is a finite resources and thus an application should not run too many concurrent kernel threads. The context switching overheads would significantly reduce overall performance when the number of concurrent kernel threads exceed the available CPU power. A rule of thumb is to keep the number of concurrent kernel threads to around 100. Coroutine By default, the system will execute functions in the event loop, thus reducing CPU load. Suspend function For a function that make RPC calls, we would recommend writing it as a suspend function using the KotlinLambdaFunction interface. This yields higher throughput to support more concurrent users and sessions. Things to avoid You should avoid blocking methods in your functions. For example, the \"Synchronous\" keyword, Object wait and lock, \"Thread\" sleep method, BlockingQueue, etc. The non-blocking \"delay\" API is a direct replacement of the \"Thread.sleep\" method. You can also use the PostOffice's \"sendLater\" API to schedule an event. Conclusion While Mercury has been enhanced from the ground up, the core APIs are intact. The main breaking change is the removal of blocking RPC APIs. You should leverage IDE automated code conversion to reduce migration risks. The three function execution strategies would provide low-level control of how your application runs, making performance tuning more scientific. Chapter-9 Home Appendix-I API overview Table of Contents Application Configuration","title":"Chapter-10"},{"location":"guides/CHAPTER-10/#migration-guide","text":"Let's discuss some migration tips from Mercury version 2 to 3.","title":"Migration Guide"},{"location":"guides/CHAPTER-10/#breaking-changes","text":"Mercury version 3 is a fully non-blocking event system. If you are using Mercury version 2 for production, please note that version 2 codebase has been archived to the \"release/v2-8-0\" branch. To enjoy rapid software development and higher application performance and throughput, we recommend porting your application to Mercury version 3 as soon as possible. The following are the breaking changes that require some code refactoring: Retired blocking APIs - the \"po.request\" methods for RPC have been replaced by the new \"FastRPC\" APIs. Distributed tracing - a new \"PostOffice\" class is available for compatibility with coroutine. Support three function execution strategies - kernel thread pool, coroutine and suspend function. We understand the inconvenience of a major release upgrade in production environment. We believe that the benefits would out-weight the refactoring effort. Your new code will be composable, easier to read and faster. Writing code with Mercury version 3 platform-core is straight forward. By default, all functions run as coroutines. To tell the system to execute the function in a kernel thread pool, you may use the KernelThreadRunner annotation. To write a suspend function, you can use IDE (JetBrains Intellij) automated code conversion to copy-n-paste Java statements into a KotlinLambdaFunction. This is the easiest way to port code. The conversion accuracy is high. With some minor touch up, you would get your new functions up and running quickly.","title":"Breaking changes"},{"location":"guides/CHAPTER-10/#step-by-step-upgrade","text":"","title":"Step-by-step upgrade"},{"location":"guides/CHAPTER-10/#global-replace-of-postoffice-to-eventemitter","text":"The old PostOffice has been renamed as \"EventEmitter\". You can do a \"global search and replace\" to change the class name.","title":"Global replace of \"PostOffice\" to \"EventEmitter\""},{"location":"guides/CHAPTER-10/#fix-broken-code-for-rpc-calls","text":"Since blocking APIs have been removed, the original PostOffice's \"request\" methods are no longer available. There are two ways to refactor the RPC calls.","title":"Fix broken code for RPC calls"},{"location":"guides/CHAPTER-10/#convert-code-to-asynchronous-rpc-calls","text":"You can use the \"asyncRequest\" methods for RPC and fork-n-join. Since the asyncRequest's result is a \"Future\" object. You must implement the \"onSuccess\" and optionally the \"onFailure\" logic blocks. Since your new code is asynchronous, the function will immediately return before a future response arrives. If your function may be called by another function, this would break your code. For this use case, you can annotate your function as an \"EventInterceptor\" and return a dummy \"null\" value. As an EventInterceptor, you can inspect metadata of the incoming event to retrieve the \"replyTo\" and \"correlationId\". @EventInterceptor @PreLoad(route=\"my.function\", instances=10) public class MyFunction implements TypedLambdaFunction { @Override public Void handleEvent(Map headers, EventEnvelope input, int instance) { PostOffice po = new PostOffice(headers, instance); // make asyncRequest RPC call EventEnvelope request = new EventEnvelope().setTo(\"some.target.service\") .setBody(input.getBody()); po.asyncRequest(request, 5000) .onSuccess(result -> { String replyTo = input.getReplyTo(); String correlationId = input.getCorrelationId(); if (replyTo != null && correlationId != null) { EventEnvelope response = new EventEnvelope(); response.setTo(replyTo).setBody(result.getBody()) .setCorrelationId(correlationId); po.send(response); } }); return null; } } In the above example, \"my.function\" will immediately return a dummy \"null\" value which will be ignored by the event system. When it receives a response from a downstream service, it can return result to the upstream service by asynchronously sending a response.","title":"Convert code to asynchronous RPC calls"},{"location":"guides/CHAPTER-10/#convert-rpc-code-to-a-suspend-function","text":"You can convert your function containing RPC calls to a suspend function using the KotlinLambdaFunction interface. It may look like this: @PreLoad(route=\"my.function\", instances=10) class MyFunction: KotlinLambdaFunction { override suspend fun handleEvent(headers: Map, input: EventEnvelope, instance: Int): Any { val fastRPC = FastRPC(headers) val request = EventEnvelope().setTo(\"some.target.service\").setBody(input.body) return fastRPC.awaitRequest(request, 5000) } } The above example serves the same purpose as the asynchronous \"my.function\" earlier. The code is much easier to read because it is expressed in a sequential manner. Sequential non-blocking code communicates the intent clearly, and we highly recommend this coding style. If you are new to Kotlin, you may want to leverage the Intellij IDE automated code conversion feature. Just create a dummy Java class as a sketchpad. Write your code in Java and copy-n-paste the Java statements into the new Kotlin class. The IDE will convert the code automatically. The code conversion is highly accurate. With some minor touch up, your new code will be up and running quickly.","title":"Convert RPC code to a suspend function"},{"location":"guides/CHAPTER-10/#the-new-postoffice-api","text":"The new PostOffice class is backward compatible with the original asynchronous RPC and fork-n-join methods. You can obtain an instance of the PostOffice API in the \"handleEvent\" method of your function. The PostOffice constructor takes function route name, optional trace ID and path from the headers of the incoming event. These are READ only metadata inserted by the event system. It also needs the worker instance number to track the current transaction. @Override public Map handleEvent(Map headers, EventEnvelope event, int instance) { PostOffice po = new PostOffice(headers, instance); // your business logic here } When you use the PostOffice to send events or make RPC calls to other functions, the system can propagate distributed tracing information along the transaction flow automatically.","title":"The new PostOffice API"},{"location":"guides/CHAPTER-10/#the-new-fastrpc-api","text":"The non-blocking \"awaitRequest\" methods for RPC and fork-n-join are available in a new FastRPC kotlin class. The constructor is similar to the PostOffice. val fastRPC = FastRPC(headers)","title":"The new FastRPC API"},{"location":"guides/CHAPTER-10/#distributed-tracing","text":"The new PostOffice and FastRPC will propagate distributed tracing information along multiple functions in a transaction path. It will automatically detect if \"tracing\" is enabled for a transaction.","title":"Distributed tracing"},{"location":"guides/CHAPTER-10/#asynchttpclient-service","text":"In Mercury version 3, the \"async.http.request\" function can be used as a non-blocking HTTP client. To make an HTTP request to an external REST endpoint, you can create an HTTP request object using the AsyncHttpRequest class and make an async RPC call to the \"async.http.request\" function like this: PostOffice po = new PostOffice(headers, instance); AsyncHttpRequest req = new AsyncHttpRequest(); req.setMethod(\"GET\"); req.setHeader(\"accept\", \"application/json\"); req.setUrl(\"/api/hello/world?hello world=abc\"); req.setQueryParameter(\"x1\", \"y\"); List list = new ArrayList<>(); list.add(\"a\"); list.add(\"b\"); req.setQueryParameter(\"x2\", list); req.setTargetHost(\"http://127.0.0.1:8083\"); EventEnvelope request = new EventEnvelope().setTo(\"async.http.request\").setBody(req); Future res = po.asyncRequest(request, RPC_TIMEOUT); res.onSuccess(response -> { // do something with the result }); In a suspend function using KotlinLambdaFunction, the same logic may look like this: val req = AsyncHttpRequest() req.setMethod(\"GET\") req.setHeader(\"accept\", \"application/json\") req.setUrl(\"/api/hello/world?hello world=abc\") req.setQueryParameter(\"x1\", \"y\") val list: MutableList = ArrayList() list.add(\"a\") list.add(\"b\") req.setQueryParameter(\"x2\", list) req.setTargetHost(\"http://127.0.0.1:8083\") val request = EventEnvelope().setTo(\"some.target.service\").setBody(req) val response = fastRPC.awaitRequest(request, 5000) // do something with the result There is virtually no performance difference between the asynchronous approach and sequential non-blocking style. However, the latter demands less CPU resources and yields higher throughput.","title":"AsyncHttpClient service"},{"location":"guides/CHAPTER-10/#kernel-thread-pool","text":"A Java function implementing the LambdaFunction or TypedLambdaFunction will be executed as a coroutine. To tell the system to run a function using kernel thread pool, you can add the KernelThreadRunner annotation. When using a kernel thread pool, please reduce the number of concurrent worker instances when you register your function. You can register your function using the PreLoad annotation. For on-demand functions, you can programmatically register the function using the platform APIs. Java provides preemptive multitasking using kernel threads. It offers the highest performance in terms of operations per second. If your function is computational intensive and long-running, this function execution strategy is ideal. However, please be reminded that kernel thread pool is a finite resources and thus an application should not run too many concurrent kernel threads. The context switching overheads would significantly reduce overall performance when the number of concurrent kernel threads exceed the available CPU power. A rule of thumb is to keep the number of concurrent kernel threads to around 100.","title":"Kernel thread pool"},{"location":"guides/CHAPTER-10/#coroutine","text":"By default, the system will execute functions in the event loop, thus reducing CPU load.","title":"Coroutine"},{"location":"guides/CHAPTER-10/#suspend-function","text":"For a function that make RPC calls, we would recommend writing it as a suspend function using the KotlinLambdaFunction interface. This yields higher throughput to support more concurrent users and sessions.","title":"Suspend function"},{"location":"guides/CHAPTER-10/#things-to-avoid","text":"You should avoid blocking methods in your functions. For example, the \"Synchronous\" keyword, Object wait and lock, \"Thread\" sleep method, BlockingQueue, etc. The non-blocking \"delay\" API is a direct replacement of the \"Thread.sleep\" method. You can also use the PostOffice's \"sendLater\" API to schedule an event.","title":"Things to avoid"},{"location":"guides/CHAPTER-10/#conclusion","text":"While Mercury has been enhanced from the ground up, the core APIs are intact. The main breaking change is the removal of blocking RPC APIs. You should leverage IDE automated code conversion to reduce migration risks. The three function execution strategies would provide low-level control of how your application runs, making performance tuning more scientific. Chapter-9 Home Appendix-I API overview Table of Contents Application Configuration","title":"Conclusion"},{"location":"guides/CHAPTER-2/","text":"Function Execution Strategies Define a function In a composable application, each user function is self-contained with zero dependencies with other user functions. A function is a class that implements the LambdaFunction, TypedLambdaFunction or KotlinLambdaFunction interface. Within each function boundary, it may have private methods that are fully contained within the class. As discussed in Chapter-1, a function may look like this: @PreLoad(route = \"my.first.function\", instances = 10) public class MyFirstFunction implements TypedLambdaFunction { @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { // your business logic here return input; } } A function is an event listener with the \"handleEvent\" method. The data structures of input and output are defined by API interface contract during application design phase. A \"route\" or \"topic\" is associated with each function. When an event arrives to the topic, the function is executed where the event is de-serialized as the function's input. In the above example, the input is AsyncHttpRequest because this function is designed to handle an HTTP request event from a REST endpoint defined in the \"rest.yaml\" configuration file. We set the output as \"Object\" so that there is flexibility in returning a HashMap or a PoJo. You can also enforce the use of a PoJo by updating the output type. A single transaction may involve multiple functions. For example, the user submits a form from a browser that sends an HTTP request to a function. In MVC pattern, the function receiving the user's input is the \"controller\". It carries out input validation and forwards the event to a business logic function (the \"view\") that performs some processing and then submits the event to a data persistent function (the \"model\") to save a record into the database. In cloud native application, the transaction flow may be more sophisticated than the typical \"mvc\" style. You can do \"event orchestration\" in the function receiving the HTTP request and then make event requests to various functions. This \"event orchestration\" can be done by code using the \"PostOffice\" and/or \"FastRPC\" API. To further reduce coding effort, you can perform \"event orchestration\" by configuration using \"Event Script\". This feature is available in Mercury version 4.0: Mercury v4: https://github.com/Accenture/mercury-composable Documentation: https://accenture.github.io/mercury-composable/ Extensible authentication function You can add authentication function using the optional authentication tag in a service. In \"rest.yaml\", a service for a REST endpoint refers to a function in your application. An authentication function can be written using a TypedLambdaFunction that takes the input as a \"AsyncHttpRequest\". Your authentication function can return a boolean value to indicate if the request should be accepted or rejected. A typical authentication function may validate an HTTP header or cookie. e.g. forward the \"Bearer token\" from the \"Authorization\" header to your organization's OAuth 2.0 Identity Provider for validation. To approve an incoming request, your custom authentication function can return true . Optionally, you can add \"session\" key-values by returning an EventEnvelope like this: return new EventEnvelope().setHeader(\"user_id\", \"A12345\").setBody(true); The above example approves the incoming request and returns a \"session\" variable (\"user_id\": \"A12345\") to the next task. If your authentication function returns false , the user will receive a \"HTTP-401 Unauthorized\" error response. You can also control the status code and error message by throwing an AppException like this: throw new AppException(401, \"Invalid credentials\"); A composable application is assembled from a collection of modular functions. For example, data persistence functions and authentication functions are likely to be reusable in many applications. Number of workers for a function @PreLoad(route = \"my.first.function\", instances = 10) In the above function, the parameter \"instances\" tells the system to reserve a number of workers for the function. Workers are running on-demand to handle concurrent user requests. Note that you can use smaller number of workers to handle many concurrent users if your function finishes processing very quickly. If not, you should reserve more workers to handle the work load. Concurrency requires careful planning for optimal performance and throughput. Let's review the strategies for function execution. Three strategies for function execution A function is executed when an event arrives. There are three function execution strategies. Strategy Advantage Disadvantage Kernel threads Highest performance in terms of operations per seconds Lower number of concurrent threads due to high context switching overheads Coroutine Highest throughput in terms of concurrent users served by virtual threads concurrently Not suitable for long running tasks Suspend function Synchronous \"non-blocking\" for RPC (request-response) that makes code easier to read and maintain Not suitable for long running tasks Kernel thread pool When you write a function using LambdaFunction and TypedLambdaFunction with the KernelThreadRunner annotation, the function will be executed using \"kernel thread pool\" and the Java VM will run your function in native \"preemptive multitasking\" mode. While preemptive multitasking fully utilizes the CPU, its context switching overheads may increase as the number of kernel threads grow. As a rule of thumb, you should control the maximum number of kernel threads to less than 200. The parameter kernel.thread.pool is defined with a default value of 100. You can change this value to adjust to the actual CPU power in your environment. Keep the default value for best performance unless you have tested the limit in your environment. When you have more concurrent requests, your application may slow down because some functions are blocked when the number of concurrent kernel threads is reached. You should reduce the number of \"instances\" (i.e. worker pool) for a function to a small number so that your application does not exceed the maximum limit of the kernel.thread.pool parameter. Kernel threads are precious and finite resources. When your function is computational intensive or making external HTTP or database calls in a synchronous blocking manner, you may use it with a small number of worker instances. To rapidly release kernel thread resources, you can write \"asynchronous\" code. i.e. for event-driven programming, you can send event to another function asynchronously, and you can create a callback function to listen to responses. For RPC call, you can use the asyncRequest method to write asynchronous RPC calls. However, coding for asynchronous RPC pattern is more challenging. For example, you may want to return a \"pending\" result immediately using HTTP-202. Your code will move on to execute using a \"future\" that will execute callback methods ( onSuccess and onFailure ). Another approach is to annotate the function as an EventInterceptor so that your function can respond to the user in a \"future\" callback. For ease of programming, we recommend using suspend function to handle RPC calls. It allows you to program your function in a sequential manner similar to the \"async/await\" pattern of other programming languages. Coroutine By default, the system will run your function as a coroutine unless you specify KernelThreadRunner annotation in your function or declared it as a suspend function using KotlinLambdaFunction interface. Normally, coroutines are executed in an event loop using a single kernel thread. Note that the underlying Eclipse vertx is a multithreaded event system that executes coroutines in a small number of event loops concurrently for better performance. As a result, the system can handle tens of thousands of coroutines running concurrently. Since coroutine is running in a single thread, you must avoid writing \"blocking\" code because it would slow down the whole application significantly. If your function can finish processing very quickly, coroutine is ideal. Suspend function A suspend function is a coroutine that can be suspended and resumed. The best use case for a suspend function is for handling of \"sequential non-blocking\" request-response. This is the same as \"async/await\" in node.js and other programming language. To implement a \"suspend function\", you must implement the KotlinLambdaFunction interface and write code in Kotlin. If you are new to Kotlin, please download and run JetBrains Intellij IDE. The quickest way to get productive in Kotlin is to write a few statements of Java code in a placeholder class and then copy-n-paste the Java statements into the KotlinLambdaFunction's handleEvent method. Intellij will automatically convert Java code into Kotlin. The automated code conversion is mostly accurate (roughly 90%). You may need some touch up to polish the converted Kotlin code. In a suspend function, you can use a set of \"await\" methods to make non-blocking request-response (RPC) calls. For example, to make a RPC call to another function, you can use the awaitRequest method. Please refer to the FileUploadDemo class in the \"examples/lambda-example\" project. val po = PostOffice(headers, instance) val fastRPC = FastRPC(headers) val req = EventEnvelope().setTo(streamId).setHeader(TYPE, READ) while (true) { val event = fastRPC.awaitRequest(req, 5000) // handle the response event if (EOF == event.headers[TYPE]) { log.info(\"{} saved\", file) awaitBlocking { out.close() } po.send(streamId, Kv(TYPE, CLOSE)) break; } if (DATA == event.headers[TYPE]) { val block = event.body if (block is ByteArray) { total += block.size log.info(\"Saving {} - {} bytes\", filename, block.size) awaitBlocking { out.write(block) } } } } In the above code segment, it has a \"while\" loop to make RPC calls to continuously \"fetch\" blocks of data from a stream. The status of the stream is indicated in the event header \"type\". It will exit the \"while\" loop when it detects the \"End of Stream (EOF)\" signal. Suspend function will be \"suspended\" when it is waiting for a response. When it is suspended, it does not consume CPU resources, thus your application can handle a large number of concurrent users and requests. Coroutines run in a \"cooperative multitasking\" manner. Technically, each function is running sequentially. However, when many functions are suspended during waiting, it appears that all functions are running concurrently. You may notice that there is an awaitBlocking wrapper in the code segment. Sometimes, you cannot avoid blocking code. In the above example, the Java's FileOutputStream is a blocking method. To ensure that a small piece of blocking code in a coroutine does not slow down the \"event loop\", you can apply the awaitBlocking wrapper method. The system will run the blocking code in a separate worker thread without blocking the event loop. In addition to the \"await\" sets of API, the delay(milliseconds) method puts your function into sleep in a non-blocking manner. The yield() method is useful when your function requires more time to execute complex business logic. You can add the yield() statement before you execute a block of code. The yield method releases control to the event loop so that other coroutines and suspend functions will not be blocked by a heavy weighted function. Suspend function is a powerful way to write high throughput application. Your code is presented in a sequential flow that is easier to write and maintain. You may want to try the demo \"file upload\" REST endpoint to see how suspend function behaves. If you follow Chapter-1, your lambda example application is already running. To test the file upload endpoint, here is a simple Python script: import requests files = {'file': open('some_data_file.txt', 'rb')} r = requests.post('http://127.0.0.1:8085/api/upload', files=files) print(r.text) This assumes you have the python \"requests\" package installed. If not, please do pip install requests to install the dependency. The uploaded file will be kept in the \"/tmp/upload-download-demo\" folder. To download the file, point your browser to http://127.0.0.1:8085/api/download/some_data_file.txt Your browser will usually save the file in the \"Downloads\" folder. You may notice that the FileDownloadDemo class is written in Java using the interface TypedLambdaFunction . The FileDownloadDemo class will run using a kernel thread. Note that each function is independent and the functions with different execution strategies can communicate in events. The output of your function is an \"EventEnvelope\" so that you can set the HTTP response header correctly. e.g. content type and filename. When downloading a file, the FileDownloadDemo function will block if it is sending a large file. Therefore, you want it to run as a kernel thread. For very large file download, you may want to write the FileDownloadDemo function using asynchronous programming with the EventInterceptor annotation or implement a suspend function using KotlinLambdaFunction. Suspend function is non-blocking. Chapter-1 Home Chapter-3 Introduction Table of Contents REST Automation","title":"Chapter-2"},{"location":"guides/CHAPTER-2/#function-execution-strategies","text":"","title":"Function Execution Strategies"},{"location":"guides/CHAPTER-2/#define-a-function","text":"In a composable application, each user function is self-contained with zero dependencies with other user functions. A function is a class that implements the LambdaFunction, TypedLambdaFunction or KotlinLambdaFunction interface. Within each function boundary, it may have private methods that are fully contained within the class. As discussed in Chapter-1, a function may look like this: @PreLoad(route = \"my.first.function\", instances = 10) public class MyFirstFunction implements TypedLambdaFunction { @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { // your business logic here return input; } } A function is an event listener with the \"handleEvent\" method. The data structures of input and output are defined by API interface contract during application design phase. A \"route\" or \"topic\" is associated with each function. When an event arrives to the topic, the function is executed where the event is de-serialized as the function's input. In the above example, the input is AsyncHttpRequest because this function is designed to handle an HTTP request event from a REST endpoint defined in the \"rest.yaml\" configuration file. We set the output as \"Object\" so that there is flexibility in returning a HashMap or a PoJo. You can also enforce the use of a PoJo by updating the output type. A single transaction may involve multiple functions. For example, the user submits a form from a browser that sends an HTTP request to a function. In MVC pattern, the function receiving the user's input is the \"controller\". It carries out input validation and forwards the event to a business logic function (the \"view\") that performs some processing and then submits the event to a data persistent function (the \"model\") to save a record into the database. In cloud native application, the transaction flow may be more sophisticated than the typical \"mvc\" style. You can do \"event orchestration\" in the function receiving the HTTP request and then make event requests to various functions. This \"event orchestration\" can be done by code using the \"PostOffice\" and/or \"FastRPC\" API. To further reduce coding effort, you can perform \"event orchestration\" by configuration using \"Event Script\". This feature is available in Mercury version 4.0: Mercury v4: https://github.com/Accenture/mercury-composable Documentation: https://accenture.github.io/mercury-composable/","title":"Define a function"},{"location":"guides/CHAPTER-2/#extensible-authentication-function","text":"You can add authentication function using the optional authentication tag in a service. In \"rest.yaml\", a service for a REST endpoint refers to a function in your application. An authentication function can be written using a TypedLambdaFunction that takes the input as a \"AsyncHttpRequest\". Your authentication function can return a boolean value to indicate if the request should be accepted or rejected. A typical authentication function may validate an HTTP header or cookie. e.g. forward the \"Bearer token\" from the \"Authorization\" header to your organization's OAuth 2.0 Identity Provider for validation. To approve an incoming request, your custom authentication function can return true . Optionally, you can add \"session\" key-values by returning an EventEnvelope like this: return new EventEnvelope().setHeader(\"user_id\", \"A12345\").setBody(true); The above example approves the incoming request and returns a \"session\" variable (\"user_id\": \"A12345\") to the next task. If your authentication function returns false , the user will receive a \"HTTP-401 Unauthorized\" error response. You can also control the status code and error message by throwing an AppException like this: throw new AppException(401, \"Invalid credentials\"); A composable application is assembled from a collection of modular functions. For example, data persistence functions and authentication functions are likely to be reusable in many applications.","title":"Extensible authentication function"},{"location":"guides/CHAPTER-2/#number-of-workers-for-a-function","text":"@PreLoad(route = \"my.first.function\", instances = 10) In the above function, the parameter \"instances\" tells the system to reserve a number of workers for the function. Workers are running on-demand to handle concurrent user requests. Note that you can use smaller number of workers to handle many concurrent users if your function finishes processing very quickly. If not, you should reserve more workers to handle the work load. Concurrency requires careful planning for optimal performance and throughput. Let's review the strategies for function execution.","title":"Number of workers for a function"},{"location":"guides/CHAPTER-2/#three-strategies-for-function-execution","text":"A function is executed when an event arrives. There are three function execution strategies. Strategy Advantage Disadvantage Kernel threads Highest performance in terms of operations per seconds Lower number of concurrent threads due to high context switching overheads Coroutine Highest throughput in terms of concurrent users served by virtual threads concurrently Not suitable for long running tasks Suspend function Synchronous \"non-blocking\" for RPC (request-response) that makes code easier to read and maintain Not suitable for long running tasks","title":"Three strategies for function execution"},{"location":"guides/CHAPTER-2/#kernel-thread-pool","text":"When you write a function using LambdaFunction and TypedLambdaFunction with the KernelThreadRunner annotation, the function will be executed using \"kernel thread pool\" and the Java VM will run your function in native \"preemptive multitasking\" mode. While preemptive multitasking fully utilizes the CPU, its context switching overheads may increase as the number of kernel threads grow. As a rule of thumb, you should control the maximum number of kernel threads to less than 200. The parameter kernel.thread.pool is defined with a default value of 100. You can change this value to adjust to the actual CPU power in your environment. Keep the default value for best performance unless you have tested the limit in your environment. When you have more concurrent requests, your application may slow down because some functions are blocked when the number of concurrent kernel threads is reached. You should reduce the number of \"instances\" (i.e. worker pool) for a function to a small number so that your application does not exceed the maximum limit of the kernel.thread.pool parameter. Kernel threads are precious and finite resources. When your function is computational intensive or making external HTTP or database calls in a synchronous blocking manner, you may use it with a small number of worker instances. To rapidly release kernel thread resources, you can write \"asynchronous\" code. i.e. for event-driven programming, you can send event to another function asynchronously, and you can create a callback function to listen to responses. For RPC call, you can use the asyncRequest method to write asynchronous RPC calls. However, coding for asynchronous RPC pattern is more challenging. For example, you may want to return a \"pending\" result immediately using HTTP-202. Your code will move on to execute using a \"future\" that will execute callback methods ( onSuccess and onFailure ). Another approach is to annotate the function as an EventInterceptor so that your function can respond to the user in a \"future\" callback. For ease of programming, we recommend using suspend function to handle RPC calls. It allows you to program your function in a sequential manner similar to the \"async/await\" pattern of other programming languages.","title":"Kernel thread pool"},{"location":"guides/CHAPTER-2/#coroutine","text":"By default, the system will run your function as a coroutine unless you specify KernelThreadRunner annotation in your function or declared it as a suspend function using KotlinLambdaFunction interface. Normally, coroutines are executed in an event loop using a single kernel thread. Note that the underlying Eclipse vertx is a multithreaded event system that executes coroutines in a small number of event loops concurrently for better performance. As a result, the system can handle tens of thousands of coroutines running concurrently. Since coroutine is running in a single thread, you must avoid writing \"blocking\" code because it would slow down the whole application significantly. If your function can finish processing very quickly, coroutine is ideal.","title":"Coroutine"},{"location":"guides/CHAPTER-2/#suspend-function","text":"A suspend function is a coroutine that can be suspended and resumed. The best use case for a suspend function is for handling of \"sequential non-blocking\" request-response. This is the same as \"async/await\" in node.js and other programming language. To implement a \"suspend function\", you must implement the KotlinLambdaFunction interface and write code in Kotlin. If you are new to Kotlin, please download and run JetBrains Intellij IDE. The quickest way to get productive in Kotlin is to write a few statements of Java code in a placeholder class and then copy-n-paste the Java statements into the KotlinLambdaFunction's handleEvent method. Intellij will automatically convert Java code into Kotlin. The automated code conversion is mostly accurate (roughly 90%). You may need some touch up to polish the converted Kotlin code. In a suspend function, you can use a set of \"await\" methods to make non-blocking request-response (RPC) calls. For example, to make a RPC call to another function, you can use the awaitRequest method. Please refer to the FileUploadDemo class in the \"examples/lambda-example\" project. val po = PostOffice(headers, instance) val fastRPC = FastRPC(headers) val req = EventEnvelope().setTo(streamId).setHeader(TYPE, READ) while (true) { val event = fastRPC.awaitRequest(req, 5000) // handle the response event if (EOF == event.headers[TYPE]) { log.info(\"{} saved\", file) awaitBlocking { out.close() } po.send(streamId, Kv(TYPE, CLOSE)) break; } if (DATA == event.headers[TYPE]) { val block = event.body if (block is ByteArray) { total += block.size log.info(\"Saving {} - {} bytes\", filename, block.size) awaitBlocking { out.write(block) } } } } In the above code segment, it has a \"while\" loop to make RPC calls to continuously \"fetch\" blocks of data from a stream. The status of the stream is indicated in the event header \"type\". It will exit the \"while\" loop when it detects the \"End of Stream (EOF)\" signal. Suspend function will be \"suspended\" when it is waiting for a response. When it is suspended, it does not consume CPU resources, thus your application can handle a large number of concurrent users and requests. Coroutines run in a \"cooperative multitasking\" manner. Technically, each function is running sequentially. However, when many functions are suspended during waiting, it appears that all functions are running concurrently. You may notice that there is an awaitBlocking wrapper in the code segment. Sometimes, you cannot avoid blocking code. In the above example, the Java's FileOutputStream is a blocking method. To ensure that a small piece of blocking code in a coroutine does not slow down the \"event loop\", you can apply the awaitBlocking wrapper method. The system will run the blocking code in a separate worker thread without blocking the event loop. In addition to the \"await\" sets of API, the delay(milliseconds) method puts your function into sleep in a non-blocking manner. The yield() method is useful when your function requires more time to execute complex business logic. You can add the yield() statement before you execute a block of code. The yield method releases control to the event loop so that other coroutines and suspend functions will not be blocked by a heavy weighted function. Suspend function is a powerful way to write high throughput application. Your code is presented in a sequential flow that is easier to write and maintain. You may want to try the demo \"file upload\" REST endpoint to see how suspend function behaves. If you follow Chapter-1, your lambda example application is already running. To test the file upload endpoint, here is a simple Python script: import requests files = {'file': open('some_data_file.txt', 'rb')} r = requests.post('http://127.0.0.1:8085/api/upload', files=files) print(r.text) This assumes you have the python \"requests\" package installed. If not, please do pip install requests to install the dependency. The uploaded file will be kept in the \"/tmp/upload-download-demo\" folder. To download the file, point your browser to http://127.0.0.1:8085/api/download/some_data_file.txt Your browser will usually save the file in the \"Downloads\" folder. You may notice that the FileDownloadDemo class is written in Java using the interface TypedLambdaFunction . The FileDownloadDemo class will run using a kernel thread. Note that each function is independent and the functions with different execution strategies can communicate in events. The output of your function is an \"EventEnvelope\" so that you can set the HTTP response header correctly. e.g. content type and filename. When downloading a file, the FileDownloadDemo function will block if it is sending a large file. Therefore, you want it to run as a kernel thread. For very large file download, you may want to write the FileDownloadDemo function using asynchronous programming with the EventInterceptor annotation or implement a suspend function using KotlinLambdaFunction. Suspend function is non-blocking. Chapter-1 Home Chapter-3 Introduction Table of Contents REST Automation","title":"Suspend function"},{"location":"guides/CHAPTER-3/","text":"REST Automation The platform-core foundation library contains a built-in non-blocking HTTP server that you can use to create REST endpoints. Behind the curtain, it is using the vertx web client and server libraries. The REST automation system is not a code generator. The REST endpoints in the rest.yaml file are handled by the system directly - \"Config is the code\". We will use the \"rest.yaml\" sample configuration file in the \"lambda-example\" project to elaborate the configuration approach. The rest.yaml configuration has three sections: REST endpoint definition CORS header processing HTTP header transformation Turn on the REST automation engine REST automation is optional. To turn on REST automation, add or update the following parameters in the application.properties file (or application.yml if you like). rest.server.port=8085 rest.automation=true yaml.rest.automation=classpath:/rest.yaml When rest.automation=true , you can configure the server port using rest.server.port or server.port . REST automation can co-exist with Spring Boot. Please use rest.server.port for REST automation and server.port for Spring Boot. The yaml.rest.automation tells the system the location of the rest.yaml configuration file. You can configure more than one location and the system will search them sequentially. The following example tells the system to load rest.yaml from \"/tmp/config/rest.yaml\". If the file is not available, it will use the rest.yaml in the project's resources folder. yaml.rest.automation=file:/tmp/config/rest.yaml, classpath:/rest.yaml Defining a REST endpoint The \"rest\" section of the rest.yaml configuration file may contain one or more REST endpoints. A REST endpoint may look like this: - service: [\"hello.world\"] methods: ['GET', 'PUT', 'POST', 'HEAD', 'PATCH', 'DELETE'] url: \"/api/hello/world\" timeout: 10s cors: cors_1 headers: header_1 threshold: 30000 tracing: true In this example, the URL for the REST endpoint is \"/api/hello/world\" and it accepts a list of HTTP methods. When an HTTP request is sent to the URL, the HTTP event will be sent to the function declared with service route name \"hello.world\". The input event will be the \"AsyncHttpRequest\" object. Since the \"hello.world\" function is written as an inline LambdaFunction in the lambda-example application, the AsyncHttpRequest is converted to a HashMap. To process the input as an AsyncHttpRequest object, the function must be written as a regular class. See the \"services\" folder of the lambda-example for additional examples. The \"timeout\" value is the maximum time that REST endpoint will wait for a response from your function. If there is no response within the specified time interval, the user will receive an HTTP-408 timeout exception. The \"authentication\" tag is optional. If configured, the route name given in the authentication tag will be used. The input event will be delivered to a function with the authentication route name. In this example, it is \"v1.api.auth\". Your custom authentication function may look like this: @PreLoad(route = \"v1.api.auth\", instances = 10) public class SimpleAuthentication implements TypedLambdaFunction { @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { // Your authentication logic here. The return value should be true or false. return result; } } Your authentication function can return a boolean value to indicate if the request should be accepted or rejected. If true, the system will send the HTTP request to the service. In this example, it is the \"hello.world\" function. If false, the user will receive an \"HTTP-401 Unauthorized\" exception. Optionally, you can use the authentication function to return some session information after authentication. For example, your authentication can forward the \"Authorization\" header of the incoming HTTP request to your organization's OAuth 2.0 Identity Provider for authentication. To return session information to the next function, the authentication function can return an EventEnvelope. It can set the session information as key-values in the response event headers. In the lambda-example application, there is a demo authentication function in the AuthDemo class with the \"v1.api.auth\" route name. To demonstrate passing session information, the AuthDemo class set the header \"user=demo\" in the result EventEnvelope. You can test this by visiting http://127.0.0.1:8085/api/hello/generic/1 to invoke the \"hello.generic\" function. The console will print: DistributedTrace:55 - trace={path=GET /api/hello/generic/1, service=v1.api.auth, success=true, origin=20230326f84dd5f298b64be4901119ce8b6c18be, exec_time=0.056, start=2023-03-26T20:08:01.702Z, from=http.request, id=aa983244cef7455cbada03c9c2132453, round_trip=1.347, status=200} HelloGeneric:56 - Got session information {user=demo} DistributedTrace:55 - trace={path=GET /api/hello/generic/1, service=hello.generic, success=true, origin=20230326f84dd5f298b64be4901119ce8b6c18be, start=2023-03-26T20:08:01.704Z, exec_time=0.506, from=v1.api.auth, id=aa983244cef7455cbada03c9c2132453, status=200} DistributedTrace:55 - trace={path=GET /api/hello/generic/1, service=async.http.response, success=true, origin=20230326f84dd5f298b64be4901119ce8b6c18be, start=2023-03-26T20:08:01.705Z, exec_time=0.431, from=hello.generic, id=aa983244cef7455cbada03c9c2132453, status=200} This illustrates that the HTTP request has been processed by the \"v1.api.auth\" function. The \"hello.generic\" function is wired to the \"/api/hello/generic/{id}\" endpoint as follows: - service: \"hello.generic\" methods: ['GET'] url: \"/api/hello/generic/{id}\" # Turn on authentication pointing to the \"v1.api.auth\" function authentication: \"v1.api.auth\" timeout: 20s cors: cors_1 headers: header_1 tracing: true The tracing tag tells the system to turn on \"distributed tracing\". In the console log shown above, you see three lines of log from \"distributed trace\" showing that the HTTP request is processed by \"v1.api.auth\" and \"hello.generic\" before returning result to the browser using the \"async.http.response\" function. Note: the \"async.http.response\" is a built-in function to send the HTTP response to the browser. The optional cors and headers tags point to the specific CORS and HEADERS sections respectively. CORS section For ease of development, you can define CORS headers using the CORS section like this. This is a convenient feature for development. For cloud native production system, it is most likely that CORS processing is done at the API gateway level. You can define different sets of CORS headers using different IDs. cors: - id: cors_1 options: - \"Access-Control-Allow-Origin: ${api.origin:*}\" - \"Access-Control-Allow-Methods: GET, DELETE, PUT, POST, PATCH, OPTIONS\" - \"Access-Control-Allow-Headers: Origin, Authorization, X-Session-Id, X-Correlation-Id, Accept, Content-Type, X-Requested-With\" - \"Access-Control-Max-Age: 86400\" headers: - \"Access-Control-Allow-Origin: ${api.origin:*}\" - \"Access-Control-Allow-Methods: GET, DELETE, PUT, POST, PATCH, OPTIONS\" - \"Access-Control-Allow-Headers: Origin, Authorization, X-Session-Id, X-Correlation-Id, Accept, Content-Type, X-Requested-With\" - \"Access-Control-Allow-Credentials: true\" HEADERS section The HEADERS section is used to do some simple transformation for HTTP request and response headers. You can add, keep or drop headers for HTTP request and response. Sample HEADERS section is shown below. headers: - id: header_1 request: # # headers to be inserted # add: [\"hello-world: nice\"] # # keep and drop are mutually exclusive where keep has precedent over drop # i.e. when keep is not empty, it will drop all headers except those to be kept # when keep is empty and drop is not, it will drop only the headers in the drop list # e.g. # keep: ['x-session-id', 'user-agent'] # drop: ['Upgrade-Insecure-Requests', 'cache-control', 'accept-encoding', 'connection'] # drop: ['Upgrade-Insecure-Requests', 'cache-control', 'accept-encoding', 'connection'] response: # # the system can filter the response headers set by a target service, # but it cannot remove any response headers set by the underlying servlet container. # However, you may override non-essential headers using the \"add\" directive. # i.e. don't touch essential headers such as content-length. # # keep: ['only_this_header_and_drop_all'] # drop: ['drop_only_these_headers', 'another_drop_header'] # # add: [\"server: mercury\"] # # You may want to add cache-control to disable browser and CDN caching. # add: [\"Cache-Control: no-cache, no-store\", \"Pragma: no-cache\", # \"Expires: Thu, 01 Jan 1970 00:00:00 GMT\"] # add: - \"Strict-Transport-Security: max-age=31536000\" - \"Cache-Control: no-cache, no-store\" - \"Pragma: no-cache\" - \"Expires: Thu, 01 Jan 1970 00:00:00 GMT\" Static content Static content (HTML/CSS/JS bundle), if any, can be placed in the \"resources/public\" folder in your application project root. It is because the default value for the \"static.html.folder\" parameter in the application configuration is \"classpath:/resources/public\". If you want to place your static content elsewhere, you may adjust this parameter. You may point it to the local file system such as \"file:/tmp/html\". For security reason, you may add the following configuration in the rest.yaml. The following example is shown in the unit test section of the platform-core library module. # # Optional static content handling for HTML/CSS/JS bundle # ------------------------------------------------------- # # no-cache-pages - tells the browser not to cache some specific pages # # The \"filter\" section is a programmatic way to protect certain static content. # # The filter can be used to inspect HTTP path, headers and query parameters. # The typical use case is to check cookies and perform browser redirection # for SSO login. Another use case is to selectively add security HTTP # response headers such as cache control and X-Frame-Options. You can also # perform HTTP to HTTPS redirection. # # Syntax for the \"no-cache-pages\", \"path\" and \"exclusion\" parameters are: # 1. Exact match - complete path # 2. Match \"startsWith\" - use a single \"*\" as the suffix # 3. Match \"endsWith\" - use a single \"*\" as the prefix # # If filter is configured, the path and service parameters are mandatory # and the exclusion parameter is optional. # # In the following example, it will intercept the home page, all contents # under \"/assets/\" and any files with extensions \".html\" and \".js\". # It will ignore all CSS files. # static-content: no-cache-pages: [\"/\", \"/index.html\"] filter: path: [\"/\", \"/assets/*\", \"*.html\", \"*.js\"] exclusion: [\"*.css\"] service: \"http.request.filter\" A sample request filter function is available in the platform-core project like this: @PreLoad(route=\"http.request.filter\", instances=100) public class GetRequestFilter implements TypedLambdaFunction { @Override public EventEnvelope handleEvent(Map headers, AsyncHttpRequest input, int instance) { return new EventEnvelope().setHeader(\"x-filter\", \"demo\"); } } In the above http.request.filter, it adds a HTTP response header \"X-Filter\" for the unit test to validate. If you set status code in the return EventEnvelope to 302 and add a header \"Location\", the system will redirect the browser to the given URL in the location header. Please be careful to avoid HTTP redirection loop. Similarly, you can throw exception and the HTTP request will be rejected with the given status code and error message accordingly. Chapter-2 Home Chapter-4 Function Execution Strategies Table of Contents Event Orchestration","title":"Chapter-3"},{"location":"guides/CHAPTER-3/#rest-automation","text":"The platform-core foundation library contains a built-in non-blocking HTTP server that you can use to create REST endpoints. Behind the curtain, it is using the vertx web client and server libraries. The REST automation system is not a code generator. The REST endpoints in the rest.yaml file are handled by the system directly - \"Config is the code\". We will use the \"rest.yaml\" sample configuration file in the \"lambda-example\" project to elaborate the configuration approach. The rest.yaml configuration has three sections: REST endpoint definition CORS header processing HTTP header transformation","title":"REST Automation"},{"location":"guides/CHAPTER-3/#turn-on-the-rest-automation-engine","text":"REST automation is optional. To turn on REST automation, add or update the following parameters in the application.properties file (or application.yml if you like). rest.server.port=8085 rest.automation=true yaml.rest.automation=classpath:/rest.yaml When rest.automation=true , you can configure the server port using rest.server.port or server.port . REST automation can co-exist with Spring Boot. Please use rest.server.port for REST automation and server.port for Spring Boot. The yaml.rest.automation tells the system the location of the rest.yaml configuration file. You can configure more than one location and the system will search them sequentially. The following example tells the system to load rest.yaml from \"/tmp/config/rest.yaml\". If the file is not available, it will use the rest.yaml in the project's resources folder. yaml.rest.automation=file:/tmp/config/rest.yaml, classpath:/rest.yaml","title":"Turn on the REST automation engine"},{"location":"guides/CHAPTER-3/#defining-a-rest-endpoint","text":"The \"rest\" section of the rest.yaml configuration file may contain one or more REST endpoints. A REST endpoint may look like this: - service: [\"hello.world\"] methods: ['GET', 'PUT', 'POST', 'HEAD', 'PATCH', 'DELETE'] url: \"/api/hello/world\" timeout: 10s cors: cors_1 headers: header_1 threshold: 30000 tracing: true In this example, the URL for the REST endpoint is \"/api/hello/world\" and it accepts a list of HTTP methods. When an HTTP request is sent to the URL, the HTTP event will be sent to the function declared with service route name \"hello.world\". The input event will be the \"AsyncHttpRequest\" object. Since the \"hello.world\" function is written as an inline LambdaFunction in the lambda-example application, the AsyncHttpRequest is converted to a HashMap. To process the input as an AsyncHttpRequest object, the function must be written as a regular class. See the \"services\" folder of the lambda-example for additional examples. The \"timeout\" value is the maximum time that REST endpoint will wait for a response from your function. If there is no response within the specified time interval, the user will receive an HTTP-408 timeout exception. The \"authentication\" tag is optional. If configured, the route name given in the authentication tag will be used. The input event will be delivered to a function with the authentication route name. In this example, it is \"v1.api.auth\". Your custom authentication function may look like this: @PreLoad(route = \"v1.api.auth\", instances = 10) public class SimpleAuthentication implements TypedLambdaFunction { @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { // Your authentication logic here. The return value should be true or false. return result; } } Your authentication function can return a boolean value to indicate if the request should be accepted or rejected. If true, the system will send the HTTP request to the service. In this example, it is the \"hello.world\" function. If false, the user will receive an \"HTTP-401 Unauthorized\" exception. Optionally, you can use the authentication function to return some session information after authentication. For example, your authentication can forward the \"Authorization\" header of the incoming HTTP request to your organization's OAuth 2.0 Identity Provider for authentication. To return session information to the next function, the authentication function can return an EventEnvelope. It can set the session information as key-values in the response event headers. In the lambda-example application, there is a demo authentication function in the AuthDemo class with the \"v1.api.auth\" route name. To demonstrate passing session information, the AuthDemo class set the header \"user=demo\" in the result EventEnvelope. You can test this by visiting http://127.0.0.1:8085/api/hello/generic/1 to invoke the \"hello.generic\" function. The console will print: DistributedTrace:55 - trace={path=GET /api/hello/generic/1, service=v1.api.auth, success=true, origin=20230326f84dd5f298b64be4901119ce8b6c18be, exec_time=0.056, start=2023-03-26T20:08:01.702Z, from=http.request, id=aa983244cef7455cbada03c9c2132453, round_trip=1.347, status=200} HelloGeneric:56 - Got session information {user=demo} DistributedTrace:55 - trace={path=GET /api/hello/generic/1, service=hello.generic, success=true, origin=20230326f84dd5f298b64be4901119ce8b6c18be, start=2023-03-26T20:08:01.704Z, exec_time=0.506, from=v1.api.auth, id=aa983244cef7455cbada03c9c2132453, status=200} DistributedTrace:55 - trace={path=GET /api/hello/generic/1, service=async.http.response, success=true, origin=20230326f84dd5f298b64be4901119ce8b6c18be, start=2023-03-26T20:08:01.705Z, exec_time=0.431, from=hello.generic, id=aa983244cef7455cbada03c9c2132453, status=200} This illustrates that the HTTP request has been processed by the \"v1.api.auth\" function. The \"hello.generic\" function is wired to the \"/api/hello/generic/{id}\" endpoint as follows: - service: \"hello.generic\" methods: ['GET'] url: \"/api/hello/generic/{id}\" # Turn on authentication pointing to the \"v1.api.auth\" function authentication: \"v1.api.auth\" timeout: 20s cors: cors_1 headers: header_1 tracing: true The tracing tag tells the system to turn on \"distributed tracing\". In the console log shown above, you see three lines of log from \"distributed trace\" showing that the HTTP request is processed by \"v1.api.auth\" and \"hello.generic\" before returning result to the browser using the \"async.http.response\" function. Note: the \"async.http.response\" is a built-in function to send the HTTP response to the browser. The optional cors and headers tags point to the specific CORS and HEADERS sections respectively.","title":"Defining a REST endpoint"},{"location":"guides/CHAPTER-3/#cors-section","text":"For ease of development, you can define CORS headers using the CORS section like this. This is a convenient feature for development. For cloud native production system, it is most likely that CORS processing is done at the API gateway level. You can define different sets of CORS headers using different IDs. cors: - id: cors_1 options: - \"Access-Control-Allow-Origin: ${api.origin:*}\" - \"Access-Control-Allow-Methods: GET, DELETE, PUT, POST, PATCH, OPTIONS\" - \"Access-Control-Allow-Headers: Origin, Authorization, X-Session-Id, X-Correlation-Id, Accept, Content-Type, X-Requested-With\" - \"Access-Control-Max-Age: 86400\" headers: - \"Access-Control-Allow-Origin: ${api.origin:*}\" - \"Access-Control-Allow-Methods: GET, DELETE, PUT, POST, PATCH, OPTIONS\" - \"Access-Control-Allow-Headers: Origin, Authorization, X-Session-Id, X-Correlation-Id, Accept, Content-Type, X-Requested-With\" - \"Access-Control-Allow-Credentials: true\"","title":"CORS section"},{"location":"guides/CHAPTER-3/#headers-section","text":"The HEADERS section is used to do some simple transformation for HTTP request and response headers. You can add, keep or drop headers for HTTP request and response. Sample HEADERS section is shown below. headers: - id: header_1 request: # # headers to be inserted # add: [\"hello-world: nice\"] # # keep and drop are mutually exclusive where keep has precedent over drop # i.e. when keep is not empty, it will drop all headers except those to be kept # when keep is empty and drop is not, it will drop only the headers in the drop list # e.g. # keep: ['x-session-id', 'user-agent'] # drop: ['Upgrade-Insecure-Requests', 'cache-control', 'accept-encoding', 'connection'] # drop: ['Upgrade-Insecure-Requests', 'cache-control', 'accept-encoding', 'connection'] response: # # the system can filter the response headers set by a target service, # but it cannot remove any response headers set by the underlying servlet container. # However, you may override non-essential headers using the \"add\" directive. # i.e. don't touch essential headers such as content-length. # # keep: ['only_this_header_and_drop_all'] # drop: ['drop_only_these_headers', 'another_drop_header'] # # add: [\"server: mercury\"] # # You may want to add cache-control to disable browser and CDN caching. # add: [\"Cache-Control: no-cache, no-store\", \"Pragma: no-cache\", # \"Expires: Thu, 01 Jan 1970 00:00:00 GMT\"] # add: - \"Strict-Transport-Security: max-age=31536000\" - \"Cache-Control: no-cache, no-store\" - \"Pragma: no-cache\" - \"Expires: Thu, 01 Jan 1970 00:00:00 GMT\"","title":"HEADERS section"},{"location":"guides/CHAPTER-3/#static-content","text":"Static content (HTML/CSS/JS bundle), if any, can be placed in the \"resources/public\" folder in your application project root. It is because the default value for the \"static.html.folder\" parameter in the application configuration is \"classpath:/resources/public\". If you want to place your static content elsewhere, you may adjust this parameter. You may point it to the local file system such as \"file:/tmp/html\". For security reason, you may add the following configuration in the rest.yaml. The following example is shown in the unit test section of the platform-core library module. # # Optional static content handling for HTML/CSS/JS bundle # ------------------------------------------------------- # # no-cache-pages - tells the browser not to cache some specific pages # # The \"filter\" section is a programmatic way to protect certain static content. # # The filter can be used to inspect HTTP path, headers and query parameters. # The typical use case is to check cookies and perform browser redirection # for SSO login. Another use case is to selectively add security HTTP # response headers such as cache control and X-Frame-Options. You can also # perform HTTP to HTTPS redirection. # # Syntax for the \"no-cache-pages\", \"path\" and \"exclusion\" parameters are: # 1. Exact match - complete path # 2. Match \"startsWith\" - use a single \"*\" as the suffix # 3. Match \"endsWith\" - use a single \"*\" as the prefix # # If filter is configured, the path and service parameters are mandatory # and the exclusion parameter is optional. # # In the following example, it will intercept the home page, all contents # under \"/assets/\" and any files with extensions \".html\" and \".js\". # It will ignore all CSS files. # static-content: no-cache-pages: [\"/\", \"/index.html\"] filter: path: [\"/\", \"/assets/*\", \"*.html\", \"*.js\"] exclusion: [\"*.css\"] service: \"http.request.filter\" A sample request filter function is available in the platform-core project like this: @PreLoad(route=\"http.request.filter\", instances=100) public class GetRequestFilter implements TypedLambdaFunction { @Override public EventEnvelope handleEvent(Map headers, AsyncHttpRequest input, int instance) { return new EventEnvelope().setHeader(\"x-filter\", \"demo\"); } } In the above http.request.filter, it adds a HTTP response header \"X-Filter\" for the unit test to validate. If you set status code in the return EventEnvelope to 302 and add a header \"Location\", the system will redirect the browser to the given URL in the location header. Please be careful to avoid HTTP redirection loop. Similarly, you can throw exception and the HTTP request will be rejected with the given status code and error message accordingly. Chapter-2 Home Chapter-4 Function Execution Strategies Table of Contents Event Orchestration","title":"Static content"},{"location":"guides/CHAPTER-4/","text":"Event Orchestration In traditional programming, we can write modular software components and wire them together as a single application. There are many ways to do that. You can rely on a \"dependency injection\" framework. In many cases, you would need to write orchestration logic to coordinate how the various components talk to each other to process a transaction. In a composable application, you write modular functions using the first principle of \"input-process-output\". Functions communicate with each other using events and each function has a \"handleEvent\" method to process \"input\" and return result as \"output\". Writing software component in the first principle makes Test Driven Development (TDD) straight forward. You can write mock function and unit tests before you put in actual business logic. Mocking an event-driven function in a composable application is as simple as overriding the function's route name with a mock function. Register a function with the in-memory event system There are two ways to register a function: Programmatic registration Declarative registration In programmatic registration, you can register a function like this: Platform platform = Platform.getInstance(); platform.registerPrivate(\"my.function\", new MyFunction(), 10); In the above example, You obtain a singleton instance of the Platform API class and use it to register a private function MyFunction with a route name \"my.function\". In declarative approach, you use the PreLoad annotation to register a class with an event handler. Your function should implement the LambdaFunction, TypedLambdaFunction or KotlinLambdaFunction. While LambdaFunction is untyped, the event system can transport PoJo and your function should test the object type and cast it to the correct PoJo. TypedLambdaFunction and KotlinLambdaFunction are typed, and you must declare the input and output classes according to the input/output API contract of your function. For example, the SimpleDemoEndpoint has the \"PreLoad\" annotation to declare the route name and number of worker instances. By default, LambdaFunction and TypedLambdaFunction are executed as \"coroutine\" for the worker instances. To tell the system to run it using kernel threads, you can add the KernelThreadRunner annotation. @KernelThreadRunner @PreLoad(route = \"hello.simple\", instances = 10) public class SimpleDemoEndpoint implements TypedLambdaFunction { @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { // business logic here } } Once a function is created using the declarative method, you can override it with a mock function by using the programmatic registration method in a unit test. Private vs public functions When you use the programmatic registration approach, you can use the \"register\" or the \"registerPrivate\" method to set the function as \"public\" or \"private\" respectively. For declarative approach, the PreLoad annotation contains a parameter to define the visibility of the function. // or register it as \"public\" platform.register(\"my.function\", new MyFunction(), 10); // register a function as \"private\" platform.registerPrivate(\"my.function\", new MyFunction(), 10); A private function is visible by other functions in the same application memory space. A public function is accessible by other function from another application instance using service mesh or \"Event over HTTP\" method. We will discuss inter-container communication in Chapter-7 and Chapter-8 . Post Office API To send an asynchronous event or an event RPC call from one function to another, you can use the PostOffice APIs. In your function, you can obtain an instance of the PostOffice like this: @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { PostOffice po = new PostOffice(headers, instance); // e.g. po.send and po.asyncRequest for sending asynchronous event and making RPC call } The PostOffice API detects if tracing is enabled in the incoming request. If yes, it will propagate tracing information to \"downstream\" functions. Event patterns RPC \u201cRequest-response\u201d, best for interactivity Asynchronous e.g. Drop-n-forget Callback e.g. Progressive rendering Pipeline e.g. Work-flow application Streaming e.g. File transfer Request-response (RPC) In enterprise application, RPC is the most common pattern in making call from one function to another. The \"calling\" function makes a request and waits for the response from the \"called\" function. In Mercury version 3, there are 2 types of RPC calls - \"asynchronous\" and \"sequential non-blocking\". Asynchronous RPC You can use the asyncRequest method to make an asynchronous RPC call. Asynchronous means that the response will be delivered to the onSuccess or onFailure callback method. Note that normal response and exception are sent to the onSuccess method and timeout exception to the onFailure method. If you set \"timeoutException\" to false, the timeout exception will be delivered to the onSuccess callback and the onFailure callback will be ignored. Future asyncRequest(final EventEnvelope event, long timeout) throws IOException; Future asyncRequest(final EventEnvelope event, long timeout, boolean timeoutException) throws IOException; // example EventEnvelope request = new EventEnvelope().setTo(SERVICE).setBody(TEXT); Future response = po.asyncRequest(request, 2000); response.onSuccess(result -> { // handle the response event }).onFailure(ex -> { // handle timeout exception }); The timeout value is measured in milliseconds. Asynchronous fork-n-join A special version of RPC is the fork-n-join API. This allows you to make concurrent requests to multiple functions. The system will consolidate all responses and return them as a list of events. Normal responses and user defined exceptions are sent to the onSuccess method and timeout exception to the onFailure method. Your function will receive all responses or a timeout exception. If you set \"timeoutException\" to false, partial results will be delivered to the onSuccess method when one or more services fail to respond on-time. The onFailure method is not required. Future> asyncRequest(final List event, long timeout) throws IOException; Future> asyncRequest(final List event, long timeout, boolean timeoutException) throws IOException; // example List requests = new ArrayList<>(); requests.add(new EventEnvelope().setTo(SERVICE1).setBody(TEXT1)); requests.add(new EventEnvelope().setTo(SERVICE2).setBody(TEXT2)); Future> responses = po.asyncRequest(requests, 2000); responses.onSuccess(events -> { // handle the response events }).onFailure(ex -> { // handle timeout exception }); Asynchronous programming technique When your function is a service by itself, asynchronous RPC and fork-n-join require different programming approaches. There are two ways to do that: 1. Your function returns an immediate result and waits for the response(s) to the onSuccess or onFailure callback 2. Your function is implemented as an \"EventInterceptor\" For the first approach, your function can return an immediate result telling the caller that your function would need time to process the request. This works when the caller can be reached by a callback. For the second approach, your function is annotated with the keyword EventInterceptor . It can immediately return a \"null\" response that will be ignored by the event system. Your function can inspect the \"replyTo\" address and correlation ID in the incoming event and include them in a future response to the caller. Sequential non-blocking RPC and fork-n-join To simplify coding, you can implement a \"suspend function\" using the KotlinLambdaFunction interface. The following code segment illustrates the creation of the \"hello.world\" function that makes a non-blocking RPC call to \"another.service\". @PreLoad(route=\"hello.world\", instances=10) class FileUploadDemo: KotlinLambdaFunction { override suspend fun handleEvent(headers: Map, input: AsyncHttpRequest, instance: Int): Any { val fastRPC = FastRPC(headers) // your business logic here... val req = EventEnvelope().setTo(\"another.service\").setBody(myPoJo) return fastRPC.awaitRequest(req, 5000) } } The API method signature for non-blocking RPC and fork-n-join are as follows: @Throws(IOException::class) suspend fun awaitRequest(request: EventEnvelope, timeout: Long): EventEnvelope @Throws(IOException::class) suspend fun awaitRequest(requests: List, timeout: Long): List Asynchronous drop-n-forget To make an asynchronous call from one function to another, use the send method. void send(String to, Kv... parameters) throws IOException; void send(String to, Object body) throws IOException; void send(String to, Object body, Kv... parameters) throws IOException; void send(final EventEnvelope event) throws IOException; Kv is a key-value pair for holding one parameter. Asynchronous event calls are handled in the background so that your function can continue processing. For example, sending a notification message to a user. Callback You can declare another function as a \"callback\". When you send a request to another function, you can set the \"replyTo\" address in the request event. When a response is received, your callback function will be invoked to handle the response event. EventEnvelope req = new EventEnvelope().setTo(\"some.service\") .setBody(myPoJo).setReplyTo(\"my.callback\"); po.send(req); In the above example, you have a callback function with route name \"my.callback\". You send the request event with a MyPoJo object as payload to the \"some.service\" function. When a response is received, the \"my.callback\" function will get the response as input. Pipeline Pipeline is a linked list of event calls. There are many ways to do pipeline. One way is to keep the pipeline plan in an event's header and pass the event across multiple functions where you can set the \"replyTo\" address from the pipeline plan. You should handle exception cases when a pipeline breaks in the middle of a transaction. An example of the pipeline header key-value may look like this: pipeline=service.1, service.2, service.3, service.4, service.5 In the above example, when the pipeline event is received by a function, the function can check its position in the pipeline by comparing its own route name with the pipeline plan. PostOffice po = new PostOffice(headers, instance); // some business logic here... String myRoute = po.getRoute(); Suppose myRoute is \"service.2\", the function can send the response event to \"service.3\". When \"service.3\" receives the event, it can send its response event to the next one. i.e. \"service.4\". When the event reaches the last service (\"service.5\"), the processing will complete. Streaming If you set a function as singleton (i.e. one worker instance), it will receive event in an orderly fashion. This way you can \"stream\" events to the function, and it will process the events one by one. Another means to do streaming is to create an \"ObjectStreamIO\" event stream like this: ObjectStreamIO stream = new ObjectStreamIO(60); ObjectStreamWriter out = new ObjectStreamWriter(stream.getOutputStreamId()); out.write(messageOne); out.write(messageTwo); out.close(); String streamId = stream.getInputStreamId(); // pass the streamId to another function In the code segment above, your function creates an object event stream and writes 2 messages into the stream It obtains the streamId of the event stream and sends it to another function. The other function can read the data blocks orderly. You must declare \"end of stream\" by closing the output stream. If you do not close an output stream, it remains open and idle. If a function is trying to read an input stream using the stream ID and the next data block is not available, it will time out. A stream will be automatically closed when the idle inactivity timer is reached. In the above example, ObjectStreamIO(60) means an idle inactivity timer of 60 seconds. IMPORTANT: To improve the non-blocking design of your function, you can implement your function as a KotlinLambdaFunction. If you need to send many blocks of data continuously in a \"while\" loop, you should add the \"yield()\" statement before it writes a block of data to the output stream. This way, a long-running function will be non-blocking. There are two ways to read an input event stream - asynchronous or sequential non-blocking. AsyncObjectStreamReader To read events from a stream, you can create an instance of the AsyncObjectStreamReader like this: AsyncObjectStreamReader in = new AsyncObjectStreamReader(stream.getInputStreamId(), 8000); Future block = in.get(); block.onSuccess(b -> { if (b != null) { // process the data block } else { // end of stream. Do additional processing. in.close(); } }); The above illustrates reading the first block of data. The function would need to iteratively read the stream until end of stream (i.e. when the stream returns null). As a result, asynchronous application code for stream processing is more challenging to write. Sequential non-blocking method The industry trend is to use sequential non-blocking method instead of \"asynchronous callback\" because your code will be much easier to read. You can use the awaitRequest method to read the next block of data from an event stream. An example for reading a stream is shown in the FileUploadDemo kotlin class in the lambda-example project. It is using a simple \"while\" loop to read the stream. When the function fetches the next block of data using the awaitRequest method, the function is suspended until the next data block or \"end of stream\" signal is received. It may look like this: val po = PostOffice(headers, instance) val fastRPC = FastRPC(headers) val req = EventEnvelope().setTo(streamId).setHeader(TYPE, READ) while (true) { val event = fastRPC.awaitRequest(req, 5000) if (event.status == 408) { // handle input stream timeout break } if (\"eof\" == event.headers[\"type\"]) { po.send(streamId, Kv(\"type\", \"close\")) break } if (\"data\" == event.headers[\"type\"]) { val block = event.body if (block is ByteArray) { // handle the data block from the input stream } } } Since the code style is \"sequential non-blocking\", using a \"while\" loop does not block the \"event loop\" provided that you are using an \"await\" API inside the while-loop. In this fashion, the intent of the code is clear. Sequential non-blocking method offers high throughput because it does not consume CPU resources while the function is waiting for a response from another function. We recommend sequential non-blocking style for more sophisticated event streaming logic. Note: \"await\" methods are only supported in KotlinLambdaFunction which is a suspend function. When Java 19 virtual thread feature becomes officially available, we will enhance the function execution strategies accordingly. Orchestration layer Once you have implemented modular functions in a self-contained manner, the best practice is to write one or more functions to do \"event orchestration\". Think of the orchestration function as a music conductor who guides the whole team to perform. For event orchestration, your function can be the \"conductor\" that sends events to the individual functions so that they operate together as a single application. To simplify design, the best practice is to apply event orchestration for each transaction or use case. The event orchestration function also serves as a living documentation about how your application works. It makes your code more readable. Event Script To automate event orchestration, there is an enterprise add-on module called \"Event Script\". This is the idea of \"config over code\" or \"declarative programming\". The primary purpose of \"Event Script\" is to reduce coding effort so that the team can focus in improving application design and code quality. To use event script, please upgrade to Mercury v4. Mercury v4: https://github.com/Accenture/mercury-composable Documentation: https://accenture.github.io/mercury-composable/ In the next chapter, we will discuss the build, test and deploy process. Chapter-3 Home Chapter-5 REST Automation Table of Contents Build, Test and Deploy","title":"Chapter-4"},{"location":"guides/CHAPTER-4/#event-orchestration","text":"In traditional programming, we can write modular software components and wire them together as a single application. There are many ways to do that. You can rely on a \"dependency injection\" framework. In many cases, you would need to write orchestration logic to coordinate how the various components talk to each other to process a transaction. In a composable application, you write modular functions using the first principle of \"input-process-output\". Functions communicate with each other using events and each function has a \"handleEvent\" method to process \"input\" and return result as \"output\". Writing software component in the first principle makes Test Driven Development (TDD) straight forward. You can write mock function and unit tests before you put in actual business logic. Mocking an event-driven function in a composable application is as simple as overriding the function's route name with a mock function.","title":"Event Orchestration"},{"location":"guides/CHAPTER-4/#register-a-function-with-the-in-memory-event-system","text":"There are two ways to register a function: Programmatic registration Declarative registration In programmatic registration, you can register a function like this: Platform platform = Platform.getInstance(); platform.registerPrivate(\"my.function\", new MyFunction(), 10); In the above example, You obtain a singleton instance of the Platform API class and use it to register a private function MyFunction with a route name \"my.function\". In declarative approach, you use the PreLoad annotation to register a class with an event handler. Your function should implement the LambdaFunction, TypedLambdaFunction or KotlinLambdaFunction. While LambdaFunction is untyped, the event system can transport PoJo and your function should test the object type and cast it to the correct PoJo. TypedLambdaFunction and KotlinLambdaFunction are typed, and you must declare the input and output classes according to the input/output API contract of your function. For example, the SimpleDemoEndpoint has the \"PreLoad\" annotation to declare the route name and number of worker instances. By default, LambdaFunction and TypedLambdaFunction are executed as \"coroutine\" for the worker instances. To tell the system to run it using kernel threads, you can add the KernelThreadRunner annotation. @KernelThreadRunner @PreLoad(route = \"hello.simple\", instances = 10) public class SimpleDemoEndpoint implements TypedLambdaFunction { @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { // business logic here } } Once a function is created using the declarative method, you can override it with a mock function by using the programmatic registration method in a unit test.","title":"Register a function with the in-memory event system"},{"location":"guides/CHAPTER-4/#private-vs-public-functions","text":"When you use the programmatic registration approach, you can use the \"register\" or the \"registerPrivate\" method to set the function as \"public\" or \"private\" respectively. For declarative approach, the PreLoad annotation contains a parameter to define the visibility of the function. // or register it as \"public\" platform.register(\"my.function\", new MyFunction(), 10); // register a function as \"private\" platform.registerPrivate(\"my.function\", new MyFunction(), 10); A private function is visible by other functions in the same application memory space. A public function is accessible by other function from another application instance using service mesh or \"Event over HTTP\" method. We will discuss inter-container communication in Chapter-7 and Chapter-8 .","title":"Private vs public functions"},{"location":"guides/CHAPTER-4/#post-office-api","text":"To send an asynchronous event or an event RPC call from one function to another, you can use the PostOffice APIs. In your function, you can obtain an instance of the PostOffice like this: @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { PostOffice po = new PostOffice(headers, instance); // e.g. po.send and po.asyncRequest for sending asynchronous event and making RPC call } The PostOffice API detects if tracing is enabled in the incoming request. If yes, it will propagate tracing information to \"downstream\" functions.","title":"Post Office API"},{"location":"guides/CHAPTER-4/#event-patterns","text":"RPC \u201cRequest-response\u201d, best for interactivity Asynchronous e.g. Drop-n-forget Callback e.g. Progressive rendering Pipeline e.g. Work-flow application Streaming e.g. File transfer","title":"Event patterns"},{"location":"guides/CHAPTER-4/#request-response-rpc","text":"In enterprise application, RPC is the most common pattern in making call from one function to another. The \"calling\" function makes a request and waits for the response from the \"called\" function. In Mercury version 3, there are 2 types of RPC calls - \"asynchronous\" and \"sequential non-blocking\".","title":"Request-response (RPC)"},{"location":"guides/CHAPTER-4/#asynchronous-rpc","text":"You can use the asyncRequest method to make an asynchronous RPC call. Asynchronous means that the response will be delivered to the onSuccess or onFailure callback method. Note that normal response and exception are sent to the onSuccess method and timeout exception to the onFailure method. If you set \"timeoutException\" to false, the timeout exception will be delivered to the onSuccess callback and the onFailure callback will be ignored. Future asyncRequest(final EventEnvelope event, long timeout) throws IOException; Future asyncRequest(final EventEnvelope event, long timeout, boolean timeoutException) throws IOException; // example EventEnvelope request = new EventEnvelope().setTo(SERVICE).setBody(TEXT); Future response = po.asyncRequest(request, 2000); response.onSuccess(result -> { // handle the response event }).onFailure(ex -> { // handle timeout exception }); The timeout value is measured in milliseconds.","title":"Asynchronous RPC"},{"location":"guides/CHAPTER-4/#asynchronous-fork-n-join","text":"A special version of RPC is the fork-n-join API. This allows you to make concurrent requests to multiple functions. The system will consolidate all responses and return them as a list of events. Normal responses and user defined exceptions are sent to the onSuccess method and timeout exception to the onFailure method. Your function will receive all responses or a timeout exception. If you set \"timeoutException\" to false, partial results will be delivered to the onSuccess method when one or more services fail to respond on-time. The onFailure method is not required. Future> asyncRequest(final List event, long timeout) throws IOException; Future> asyncRequest(final List event, long timeout, boolean timeoutException) throws IOException; // example List requests = new ArrayList<>(); requests.add(new EventEnvelope().setTo(SERVICE1).setBody(TEXT1)); requests.add(new EventEnvelope().setTo(SERVICE2).setBody(TEXT2)); Future> responses = po.asyncRequest(requests, 2000); responses.onSuccess(events -> { // handle the response events }).onFailure(ex -> { // handle timeout exception });","title":"Asynchronous fork-n-join"},{"location":"guides/CHAPTER-4/#asynchronous-programming-technique","text":"When your function is a service by itself, asynchronous RPC and fork-n-join require different programming approaches. There are two ways to do that: 1. Your function returns an immediate result and waits for the response(s) to the onSuccess or onFailure callback 2. Your function is implemented as an \"EventInterceptor\" For the first approach, your function can return an immediate result telling the caller that your function would need time to process the request. This works when the caller can be reached by a callback. For the second approach, your function is annotated with the keyword EventInterceptor . It can immediately return a \"null\" response that will be ignored by the event system. Your function can inspect the \"replyTo\" address and correlation ID in the incoming event and include them in a future response to the caller.","title":"Asynchronous programming technique"},{"location":"guides/CHAPTER-4/#sequential-non-blocking-rpc-and-fork-n-join","text":"To simplify coding, you can implement a \"suspend function\" using the KotlinLambdaFunction interface. The following code segment illustrates the creation of the \"hello.world\" function that makes a non-blocking RPC call to \"another.service\". @PreLoad(route=\"hello.world\", instances=10) class FileUploadDemo: KotlinLambdaFunction { override suspend fun handleEvent(headers: Map, input: AsyncHttpRequest, instance: Int): Any { val fastRPC = FastRPC(headers) // your business logic here... val req = EventEnvelope().setTo(\"another.service\").setBody(myPoJo) return fastRPC.awaitRequest(req, 5000) } } The API method signature for non-blocking RPC and fork-n-join are as follows: @Throws(IOException::class) suspend fun awaitRequest(request: EventEnvelope, timeout: Long): EventEnvelope @Throws(IOException::class) suspend fun awaitRequest(requests: List, timeout: Long): List","title":"Sequential non-blocking RPC and fork-n-join"},{"location":"guides/CHAPTER-4/#asynchronous-drop-n-forget","text":"To make an asynchronous call from one function to another, use the send method. void send(String to, Kv... parameters) throws IOException; void send(String to, Object body) throws IOException; void send(String to, Object body, Kv... parameters) throws IOException; void send(final EventEnvelope event) throws IOException; Kv is a key-value pair for holding one parameter. Asynchronous event calls are handled in the background so that your function can continue processing. For example, sending a notification message to a user.","title":"Asynchronous drop-n-forget"},{"location":"guides/CHAPTER-4/#callback","text":"You can declare another function as a \"callback\". When you send a request to another function, you can set the \"replyTo\" address in the request event. When a response is received, your callback function will be invoked to handle the response event. EventEnvelope req = new EventEnvelope().setTo(\"some.service\") .setBody(myPoJo).setReplyTo(\"my.callback\"); po.send(req); In the above example, you have a callback function with route name \"my.callback\". You send the request event with a MyPoJo object as payload to the \"some.service\" function. When a response is received, the \"my.callback\" function will get the response as input.","title":"Callback"},{"location":"guides/CHAPTER-4/#pipeline","text":"Pipeline is a linked list of event calls. There are many ways to do pipeline. One way is to keep the pipeline plan in an event's header and pass the event across multiple functions where you can set the \"replyTo\" address from the pipeline plan. You should handle exception cases when a pipeline breaks in the middle of a transaction. An example of the pipeline header key-value may look like this: pipeline=service.1, service.2, service.3, service.4, service.5 In the above example, when the pipeline event is received by a function, the function can check its position in the pipeline by comparing its own route name with the pipeline plan. PostOffice po = new PostOffice(headers, instance); // some business logic here... String myRoute = po.getRoute(); Suppose myRoute is \"service.2\", the function can send the response event to \"service.3\". When \"service.3\" receives the event, it can send its response event to the next one. i.e. \"service.4\". When the event reaches the last service (\"service.5\"), the processing will complete.","title":"Pipeline"},{"location":"guides/CHAPTER-4/#streaming","text":"If you set a function as singleton (i.e. one worker instance), it will receive event in an orderly fashion. This way you can \"stream\" events to the function, and it will process the events one by one. Another means to do streaming is to create an \"ObjectStreamIO\" event stream like this: ObjectStreamIO stream = new ObjectStreamIO(60); ObjectStreamWriter out = new ObjectStreamWriter(stream.getOutputStreamId()); out.write(messageOne); out.write(messageTwo); out.close(); String streamId = stream.getInputStreamId(); // pass the streamId to another function In the code segment above, your function creates an object event stream and writes 2 messages into the stream It obtains the streamId of the event stream and sends it to another function. The other function can read the data blocks orderly. You must declare \"end of stream\" by closing the output stream. If you do not close an output stream, it remains open and idle. If a function is trying to read an input stream using the stream ID and the next data block is not available, it will time out. A stream will be automatically closed when the idle inactivity timer is reached. In the above example, ObjectStreamIO(60) means an idle inactivity timer of 60 seconds. IMPORTANT: To improve the non-blocking design of your function, you can implement your function as a KotlinLambdaFunction. If you need to send many blocks of data continuously in a \"while\" loop, you should add the \"yield()\" statement before it writes a block of data to the output stream. This way, a long-running function will be non-blocking. There are two ways to read an input event stream - asynchronous or sequential non-blocking.","title":"Streaming"},{"location":"guides/CHAPTER-4/#asyncobjectstreamreader","text":"To read events from a stream, you can create an instance of the AsyncObjectStreamReader like this: AsyncObjectStreamReader in = new AsyncObjectStreamReader(stream.getInputStreamId(), 8000); Future block = in.get(); block.onSuccess(b -> { if (b != null) { // process the data block } else { // end of stream. Do additional processing. in.close(); } }); The above illustrates reading the first block of data. The function would need to iteratively read the stream until end of stream (i.e. when the stream returns null). As a result, asynchronous application code for stream processing is more challenging to write.","title":"AsyncObjectStreamReader"},{"location":"guides/CHAPTER-4/#sequential-non-blocking-method","text":"The industry trend is to use sequential non-blocking method instead of \"asynchronous callback\" because your code will be much easier to read. You can use the awaitRequest method to read the next block of data from an event stream. An example for reading a stream is shown in the FileUploadDemo kotlin class in the lambda-example project. It is using a simple \"while\" loop to read the stream. When the function fetches the next block of data using the awaitRequest method, the function is suspended until the next data block or \"end of stream\" signal is received. It may look like this: val po = PostOffice(headers, instance) val fastRPC = FastRPC(headers) val req = EventEnvelope().setTo(streamId).setHeader(TYPE, READ) while (true) { val event = fastRPC.awaitRequest(req, 5000) if (event.status == 408) { // handle input stream timeout break } if (\"eof\" == event.headers[\"type\"]) { po.send(streamId, Kv(\"type\", \"close\")) break } if (\"data\" == event.headers[\"type\"]) { val block = event.body if (block is ByteArray) { // handle the data block from the input stream } } } Since the code style is \"sequential non-blocking\", using a \"while\" loop does not block the \"event loop\" provided that you are using an \"await\" API inside the while-loop. In this fashion, the intent of the code is clear. Sequential non-blocking method offers high throughput because it does not consume CPU resources while the function is waiting for a response from another function. We recommend sequential non-blocking style for more sophisticated event streaming logic. Note: \"await\" methods are only supported in KotlinLambdaFunction which is a suspend function. When Java 19 virtual thread feature becomes officially available, we will enhance the function execution strategies accordingly.","title":"Sequential non-blocking method"},{"location":"guides/CHAPTER-4/#orchestration-layer","text":"Once you have implemented modular functions in a self-contained manner, the best practice is to write one or more functions to do \"event orchestration\". Think of the orchestration function as a music conductor who guides the whole team to perform. For event orchestration, your function can be the \"conductor\" that sends events to the individual functions so that they operate together as a single application. To simplify design, the best practice is to apply event orchestration for each transaction or use case. The event orchestration function also serves as a living documentation about how your application works. It makes your code more readable.","title":"Orchestration layer"},{"location":"guides/CHAPTER-4/#event-script","text":"To automate event orchestration, there is an enterprise add-on module called \"Event Script\". This is the idea of \"config over code\" or \"declarative programming\". The primary purpose of \"Event Script\" is to reduce coding effort so that the team can focus in improving application design and code quality. To use event script, please upgrade to Mercury v4. Mercury v4: https://github.com/Accenture/mercury-composable Documentation: https://accenture.github.io/mercury-composable/ In the next chapter, we will discuss the build, test and deploy process. Chapter-3 Home Chapter-5 REST Automation Table of Contents Build, Test and Deploy","title":"Event Script"},{"location":"guides/CHAPTER-5/","text":"Build, Test and Deploy The first step in writing an application is to create an entry point for your application. Main application A minimalist main application template is shown as follows: @MainApplication public class MainApp implements EntryPoint { public static void main(String[] args) { AutoStart.main(args); } @Override public void start(String[] args) { // your startup logic here log.info(\"Started\"); } } Note that MainApplication is mandatory. You must have at least one \"main application\" module. Note: Please adjust the parameter \"web.component.scan\" in application.properties to point to your user application package(s) in your source code project. If your application does not require additional startup logic, you may just print a greeting message. The AutoStart.main() statement in the \"main\" method is used when you want to start your application within the IDE. You can \"right-click\" the main method and select \"run\". You can also build and run the application from command line like this: cd sandbox/mercury/examples/lambda-example mvn clean package java -jar target/lambda-example-3.0.9.jar The lambda-example is a sample application that you can use as a template to write your own code. Please review the pom.xml and the source directory structure. The pom.xml is pre-configured to support Java and Kotlin. In the lambda-example project root, you will find the following directories: src/main/java src/main/kotlin src/test/java Note that kotlin unit test directory is not included because you can test all functions in Java unit tests. Since all functions are connected using the in-memory event bus, you can test any function by sending events from a unit test module in Java. If you are comfortable with the Kotlin language, you may also set up Kotlin unit tests accordingly. There is no harm having both types of unit tests in the same project. Source code documentation Since the source project contains both Java and Kotlin, we have replaced javadoc maven plugin with Jetbrains \"dokka\" documentation engine for both Java and Kotlin. Javadoc is useful if you want to write and publish your own libraries. To generate Java and Kotlin source documentation, please run \"mvn dokka:dokka\". You may \"cd\" to the platform-core project to try the maven dokka command to generate some source documentation. The home page will be available in \"target/dokka/index.html\" Writing your functions Please follow the step-by-step learning guide in Chapter-1 to write your own functions. You can then configure new REST endpoints to use your new functions. In Chapter-1 , we have discussed the three function execution strategies to optimize your application to the full potential of stability, performance and throughput. HTTP forwarding In Chapter-3 , we have presented the configuration syntax for the \"rest.yaml\" REST automation definition file. Please review the sample rest.yaml file in the lambda-example project. You may notice that it has an entry for HTTP forwarding. The following entry in the sample rest.yaml file illustrates an HTTP forwarding endpoint. In HTTP forwarding, you can replace the \"service\" route name with a direct HTTP target host. You can do \"URL rewrite\" to change the URL path to the target endpoint path. In the below example, /api/v1/* will be mapped to /api/* in the target endpoint. - service: \"http://127.0.0.1:${rest.server.port}\" trust_all_cert: true methods: ['GET', 'PUT', 'POST'] url: \"/api/v1/*\" url_rewrite: ['/api/v1', '/api'] timeout: 20 cors: cors_1 headers: header_1 tracing: true Sending HTTP request event to more than one service One feature in REST automation \"rest.yaml\" configuration is that you can configure more than one function in the \"service\" section. In the following example, there are two function route names (\"hello.world\" and \"hello.copy\"). The first one \"hello.world\" is the primary service provider. The second one \"hello.copy\" will receive a copy of the incoming event automatically. This feature allows you to write new version of a function without disruption to current functionality. Once you are happy with the new version of function, you can route the endpoint directly to the new version by updating the \"rest.yaml\" configuration file. - service: [\"hello.world\", \"hello.copy\"] Writing your first unit test Please refer to \"rpcTest\" method in the \"HelloWorldTest\" class in the lambda-example to get started. In unit test, we want to start the main application so that all the functions are ready for tests. First, we write a \"TestBase\" class to use the BeforeClass setup method to start the main application like this: public class TestBase { private static final AtomicInteger seq = new AtomicInteger(0); @BeforeClass public static void setup() { if (seq.incrementAndGet() == 1) { AutoStart.main(new String[0]); } } } The atomic integer \"seq\" is used to ensure the main application entry point is executed only once. Your first unit test may look like this: @SuppressWarnings(\"unchecked\") @Test public void rpcTest() throws IOException, InterruptedException { Utility util = Utility.getInstance(); BlockingQueue bench = new ArrayBlockingQueue<>(1); String name = \"hello\"; String address = \"world\"; String telephone = \"123-456-7890\"; DemoPoJo pojo = new DemoPoJo(name, address, telephone); PostOffice po = new PostOffice(\"unit.test\", \"12345\", \"POST /api/hello/world\"); EventEnvelope request = new EventEnvelope().setTo(\"hello.world\") .setHeader(\"a\", \"b\").setBody(pojo.toMap()); po.asyncRequest(request, 800).onSuccess(bench::offer); EventEnvelope response = bench.poll(10, TimeUnit.SECONDS); assert response != null; Assert.assertEquals(HashMap.class, response.getBody().getClass()); MultiLevelMap map = new MultiLevelMap((Map) response.getBody()); Assert.assertEquals(\"b\", map.getElement(\"headers.a\")); Assert.assertEquals(name, map.getElement(\"body.name\")); Assert.assertEquals(address, map.getElement(\"body.address\")); Assert.assertEquals(telephone, map.getElement(\"body.telephone\")); Assert.assertEquals(util.date2str(pojo.time), map.getElement(\"body.time\")); } Note that the PostOffice instance can be created with tracing information in a Unit Test. The above example tells the system that the sender is \"unit.test\", the trace ID is 12345 and the trace path is \"POST /api/hello/world\". For unit test, we need to convert the asynchronous code into \"synchronous\" execution so that unit test can run sequentially. \"BlockingQueue\" is a good choice for this. The \"hello.world\" is an echo function. The above unit test sends an event containing a key-value {\"a\":\"b\"} and the payload of a HashMap from the DemoPoJo. If the function is designed to handle PoJo, we can send PoJo directly instead of a Map. IMPORTANT: blocking code should only be used for unit tests. DO NOT use blocking code in your application code because it will block the event system and dramatically slow down your application. Convenient utility classes The Utility and MultiLevelMap classes are convenient tools for unit tests. In the above example, we use the Utility class to convert a date object into a UTC timestamp. It is because date object is serialized as a UTC timestamp in an event. The MultiLevelMap supports reading an element using the convenient \"dot and bracket\" format. For example, given a map like this: { \"body\": { \"time\": \"2023-03-27T18:10:34.234Z\", \"hello\": [1, 2, 3] } } Example Command Result 1 map.getElement(\"body.time\") 2023-03-27T18:10:34.234Z 2 map.getElement(\"body.hello[2]\") 3 The second unit test Let's do a unit test for PoJo. In this second unit test, it sends a RPC request to the \"hello.pojo\" function that is designed to return a SamplePoJo object with some mock data. Please refer to \"pojoRpcTest\" method in the \"PoJoTest\" class in the lambda-example for details. The unit test verifies that the \"hello.pojo\" has correctly returned the SamplePoJo object with the pre-defined mock value. @Test public void pojoTest() throws IOException, InterruptedException { Integer ID = 1; String NAME = \"Simple PoJo class\"; String ADDRESS = \"100 World Blvd, Planet Earth\"; BlockingQueue bench = new ArrayBlockingQueue<>(1); PostOffice po = new PostOffice(\"unit.test\", \"20001\", \"GET /api/hello/pojo\"); EventEnvelope request = new EventEnvelope().setTo(\"hello.pojo\").setHeader(\"id\", \"1\"); po.asyncRequest(request, 800).onSuccess(bench::offer); EventEnvelope response = bench.poll(10, TimeUnit.SECONDS); assert response != null; Assert.assertEquals(SamplePoJo.class, response.getBody().getClass()); SamplePoJo pojo = response.getBody(SamplePoJo.class); Assert.assertEquals(ID, pojo.getId()); Assert.assertEquals(NAME, pojo.getName()); Assert.assertEquals(ADDRESS, pojo.getAddress()); } Note that you can do class \"casting\" or use the built-in casting API as shown below: SamplePoJo pojo = (SamplePoJo) response.getBody() SamplePoJo pojo = response.getBody(SamplePoJo.class) The third unit test Testing Kotlin suspend functions is challenging. However, testing suspend function using events is straight forward because of loose coupling. Let's do a unit test for the lambda-example's FileUploadDemo function. Its route name is \"hello.upload\". Please refer to \"uploadTest\" method in the \"SuspendFunctionTest\" class in the lambda-example for details. @SuppressWarnings(\"unchecked\") @Test public void uploadTest() throws IOException, InterruptedException { String FILENAME = \"unit-test-data.txt\"; BlockingQueue bench = new ArrayBlockingQueue<>(1); Utility util = Utility.getInstance(); PostOffice po = PostOffice.getInstance(); int len = 0; ByteArrayOutputStream bytes = new ByteArrayOutputStream(); ObjectStreamIO stream = new ObjectStreamIO(); ObjectStreamWriter out = new ObjectStreamWriter(stream.getOutputStreamId()); for (int i=0; i < 10; i++) { String line = \"hello world \"+i+\"\\n\"; byte[] d = util.getUTF(line); out.write(d); bytes.write(d); len += d.length; } out.close(); // emulate a multi-part file upload AsyncHttpRequest req = new AsyncHttpRequest(); req.setMethod(\"POST\"); req.setUrl(\"/api/upload/demo\"); req.setTargetHost(\"http://127.0.0.1:8080\"); req.setHeader(\"accept\", \"application/json\"); req.setHeader(\"content-type\", \"multipart/form-data\"); req.setContentLength(len); req.setFileName(FILENAME); req.setStreamRoute(stream.getInputStreamId()); // send the HTTP request event to the \"hello.upload\" function EventEnvelope request = new EventEnvelope().setTo(\"hello.upload\").setBody(req); po.asyncRequest(request, 8000).onSuccess(bench::offer); EventEnvelope response = bench.poll(10, TimeUnit.SECONDS); assert response != null; Assert.assertEquals(HashMap.class, response.getBody().getClass()); Map map = (Map) response.getBody(); System.out.println(response.getBody()); Assert.assertEquals(len, map.get(\"expected_size\")); Assert.assertEquals(len, map.get(\"actual_size\")); Assert.assertEquals(FILENAME, map.get(\"filename\")); Assert.assertEquals(\"Upload completed\", map.get(\"message\")); // finally check that \"hello.upload\" has saved the test file File dir = new File(\"/tmp/upload-download-demo\"); File file = new File(dir, FILENAME); Assert.assertTrue(file.exists()); Assert.assertEquals(len, file.length()); // compare file content byte[] b = Utility.getInstance().file2bytes(file); Assert.assertArrayEquals(bytes.toByteArray(), b); } In the above unit test, we use the ObjectStreamIO to emulate a file stream and write 10 blocks of data into it. The unit test then makes an RPC call to the \"hello.upload\" with the emulated HTTP request event. The \"hello.upload\" is a Kotlin suspend function. It will be executed when the event arrives. After saving the test file, it will return an HTTP response object that the unit test can validate. In this fashion, you can create unit tests to test suspend functions in an event-driven manner. Deployment The pom.xml is pre-configured to generate an executable JAR. The following is extracted from the pom.xml. The main class is AutoStart that will load the \"main application\" and use it as the entry point to run the application. org.springframework.boot spring-boot-maven-plugin org.platformlambda.core.system.AutoStart build-info build-info Composable application is designed to be deployable using Kubernetes or serverless. A sample Dockerfile for an executable JAR may look like this: FROM mcr.microsoft.com/openjdk/jdk:11-ubuntu EXPOSE 8083 WORKDIR /app COPY target/your-app-name.jar . ENTRYPOINT [\"java\",\"-jar\",\"your-app-name.jar\"] Distributed tracing The system has a built-in distributed tracing feature. You can enable tracing for any REST endpoint by adding \"tracing=true\" in the endpoint definition in the \"rest.yaml\" configuration file. You may also upload performance metrics from the distributed tracing data to your favorite telemetry system dashboard. To do that, please implement a custom metrics function with the route name distributed.trace.forwarder . The input to the function will be a HashMap like this: trace={path=/api/upload/demo, service=hello.upload, success=true, origin=2023032731e2a5eeae8f4da09f3d9ac6b55fb0a4, exec_time=77.462, start=2023-03-27T19:38:30.061Z, from=http.request, id=12345, round_trip=132.296, status=200} The system will detect if distributed.trace.forwarder is available. If yes, it will forward performance metrics from distributed trace to your custom function. Request-response journaling Optionally, you may also implement a custom audit function named transaction.journal.recorder to monitor request-response payloads. To enable journaling, please add this to the application.properties file. journal.yaml=classpath:/journal.yaml and add the \"journal.yaml\" configuration file to the project's resources folder with content like this: journal: - \"my.test.function\" - \"another.function\" In the above example, the \"my.test.function\" and \"another.function\" will be monitored and their request-response payloads will be forwarded to your custom audit function. The input to your audit function will be a HashMap containing the performance metrics data and a \"journal\" section with the request and response payloads in clear form. IMPORTANT: journaling may contain sensitive personally identifiable data and secrets. Please check security compliance before storing them into access restricted audit data store. Chapter-4 Home Chapter-6 Event orchestration Table of Contents Spring Boot","title":"Chapter-5"},{"location":"guides/CHAPTER-5/#build-test-and-deploy","text":"The first step in writing an application is to create an entry point for your application.","title":"Build, Test and Deploy"},{"location":"guides/CHAPTER-5/#main-application","text":"A minimalist main application template is shown as follows: @MainApplication public class MainApp implements EntryPoint { public static void main(String[] args) { AutoStart.main(args); } @Override public void start(String[] args) { // your startup logic here log.info(\"Started\"); } } Note that MainApplication is mandatory. You must have at least one \"main application\" module. Note: Please adjust the parameter \"web.component.scan\" in application.properties to point to your user application package(s) in your source code project. If your application does not require additional startup logic, you may just print a greeting message. The AutoStart.main() statement in the \"main\" method is used when you want to start your application within the IDE. You can \"right-click\" the main method and select \"run\". You can also build and run the application from command line like this: cd sandbox/mercury/examples/lambda-example mvn clean package java -jar target/lambda-example-3.0.9.jar The lambda-example is a sample application that you can use as a template to write your own code. Please review the pom.xml and the source directory structure. The pom.xml is pre-configured to support Java and Kotlin. In the lambda-example project root, you will find the following directories: src/main/java src/main/kotlin src/test/java Note that kotlin unit test directory is not included because you can test all functions in Java unit tests. Since all functions are connected using the in-memory event bus, you can test any function by sending events from a unit test module in Java. If you are comfortable with the Kotlin language, you may also set up Kotlin unit tests accordingly. There is no harm having both types of unit tests in the same project.","title":"Main application"},{"location":"guides/CHAPTER-5/#source-code-documentation","text":"Since the source project contains both Java and Kotlin, we have replaced javadoc maven plugin with Jetbrains \"dokka\" documentation engine for both Java and Kotlin. Javadoc is useful if you want to write and publish your own libraries. To generate Java and Kotlin source documentation, please run \"mvn dokka:dokka\". You may \"cd\" to the platform-core project to try the maven dokka command to generate some source documentation. The home page will be available in \"target/dokka/index.html\"","title":"Source code documentation"},{"location":"guides/CHAPTER-5/#writing-your-functions","text":"Please follow the step-by-step learning guide in Chapter-1 to write your own functions. You can then configure new REST endpoints to use your new functions. In Chapter-1 , we have discussed the three function execution strategies to optimize your application to the full potential of stability, performance and throughput.","title":"Writing your functions"},{"location":"guides/CHAPTER-5/#http-forwarding","text":"In Chapter-3 , we have presented the configuration syntax for the \"rest.yaml\" REST automation definition file. Please review the sample rest.yaml file in the lambda-example project. You may notice that it has an entry for HTTP forwarding. The following entry in the sample rest.yaml file illustrates an HTTP forwarding endpoint. In HTTP forwarding, you can replace the \"service\" route name with a direct HTTP target host. You can do \"URL rewrite\" to change the URL path to the target endpoint path. In the below example, /api/v1/* will be mapped to /api/* in the target endpoint. - service: \"http://127.0.0.1:${rest.server.port}\" trust_all_cert: true methods: ['GET', 'PUT', 'POST'] url: \"/api/v1/*\" url_rewrite: ['/api/v1', '/api'] timeout: 20 cors: cors_1 headers: header_1 tracing: true","title":"HTTP forwarding"},{"location":"guides/CHAPTER-5/#sending-http-request-event-to-more-than-one-service","text":"One feature in REST automation \"rest.yaml\" configuration is that you can configure more than one function in the \"service\" section. In the following example, there are two function route names (\"hello.world\" and \"hello.copy\"). The first one \"hello.world\" is the primary service provider. The second one \"hello.copy\" will receive a copy of the incoming event automatically. This feature allows you to write new version of a function without disruption to current functionality. Once you are happy with the new version of function, you can route the endpoint directly to the new version by updating the \"rest.yaml\" configuration file. - service: [\"hello.world\", \"hello.copy\"]","title":"Sending HTTP request event to more than one service"},{"location":"guides/CHAPTER-5/#writing-your-first-unit-test","text":"Please refer to \"rpcTest\" method in the \"HelloWorldTest\" class in the lambda-example to get started. In unit test, we want to start the main application so that all the functions are ready for tests. First, we write a \"TestBase\" class to use the BeforeClass setup method to start the main application like this: public class TestBase { private static final AtomicInteger seq = new AtomicInteger(0); @BeforeClass public static void setup() { if (seq.incrementAndGet() == 1) { AutoStart.main(new String[0]); } } } The atomic integer \"seq\" is used to ensure the main application entry point is executed only once. Your first unit test may look like this: @SuppressWarnings(\"unchecked\") @Test public void rpcTest() throws IOException, InterruptedException { Utility util = Utility.getInstance(); BlockingQueue bench = new ArrayBlockingQueue<>(1); String name = \"hello\"; String address = \"world\"; String telephone = \"123-456-7890\"; DemoPoJo pojo = new DemoPoJo(name, address, telephone); PostOffice po = new PostOffice(\"unit.test\", \"12345\", \"POST /api/hello/world\"); EventEnvelope request = new EventEnvelope().setTo(\"hello.world\") .setHeader(\"a\", \"b\").setBody(pojo.toMap()); po.asyncRequest(request, 800).onSuccess(bench::offer); EventEnvelope response = bench.poll(10, TimeUnit.SECONDS); assert response != null; Assert.assertEquals(HashMap.class, response.getBody().getClass()); MultiLevelMap map = new MultiLevelMap((Map) response.getBody()); Assert.assertEquals(\"b\", map.getElement(\"headers.a\")); Assert.assertEquals(name, map.getElement(\"body.name\")); Assert.assertEquals(address, map.getElement(\"body.address\")); Assert.assertEquals(telephone, map.getElement(\"body.telephone\")); Assert.assertEquals(util.date2str(pojo.time), map.getElement(\"body.time\")); } Note that the PostOffice instance can be created with tracing information in a Unit Test. The above example tells the system that the sender is \"unit.test\", the trace ID is 12345 and the trace path is \"POST /api/hello/world\". For unit test, we need to convert the asynchronous code into \"synchronous\" execution so that unit test can run sequentially. \"BlockingQueue\" is a good choice for this. The \"hello.world\" is an echo function. The above unit test sends an event containing a key-value {\"a\":\"b\"} and the payload of a HashMap from the DemoPoJo. If the function is designed to handle PoJo, we can send PoJo directly instead of a Map. IMPORTANT: blocking code should only be used for unit tests. DO NOT use blocking code in your application code because it will block the event system and dramatically slow down your application.","title":"Writing your first unit test"},{"location":"guides/CHAPTER-5/#convenient-utility-classes","text":"The Utility and MultiLevelMap classes are convenient tools for unit tests. In the above example, we use the Utility class to convert a date object into a UTC timestamp. It is because date object is serialized as a UTC timestamp in an event. The MultiLevelMap supports reading an element using the convenient \"dot and bracket\" format. For example, given a map like this: { \"body\": { \"time\": \"2023-03-27T18:10:34.234Z\", \"hello\": [1, 2, 3] } } Example Command Result 1 map.getElement(\"body.time\") 2023-03-27T18:10:34.234Z 2 map.getElement(\"body.hello[2]\") 3","title":"Convenient utility classes"},{"location":"guides/CHAPTER-5/#the-second-unit-test","text":"Let's do a unit test for PoJo. In this second unit test, it sends a RPC request to the \"hello.pojo\" function that is designed to return a SamplePoJo object with some mock data. Please refer to \"pojoRpcTest\" method in the \"PoJoTest\" class in the lambda-example for details. The unit test verifies that the \"hello.pojo\" has correctly returned the SamplePoJo object with the pre-defined mock value. @Test public void pojoTest() throws IOException, InterruptedException { Integer ID = 1; String NAME = \"Simple PoJo class\"; String ADDRESS = \"100 World Blvd, Planet Earth\"; BlockingQueue bench = new ArrayBlockingQueue<>(1); PostOffice po = new PostOffice(\"unit.test\", \"20001\", \"GET /api/hello/pojo\"); EventEnvelope request = new EventEnvelope().setTo(\"hello.pojo\").setHeader(\"id\", \"1\"); po.asyncRequest(request, 800).onSuccess(bench::offer); EventEnvelope response = bench.poll(10, TimeUnit.SECONDS); assert response != null; Assert.assertEquals(SamplePoJo.class, response.getBody().getClass()); SamplePoJo pojo = response.getBody(SamplePoJo.class); Assert.assertEquals(ID, pojo.getId()); Assert.assertEquals(NAME, pojo.getName()); Assert.assertEquals(ADDRESS, pojo.getAddress()); } Note that you can do class \"casting\" or use the built-in casting API as shown below: SamplePoJo pojo = (SamplePoJo) response.getBody() SamplePoJo pojo = response.getBody(SamplePoJo.class)","title":"The second unit test"},{"location":"guides/CHAPTER-5/#the-third-unit-test","text":"Testing Kotlin suspend functions is challenging. However, testing suspend function using events is straight forward because of loose coupling. Let's do a unit test for the lambda-example's FileUploadDemo function. Its route name is \"hello.upload\". Please refer to \"uploadTest\" method in the \"SuspendFunctionTest\" class in the lambda-example for details. @SuppressWarnings(\"unchecked\") @Test public void uploadTest() throws IOException, InterruptedException { String FILENAME = \"unit-test-data.txt\"; BlockingQueue bench = new ArrayBlockingQueue<>(1); Utility util = Utility.getInstance(); PostOffice po = PostOffice.getInstance(); int len = 0; ByteArrayOutputStream bytes = new ByteArrayOutputStream(); ObjectStreamIO stream = new ObjectStreamIO(); ObjectStreamWriter out = new ObjectStreamWriter(stream.getOutputStreamId()); for (int i=0; i < 10; i++) { String line = \"hello world \"+i+\"\\n\"; byte[] d = util.getUTF(line); out.write(d); bytes.write(d); len += d.length; } out.close(); // emulate a multi-part file upload AsyncHttpRequest req = new AsyncHttpRequest(); req.setMethod(\"POST\"); req.setUrl(\"/api/upload/demo\"); req.setTargetHost(\"http://127.0.0.1:8080\"); req.setHeader(\"accept\", \"application/json\"); req.setHeader(\"content-type\", \"multipart/form-data\"); req.setContentLength(len); req.setFileName(FILENAME); req.setStreamRoute(stream.getInputStreamId()); // send the HTTP request event to the \"hello.upload\" function EventEnvelope request = new EventEnvelope().setTo(\"hello.upload\").setBody(req); po.asyncRequest(request, 8000).onSuccess(bench::offer); EventEnvelope response = bench.poll(10, TimeUnit.SECONDS); assert response != null; Assert.assertEquals(HashMap.class, response.getBody().getClass()); Map map = (Map) response.getBody(); System.out.println(response.getBody()); Assert.assertEquals(len, map.get(\"expected_size\")); Assert.assertEquals(len, map.get(\"actual_size\")); Assert.assertEquals(FILENAME, map.get(\"filename\")); Assert.assertEquals(\"Upload completed\", map.get(\"message\")); // finally check that \"hello.upload\" has saved the test file File dir = new File(\"/tmp/upload-download-demo\"); File file = new File(dir, FILENAME); Assert.assertTrue(file.exists()); Assert.assertEquals(len, file.length()); // compare file content byte[] b = Utility.getInstance().file2bytes(file); Assert.assertArrayEquals(bytes.toByteArray(), b); } In the above unit test, we use the ObjectStreamIO to emulate a file stream and write 10 blocks of data into it. The unit test then makes an RPC call to the \"hello.upload\" with the emulated HTTP request event. The \"hello.upload\" is a Kotlin suspend function. It will be executed when the event arrives. After saving the test file, it will return an HTTP response object that the unit test can validate. In this fashion, you can create unit tests to test suspend functions in an event-driven manner.","title":"The third unit test"},{"location":"guides/CHAPTER-5/#deployment","text":"The pom.xml is pre-configured to generate an executable JAR. The following is extracted from the pom.xml. The main class is AutoStart that will load the \"main application\" and use it as the entry point to run the application. org.springframework.boot spring-boot-maven-plugin org.platformlambda.core.system.AutoStart build-info build-info Composable application is designed to be deployable using Kubernetes or serverless. A sample Dockerfile for an executable JAR may look like this: FROM mcr.microsoft.com/openjdk/jdk:11-ubuntu EXPOSE 8083 WORKDIR /app COPY target/your-app-name.jar . ENTRYPOINT [\"java\",\"-jar\",\"your-app-name.jar\"]","title":"Deployment"},{"location":"guides/CHAPTER-5/#distributed-tracing","text":"The system has a built-in distributed tracing feature. You can enable tracing for any REST endpoint by adding \"tracing=true\" in the endpoint definition in the \"rest.yaml\" configuration file. You may also upload performance metrics from the distributed tracing data to your favorite telemetry system dashboard. To do that, please implement a custom metrics function with the route name distributed.trace.forwarder . The input to the function will be a HashMap like this: trace={path=/api/upload/demo, service=hello.upload, success=true, origin=2023032731e2a5eeae8f4da09f3d9ac6b55fb0a4, exec_time=77.462, start=2023-03-27T19:38:30.061Z, from=http.request, id=12345, round_trip=132.296, status=200} The system will detect if distributed.trace.forwarder is available. If yes, it will forward performance metrics from distributed trace to your custom function.","title":"Distributed tracing"},{"location":"guides/CHAPTER-5/#request-response-journaling","text":"Optionally, you may also implement a custom audit function named transaction.journal.recorder to monitor request-response payloads. To enable journaling, please add this to the application.properties file. journal.yaml=classpath:/journal.yaml and add the \"journal.yaml\" configuration file to the project's resources folder with content like this: journal: - \"my.test.function\" - \"another.function\" In the above example, the \"my.test.function\" and \"another.function\" will be monitored and their request-response payloads will be forwarded to your custom audit function. The input to your audit function will be a HashMap containing the performance metrics data and a \"journal\" section with the request and response payloads in clear form. IMPORTANT: journaling may contain sensitive personally identifiable data and secrets. Please check security compliance before storing them into access restricted audit data store. Chapter-4 Home Chapter-6 Event orchestration Table of Contents Spring Boot","title":"Request-response journaling"},{"location":"guides/CHAPTER-6/","text":"Spring Boot Integration While the platform-core foundation code includes a lightweight non-blocking HTTP server, you can also turn your application into an executable Spring Boot application. There are two ways to do that: Add dependency for Spring Boot version 2.7.10 (or version 3.0.5) and implement your Spring Boot main application Add the rest-spring-2 or rest-spring-3 add-on library for a pre-configured Spring Boot experience Add platform-core to an existing Spring Boot application For option 1, the platform-core library can co-exist with Spring Boot. You can write code specific to Spring Boot and the Spring framework ecosystem. Please make sure you add the following startup code to your Spring Boot main application like this: @SpringBootApplication public class MyMainApp extends SpringBootServletInitializer { public static void main(String[] args) { AutoStart.main(args); SpringApplication.run(MyMainApp.class, args); } } We suggest running AutoStart.main before the SpringApplication.run statement. This would allow the platform-core foundation code to load the event-listener functions into memory before Spring Boot starts. Use the rest-spring library in your application You can add the rest-spring-2 or rest-spring-3 library in your application and turn it into a pre-configured Spring Boot 2 or 3 application. The \"rest-spring\" library configures Spring Boot's serializers (XML and JSON) to behave consistently as the built-in lightweight non-blocking HTTP server. If you want to disable the lightweight HTTP server, you can set rest.automation=false in application.properties. The REST automation engine and the lightweight HTTP server will be turned off. IMPORTANT: the platform-core library assumes the application configuration files to be either application.yml or application.properties. If you use custom Spring profile, please keep the application.yml or application.properties for the platform-core. If you use default Spring profile, both platform-core and Spring Boot will use the same configuration files. You can customize your error page using the default errorPage.html by copying it from the platform-core's or rest-spring's resources folder to your source project. The default page is shown below. This is the HTML error page that the platform-core or rest-spring library uses. You can update it with your corporate style guide. Please keep the parameters (status, message, path, warning) intact. HTTP Error

HTTP-${status}

${warning}

Typeerror
Status${status}
Message${message}
Path${path}
If you want to keep REST automation's lightweight HTTP server together with Spring Boot's Tomcat or other application server, please add the following to your application.properties file: server.port=8083 rest.server.port=8085 rest.automation=true The platform-core and Spring Boot will use rest.server.port and server.port respectively. The rest-spring-2-example demo application Let's review the rest-spring-2-example demo application in the \"examples/rest-spring-2-example\" project. You can use the rest-spring-2-example as a template to create a Spring Boot application. In addition to the REST automation engine that let you create REST endpoints by configuration, you can also programmatically create REST endpoints with the following approaches: JAX-RS REST endpoints Spring RestControllers Servlet 3.1 WebServlets We will examine asynchronous REST endpoint with the AsyncHelloWorld class. Since the platform-core is event-driven, we would like to use JAX-RS asynchronous HTTP context AsyncResponse in the REST endpoints so that the endpoint does not block. @Path(\"/hello\") public class AsyncHelloWorld { private static final AtomicInteger seq = new AtomicInteger(0); @GET @Path(\"/world\") @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public void hello(@Context HttpServletRequest request, @Suspended AsyncResponse response) { String traceId = Utility.getInstance().getUuid(); PostOffice po = new PostOffice(\"hello.world.endpoint\", traceId, \"GET /api/hello/world\"); Map forward = new HashMap<>(); Enumeration headers = request.getHeaderNames(); while (headers.hasMoreElements()) { String key = headers.nextElement(); forward.put(key, request.getHeader(key)); } // As a demo, just put the incoming HTTP headers as a payload and add a counter // The echo service will return both. int n = seq.incrementAndGet(); EventEnvelope req = new EventEnvelope(); req.setTo(\"hello.world\").setBody(forward).setHeader(\"seq\", n); Future res = po.asyncRequest(req, 3000); res.onSuccess(event -> { Map result = new HashMap<>(); result.put(\"status\", event.getStatus()); result.put(\"headers\", event.getHeaders()); result.put(\"body\", event.getBody()); result.put(\"execution_time\", event.getExecutionTime()); result.put(\"round_trip\", event.getRoundTrip()); response.resume(result); }); res.onFailure(ex -> response.resume(new AppException(408, ex.getMessage()))); } } In this hello world REST endpoint, JAX-RS runs the \"hello\" method asynchronously without waiting for a response. The example code copies the HTTP requests and sends it as the request payload to the \"hello.world\" function. The function is defined in the MainApp like this: Platform platform = Platform.getInstance(); LambdaFunction echo = (headers, input, instance) -> { Map result = new HashMap<>(); result.put(\"headers\", headers); result.put(\"body\", input); result.put(\"instance\", instance); result.put(\"origin\", platform.getOrigin()); return result; }; platform.register(\"hello.world\", echo, 20); When \"hello.world\" responds, its result set will be returned to the onSuccess method as a \"future response\". The \"onSuccess\" method then sends the response to the browser using the JAX-RS resume mechanism. The AsyncHelloConcurrent is the same as the AsyncHelloWorld except that it performs a \"fork-n-join\" operation to multiple instances of the \"hello.world\" function. Unlike \"rest.yaml\" that defines tracing by configuration, you can turn on tracing programmatically in a JAX-RS endpoint. To enable tracing, the function sets the trace ID and path in the PostOffice constructor. When you try the endpoint at http://127.0.0.1:8083/api/hello/world, it will echo your HTTP request headers. In the command terminal, you will see tracing information in the console log like this: DistributedTrace:67 - trace={path=GET /api/hello/world, service=hello.world, success=true, origin=20230403364f70ebeb54477f91986289dfcd7b75, exec_time=0.249, start=2023-04-03T04:42:43.445Z, from=hello.world.endpoint, id=e12e871096ba4938b871ee72ef09aa0a, round_trip=20.018, status=200} Lightweight non-blocking websocket server If you want to turn on a non-blocking websocket server, you can add the following configuration to application.properties. server.port=8083 websocket.server.port=8085 The above assumes Spring Boot runs on port 8083 and the websocket server runs on port 8085. Note that \"websocket.server.port\" is an alias of \"rest.server.port\" You can create a websocket service with a Java class like this: @WebSocketService(\"hello\") public class WsEchoDemo implements LambdaFunction { @Override public Object handleEvent(Map headers, Object body, int instance) { // handle the incoming websocket events (type = open, close, bytes or string) } } The above creates a websocket service at the URL \"/ws/hello\" server endpoint. Please review the example code in the WsEchoDemo class in the rest-spring-2-example project for details. If you want to use Spring Boot's Tomcat websocket server, you can disable the non-blocking websocket server feature by removing the websocket.server.port configuration and any websocket service classes with the WebSocketService annotation. To try out the demo websocket server, visit http://127.0.0.1:8083 and select \"Websocket demo\". Spring Boot version 3 The rest-spring-3 subproject is a pre-configured Spring Boot 3 library. In \"rest-spring-3\", Spring WebFlux replaces JAX-RS as the asynchronous HTTP servlet engine. Chapter-5 Home Chapter-7 Build, Test and Deploy Table of Contents Event over HTTP","title":"Chapter-6"},{"location":"guides/CHAPTER-6/#spring-boot-integration","text":"While the platform-core foundation code includes a lightweight non-blocking HTTP server, you can also turn your application into an executable Spring Boot application. There are two ways to do that: Add dependency for Spring Boot version 2.7.10 (or version 3.0.5) and implement your Spring Boot main application Add the rest-spring-2 or rest-spring-3 add-on library for a pre-configured Spring Boot experience","title":"Spring Boot Integration"},{"location":"guides/CHAPTER-6/#add-platform-core-to-an-existing-spring-boot-application","text":"For option 1, the platform-core library can co-exist with Spring Boot. You can write code specific to Spring Boot and the Spring framework ecosystem. Please make sure you add the following startup code to your Spring Boot main application like this: @SpringBootApplication public class MyMainApp extends SpringBootServletInitializer { public static void main(String[] args) { AutoStart.main(args); SpringApplication.run(MyMainApp.class, args); } } We suggest running AutoStart.main before the SpringApplication.run statement. This would allow the platform-core foundation code to load the event-listener functions into memory before Spring Boot starts.","title":"Add platform-core to an existing Spring Boot application"},{"location":"guides/CHAPTER-6/#use-the-rest-spring-library-in-your-application","text":"You can add the rest-spring-2 or rest-spring-3 library in your application and turn it into a pre-configured Spring Boot 2 or 3 application. The \"rest-spring\" library configures Spring Boot's serializers (XML and JSON) to behave consistently as the built-in lightweight non-blocking HTTP server. If you want to disable the lightweight HTTP server, you can set rest.automation=false in application.properties. The REST automation engine and the lightweight HTTP server will be turned off. IMPORTANT: the platform-core library assumes the application configuration files to be either application.yml or application.properties. If you use custom Spring profile, please keep the application.yml or application.properties for the platform-core. If you use default Spring profile, both platform-core and Spring Boot will use the same configuration files. You can customize your error page using the default errorPage.html by copying it from the platform-core's or rest-spring's resources folder to your source project. The default page is shown below. This is the HTML error page that the platform-core or rest-spring library uses. You can update it with your corporate style guide. Please keep the parameters (status, message, path, warning) intact. HTTP Error

HTTP-${status}

${warning}

Typeerror
Status${status}
Message${message}
Path${path}
If you want to keep REST automation's lightweight HTTP server together with Spring Boot's Tomcat or other application server, please add the following to your application.properties file: server.port=8083 rest.server.port=8085 rest.automation=true The platform-core and Spring Boot will use rest.server.port and server.port respectively.","title":"Use the rest-spring library in your application"},{"location":"guides/CHAPTER-6/#the-rest-spring-2-example-demo-application","text":"Let's review the rest-spring-2-example demo application in the \"examples/rest-spring-2-example\" project. You can use the rest-spring-2-example as a template to create a Spring Boot application. In addition to the REST automation engine that let you create REST endpoints by configuration, you can also programmatically create REST endpoints with the following approaches: JAX-RS REST endpoints Spring RestControllers Servlet 3.1 WebServlets We will examine asynchronous REST endpoint with the AsyncHelloWorld class. Since the platform-core is event-driven, we would like to use JAX-RS asynchronous HTTP context AsyncResponse in the REST endpoints so that the endpoint does not block. @Path(\"/hello\") public class AsyncHelloWorld { private static final AtomicInteger seq = new AtomicInteger(0); @GET @Path(\"/world\") @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public void hello(@Context HttpServletRequest request, @Suspended AsyncResponse response) { String traceId = Utility.getInstance().getUuid(); PostOffice po = new PostOffice(\"hello.world.endpoint\", traceId, \"GET /api/hello/world\"); Map forward = new HashMap<>(); Enumeration headers = request.getHeaderNames(); while (headers.hasMoreElements()) { String key = headers.nextElement(); forward.put(key, request.getHeader(key)); } // As a demo, just put the incoming HTTP headers as a payload and add a counter // The echo service will return both. int n = seq.incrementAndGet(); EventEnvelope req = new EventEnvelope(); req.setTo(\"hello.world\").setBody(forward).setHeader(\"seq\", n); Future res = po.asyncRequest(req, 3000); res.onSuccess(event -> { Map result = new HashMap<>(); result.put(\"status\", event.getStatus()); result.put(\"headers\", event.getHeaders()); result.put(\"body\", event.getBody()); result.put(\"execution_time\", event.getExecutionTime()); result.put(\"round_trip\", event.getRoundTrip()); response.resume(result); }); res.onFailure(ex -> response.resume(new AppException(408, ex.getMessage()))); } } In this hello world REST endpoint, JAX-RS runs the \"hello\" method asynchronously without waiting for a response. The example code copies the HTTP requests and sends it as the request payload to the \"hello.world\" function. The function is defined in the MainApp like this: Platform platform = Platform.getInstance(); LambdaFunction echo = (headers, input, instance) -> { Map result = new HashMap<>(); result.put(\"headers\", headers); result.put(\"body\", input); result.put(\"instance\", instance); result.put(\"origin\", platform.getOrigin()); return result; }; platform.register(\"hello.world\", echo, 20); When \"hello.world\" responds, its result set will be returned to the onSuccess method as a \"future response\". The \"onSuccess\" method then sends the response to the browser using the JAX-RS resume mechanism. The AsyncHelloConcurrent is the same as the AsyncHelloWorld except that it performs a \"fork-n-join\" operation to multiple instances of the \"hello.world\" function. Unlike \"rest.yaml\" that defines tracing by configuration, you can turn on tracing programmatically in a JAX-RS endpoint. To enable tracing, the function sets the trace ID and path in the PostOffice constructor. When you try the endpoint at http://127.0.0.1:8083/api/hello/world, it will echo your HTTP request headers. In the command terminal, you will see tracing information in the console log like this: DistributedTrace:67 - trace={path=GET /api/hello/world, service=hello.world, success=true, origin=20230403364f70ebeb54477f91986289dfcd7b75, exec_time=0.249, start=2023-04-03T04:42:43.445Z, from=hello.world.endpoint, id=e12e871096ba4938b871ee72ef09aa0a, round_trip=20.018, status=200}","title":"The rest-spring-2-example demo application"},{"location":"guides/CHAPTER-6/#lightweight-non-blocking-websocket-server","text":"If you want to turn on a non-blocking websocket server, you can add the following configuration to application.properties. server.port=8083 websocket.server.port=8085 The above assumes Spring Boot runs on port 8083 and the websocket server runs on port 8085. Note that \"websocket.server.port\" is an alias of \"rest.server.port\" You can create a websocket service with a Java class like this: @WebSocketService(\"hello\") public class WsEchoDemo implements LambdaFunction { @Override public Object handleEvent(Map headers, Object body, int instance) { // handle the incoming websocket events (type = open, close, bytes or string) } } The above creates a websocket service at the URL \"/ws/hello\" server endpoint. Please review the example code in the WsEchoDemo class in the rest-spring-2-example project for details. If you want to use Spring Boot's Tomcat websocket server, you can disable the non-blocking websocket server feature by removing the websocket.server.port configuration and any websocket service classes with the WebSocketService annotation. To try out the demo websocket server, visit http://127.0.0.1:8083 and select \"Websocket demo\".","title":"Lightweight non-blocking websocket server"},{"location":"guides/CHAPTER-6/#spring-boot-version-3","text":"The rest-spring-3 subproject is a pre-configured Spring Boot 3 library. In \"rest-spring-3\", Spring WebFlux replaces JAX-RS as the asynchronous HTTP servlet engine. Chapter-5 Home Chapter-7 Build, Test and Deploy Table of Contents Event over HTTP","title":"Spring Boot version 3"},{"location":"guides/CHAPTER-7/","text":"Event over HTTP The in-memory event system allows functions to communicate with each other in the same application memory space. In composable architecture, applications are modular components in a network. Some transactions may require the services of more than one application. \"Event over HTTP\" extends the event system beyond a single application. The Event API service ( event.api.service ) is a built-in function in the system. The Event API endpoint To enable \"Event over HTTP\", you must first turn on the REST automation engine with the following parameters in the application.properties file: rest.server.port=8085 rest.automation=true and then check if the following entry is configured in the \"rest.yaml\" endpoint definition file. If not, update \"rest.yaml\" accordingly. The \"timeout\" value is set to 60 seconds to fit common use cases. - service: [ \"event.api.service\" ] methods: [ 'POST' ] url: \"/api/event\" timeout: 60s tracing: true This will expose the Event API endpoint at port 8085 and URL \"/api/event\". In kubernetes, The Event API endpoint of each application is reachable through internal DNS and there is no need to create \"ingress\" for this purpose. Test drive Event API You may now test drive the Event API service. First, build and run the lambda-example application in port 8085. cd examples/lambda-example java -jar target/lambda-example-3.0.9.jar Second, build and run the rest-spring-example application. cd examples/rest-spring-example-2 java -jar target/rest-spring-2-example-3.0.9.jar The rest-spring-2-example application will run as a Spring Boot application in port 8083 and 8086. These two applications will start independently. You may point your browser to http://127.0.0.1:8083/api/pojo/http/1 to invoke the HelloPojoEventOverHttp endpoint service that will in turn makes an Event API call to the lambda-example's \"hello.pojo\" service. You will see the following response in the browser. This means the rest-spring-example application has successfully made an event API call to the lambda-example application using the Event API endpoint. { \"id\": 1, \"name\": \"Simple PoJo class\", \"address\": \"100 World Blvd, Planet Earth\", \"date\": \"2023-03-27T23:17:19.257Z\", \"instance\": 6, \"seq\": 66, \"origin\": \"2023032791b6938a47614cf48779b1cf02fc89c4\" } To examine how the application makes the Event API call, please refer to the HelloPojoEventOverHttp class in the rest-spring-example. The class is extracted below: @Path(\"/pojo\") public class HelloPoJoEventOverHttp { @GET @Path(\"/http/{id}\") @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public void getPoJo(@PathParam(\"id\") Integer id, @Suspended AsyncResponse response) { AppConfigReader config = AppConfigReader.getInstance(); String remotePort = config.getProperty(\"lambda.example.port\", \"8085\"); String remoteEndpoint = \"http://127.0.0.1:\"+remotePort+\"/api/event\"; String traceId = Utility.getInstance().getUuid(); PostOffice po = new PostOffice(\"hello.pojo.endpoint\", traceId, \"GET /api/pojo/http\"); EventEnvelope req = new EventEnvelope().setTo(\"hello.pojo\").setHeader(\"id\", id); Future res = po.asyncRequest(req, 5000, Collections.emptyMap(), remoteEndpoint, true); res.onSuccess(event -> { // confirm that the PoJo object is transported correctly over the event stream system if (event.getBody() instanceof SamplePoJo) { response.resume(event.getBody()); } else { response.resume(new AppException(event.getStatus(), event.getError())); } }); res.onFailure(response::resume); } } The method signatures of the Event API is shown as follows: Asynchronous API (Java) public Future asyncRequest(final EventEnvelope event, long timeout, Map headers, String eventEndpoint, boolean rpc) throws IOException; Sequential non-blocking API (Kotlin suspend function) suspend fun awaitRequest(request: EventEnvelope?, timeout: Long, headers: Map, eventEndpoint: String, rpc: Boolean): EventEnvelope Optionally, you may add security headers in the \"headers\" argument. e.g. the \"Authorization\" header. The eventEndpoint is a fully qualified URL. e.g. http://peer/api/event The \"rpc\" boolean value is set to true so that the response from the service of the peer application instance will be delivered. For drop-n-forget use case, you can set the \"rpc\" value to false. It will immediately return an HTTP-202 response. Event-over-HTTP using configuration While you can call the \"Event-over-HTTP\" APIs programmatically, it would be more convenient to automate it with a configuration. This service abstraction means that user applications do not need to know where the target services are. You can enable Event-over-HTTP configuration by adding this parameter in application.properties: # # Optional event-over-http target maps # yaml.event.over.http=classpath:/event-over-http.yaml and then create the configuration file \"event-over-http.yaml\" like this: event: http: - route: 'event.http.test' target: 'http://127.0.0.1:${server.port}/api/event' # optional security headers headers: authorization: 'demo' - route: 'event.save.get' target: 'http://127.0.0.1:${server.port}/api/event' headers: authorization: 'demo' In the above example, there are two routes (event.http.test and event.save.get) with target URLs. If additional authentication is required for the peer's \"/api/event\" endpoint, you may add a set of security headers in each route. When you send asynchronous event or make a RPC call to \"event.save.get\" service, it will be forwarded to the peer's \"event-over-HTTP\" endpoint ( /api/event ) accordingly. You may also add variable references to the application.properties (or application.yaml) file, such as \"server.port\" in this example. Note: The configuration based \"event-over-HTTP\" feature does not support fork-n-join request API. Advantages The Event API exposes all public functions of an application instance to the network using a single REST endpoint. The advantages of Event API includes: Convenient - you do not need to write or configure individual endpoint for each public service Efficient - events are transported in binary format from one application to another Secure - you can protect the Event API endpoint with an authentication service The following configuration adds authentication service to the Event API endpoint: - service: [ \"event.api.service\" ] methods: [ 'POST' ] url: \"/api/event\" timeout: 60s authentication: \"v1.api.auth\" tracing: true This enforces every incoming request to the Event API endpoint to be authenticated by the \"v1.api.auth\" service before passing to the Event API service. You can plug in your own authentication service such as OAuth 2.0 \"bearer token\" validation. Please refer to Chapter-3 - REST automation for details. Chapter-6 Home Chapter-8 Spring Boot Table of Contents Service Mesh","title":"Chapter-7"},{"location":"guides/CHAPTER-7/#event-over-http","text":"The in-memory event system allows functions to communicate with each other in the same application memory space. In composable architecture, applications are modular components in a network. Some transactions may require the services of more than one application. \"Event over HTTP\" extends the event system beyond a single application. The Event API service ( event.api.service ) is a built-in function in the system.","title":"Event over HTTP"},{"location":"guides/CHAPTER-7/#the-event-api-endpoint","text":"To enable \"Event over HTTP\", you must first turn on the REST automation engine with the following parameters in the application.properties file: rest.server.port=8085 rest.automation=true and then check if the following entry is configured in the \"rest.yaml\" endpoint definition file. If not, update \"rest.yaml\" accordingly. The \"timeout\" value is set to 60 seconds to fit common use cases. - service: [ \"event.api.service\" ] methods: [ 'POST' ] url: \"/api/event\" timeout: 60s tracing: true This will expose the Event API endpoint at port 8085 and URL \"/api/event\". In kubernetes, The Event API endpoint of each application is reachable through internal DNS and there is no need to create \"ingress\" for this purpose.","title":"The Event API endpoint"},{"location":"guides/CHAPTER-7/#test-drive-event-api","text":"You may now test drive the Event API service. First, build and run the lambda-example application in port 8085. cd examples/lambda-example java -jar target/lambda-example-3.0.9.jar Second, build and run the rest-spring-example application. cd examples/rest-spring-example-2 java -jar target/rest-spring-2-example-3.0.9.jar The rest-spring-2-example application will run as a Spring Boot application in port 8083 and 8086. These two applications will start independently. You may point your browser to http://127.0.0.1:8083/api/pojo/http/1 to invoke the HelloPojoEventOverHttp endpoint service that will in turn makes an Event API call to the lambda-example's \"hello.pojo\" service. You will see the following response in the browser. This means the rest-spring-example application has successfully made an event API call to the lambda-example application using the Event API endpoint. { \"id\": 1, \"name\": \"Simple PoJo class\", \"address\": \"100 World Blvd, Planet Earth\", \"date\": \"2023-03-27T23:17:19.257Z\", \"instance\": 6, \"seq\": 66, \"origin\": \"2023032791b6938a47614cf48779b1cf02fc89c4\" } To examine how the application makes the Event API call, please refer to the HelloPojoEventOverHttp class in the rest-spring-example. The class is extracted below: @Path(\"/pojo\") public class HelloPoJoEventOverHttp { @GET @Path(\"/http/{id}\") @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public void getPoJo(@PathParam(\"id\") Integer id, @Suspended AsyncResponse response) { AppConfigReader config = AppConfigReader.getInstance(); String remotePort = config.getProperty(\"lambda.example.port\", \"8085\"); String remoteEndpoint = \"http://127.0.0.1:\"+remotePort+\"/api/event\"; String traceId = Utility.getInstance().getUuid(); PostOffice po = new PostOffice(\"hello.pojo.endpoint\", traceId, \"GET /api/pojo/http\"); EventEnvelope req = new EventEnvelope().setTo(\"hello.pojo\").setHeader(\"id\", id); Future res = po.asyncRequest(req, 5000, Collections.emptyMap(), remoteEndpoint, true); res.onSuccess(event -> { // confirm that the PoJo object is transported correctly over the event stream system if (event.getBody() instanceof SamplePoJo) { response.resume(event.getBody()); } else { response.resume(new AppException(event.getStatus(), event.getError())); } }); res.onFailure(response::resume); } } The method signatures of the Event API is shown as follows:","title":"Test drive Event API"},{"location":"guides/CHAPTER-7/#asynchronous-api-java","text":"public Future asyncRequest(final EventEnvelope event, long timeout, Map headers, String eventEndpoint, boolean rpc) throws IOException;","title":"Asynchronous API (Java)"},{"location":"guides/CHAPTER-7/#sequential-non-blocking-api-kotlin-suspend-function","text":"suspend fun awaitRequest(request: EventEnvelope?, timeout: Long, headers: Map, eventEndpoint: String, rpc: Boolean): EventEnvelope Optionally, you may add security headers in the \"headers\" argument. e.g. the \"Authorization\" header. The eventEndpoint is a fully qualified URL. e.g. http://peer/api/event The \"rpc\" boolean value is set to true so that the response from the service of the peer application instance will be delivered. For drop-n-forget use case, you can set the \"rpc\" value to false. It will immediately return an HTTP-202 response.","title":"Sequential non-blocking API (Kotlin suspend function)"},{"location":"guides/CHAPTER-7/#event-over-http-using-configuration","text":"While you can call the \"Event-over-HTTP\" APIs programmatically, it would be more convenient to automate it with a configuration. This service abstraction means that user applications do not need to know where the target services are. You can enable Event-over-HTTP configuration by adding this parameter in application.properties: # # Optional event-over-http target maps # yaml.event.over.http=classpath:/event-over-http.yaml and then create the configuration file \"event-over-http.yaml\" like this: event: http: - route: 'event.http.test' target: 'http://127.0.0.1:${server.port}/api/event' # optional security headers headers: authorization: 'demo' - route: 'event.save.get' target: 'http://127.0.0.1:${server.port}/api/event' headers: authorization: 'demo' In the above example, there are two routes (event.http.test and event.save.get) with target URLs. If additional authentication is required for the peer's \"/api/event\" endpoint, you may add a set of security headers in each route. When you send asynchronous event or make a RPC call to \"event.save.get\" service, it will be forwarded to the peer's \"event-over-HTTP\" endpoint ( /api/event ) accordingly. You may also add variable references to the application.properties (or application.yaml) file, such as \"server.port\" in this example. Note: The configuration based \"event-over-HTTP\" feature does not support fork-n-join request API.","title":"Event-over-HTTP using configuration"},{"location":"guides/CHAPTER-7/#advantages","text":"The Event API exposes all public functions of an application instance to the network using a single REST endpoint. The advantages of Event API includes: Convenient - you do not need to write or configure individual endpoint for each public service Efficient - events are transported in binary format from one application to another Secure - you can protect the Event API endpoint with an authentication service The following configuration adds authentication service to the Event API endpoint: - service: [ \"event.api.service\" ] methods: [ 'POST' ] url: \"/api/event\" timeout: 60s authentication: \"v1.api.auth\" tracing: true This enforces every incoming request to the Event API endpoint to be authenticated by the \"v1.api.auth\" service before passing to the Event API service. You can plug in your own authentication service such as OAuth 2.0 \"bearer token\" validation. Please refer to Chapter-3 - REST automation for details. Chapter-6 Home Chapter-8 Spring Boot Table of Contents Service Mesh","title":"Advantages"},{"location":"guides/CHAPTER-8/","text":"Service Mesh Service mesh is a dedicated infrastructure layer to facilitate inter-container communication using \"sidecar\" and \"control plane\". Service mesh systems require additional administrative containers (PODs) for \"control plane\" and \"service discovery.\" The additional infrastructure requirements vary among products. Using kafka as a service mesh We will discuss using Kafka as a minimalist service mesh. Note: Service mesh is optional. You can use \"event over HTTP\" for inter-container communication if service mesh is not suitable. Typically, a service mesh system uses a \"side-car\" to sit next to the application container in the same POD to provide service discovery and network proxy services. Kafka is a network event stream system. We have implemented libraries for a few \"cloud connectors\" to support Kafka and Hazelcast as examples. Instead of using a side-car proxy, the system maintains a distributed routing table in each application instance. When a function requests the service of another function which is not in the same memory space, the \"cloud.connector\" module will bridge the event to the peer application through a network event system like Kafka. As shown in the following table, if \"service.1\" and \"service.2\" are in the same memory space of an application, they will communicate using the in-memory event bus. If they are in different applications and the applications are configured with Kafka, the two functions will communicate via the \"cloud.connector\" service. In-memory event bus Network event stream \"service.1\" -> \"service.2\" \"service.1\" -> \"cloud.connector\" -> \"service.2\" The system supports Kafka, Hazelcast out of the box. For example, to select kafka, you can configure application.properties like this: cloud.connector=kafka The \"cloud.connector\" parameter can be set to \"none\", \"kafka\" or \"hazelcast\". The default parameter of \"cloud.connector\" is \"none\". This means the application is not using any network event system \"connector\", thus running independently. Let's set up a minimalist service mesh with Kafka to see how it works. Set up a standalone Kafka server for development You need a Kafka cluster as the network event stream system. For development and testing, you can build and run a standalone Kafka server like this. Note that the mvn clean package command is optional because the executable JAR should be available after the mvn clean install command in Chapter-1 . cd connectors/adapters/kafka/kafka-standalone mvn clean package java -jar target/kafka-standalone-3.0.9.jar The standalone Kafka server will start at port 9092. You may adjust the \"server.properties\" in the standalone-kafka project when necessary. When the kafka server is started, it will create two temporary directories in the \"/tmp\" folder: \"/tmp/zookeeper\" \"/tmp/kafka-logs\" The kafka server is designed for development purpose only. The kafka and zookeeper data stores will be cleared when the server is restarted. Prepare the kafka-presence application The \"kafka-presence\" is a \"presence monitor\" application. It is a minimalist \"control plane\" in service mesh terminology. What is a presence monitor? A presence monitor is the control plane that assigns unique \"topic\" for each user application instance. It monitors the \"presence\" of each application. If an application fails or stops, the presence monitor will advertise the event to the rest of the system so that each application container will update its corresponding distributed routing table, thus bypassing the failed application and its services. If an application has more than one container instance deployed, they will work together to share load evenly. You will start the presence monitor like this: cd connectors/adapters/kafka/kafka-presence java -jar target/kafka-presence-3.0.9.jar By default, the kafka-connector will run at port 8080. Partial start-up log is shown below: AppStarter:344 - Modules loaded in 2,370 ms AppStarter:334 - Websocket server running on port-8080 ServiceLifeCycle:73 - service.monitor, partition 0 ready HouseKeeper:72 - Registered monitor (me) 2023032896b12f9de149459f9c8b71ad8b6b49fa The presence monitor will use the topic \"service.monitor\" to connect to the Kafka server and register itself as a presence monitor. Presence monitor is resilient. You can run more than one instance to back up each other. If you are not using Docker or Kubernetes, you need to change the \"server.port\" parameter of the second instance to 8081 so that the two application instances can run in the same laptop. Launch the rest-spring-2-example and lambda-example with kafka Let's run the rest-spring-2-example (rest-spring-3-example) and lambda-example applications with Kafka connector turned on. For demo purpose, the rest-spring-2-example and lambda-example are pre-configured with \"kafka-connector\". If you do not need these libraries, please remove them from the pom.xml built script. Since kafka-connector is pre-configured, we can start the two demo applications like this: cd examples/rest-spring-2-example java -Dcloud.connector=kafka -Dmandatory.health.dependencies=cloud.connector.health -jar target/rest-spring-2-example-3.0.9.jar cd examples/lambda-example java -Dcloud.connector=kafka -Dmandatory.health.dependencies=cloud.connector.health -jar target/lambda-example-3.0.9.jar The above command uses the \"-D\" parameters to configure the \"cloud.connector\" and \"mandatory.health.dependencies\". The parameter mandatory.health.dependencies=cloud.connector.health tells the system to turn on the health check endpoint for the application. For the rest-spring-2-example, the start-up log may look like this: AppStarter:344 - Modules loaded in 2,825 ms PresenceConnector:155 - Connected pc.abb4a4de.in, 127.0.0.1:8080, /ws/presence/202303282583899cf43a49b98f0522492b9ca178 EventConsumer:160 - Subscribed multiplex.0001.0 ServiceLifeCycle:73 - multiplex.0001, partition 0 ready This means that the rest-spring-2-example has successfully connected to the presence monitor at port 8080. It has subscribed to the topic \"multiplex.0001\" partition 0. For the lambda-example, the log may look like this: AppStarter:344 - Modules loaded in 2,742 m PresenceConnector:155 - Connected pc.991a2be0.in, 127.0.0.1:8080, /ws/presence/2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 EventConsumer:160 - Subscribed multiplex.0001.1 ServiceLifeCycle:73 - multiplex.0001, partition 1 ready ServiceRegistry:242 - Peer 202303282583899cf43a49b98f0522492b9ca178 joins (rest-spring-2-example 3.0.0) ServiceRegistry:383 - hello.world (rest-spring-2-example, WEB.202303282583899cf43a49b98f0522492b9ca178) registered You notice that the lambda-example has discovered the rest-spring-2-example through Kafka and added the \"hello.world\" to the distributed routing table. At this point, the rest-spring-2-example will find the lambda-example application as well: ServiceRegistry:242 - Peer 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 joins (lambda-example 3.0.0) ServiceRegistry:383 - hello.world (lambda-example, APP.2023032808d82ebe2c0d4e5aa9ca96b3813bdd25) registered ServiceRegistry:383 - hello.pojo (lambda-example, APP.2023032808d82ebe2c0d4e5aa9ca96b3813bdd25) registered This is real-time service discovery coordinated by the \"kafka-presence\" monitor application. Now you have created a minimalist event-driven service mesh. Send an event request from rest-spring-2-example to lambda-example In Chapter-7 , you have sent a request from the rest-spring-2-example to the lambda-example using \"Event over HTTP\" without a service mesh. In this section, you can make the same request using service mesh. Please point your browser to http://127.0.0.1:8083/api/pojo/mesh/1 You will see the following response in your browser. { \"id\": 1, \"name\": \"Simple PoJo class\", \"address\": \"100 World Blvd, Planet Earth\", \"date\": \"2023-03-28T17:53:41.696Z\", \"instance\": 1, \"seq\": 1, \"origin\": \"2023032808d82ebe2c0d4e5aa9ca96b3813bdd25\" } Presence monitor info endpoint You can check the service mesh status from the presence monitor's \"/info\" endpoint. You can visit http://127.0.0.1:8080/info and it will show something like this: { \"app\": { \"name\": \"kafka-presence\", \"description\": \"Presence Monitor\", \"version\": \"3.0.0\" }, \"personality\": \"RESOURCES\", \"additional_info\": { \"total\": { \"topics\": 2, \"virtual_topics\": 2, \"connections\": 2 }, \"topics\": [ \"multiplex.0001 (32)\", \"service.monitor (11)\" ], \"virtual_topics\": [ \"multiplex.0001-000 -> 202303282583899cf43a49b98f0522492b9ca178, rest-spring-2-example v3.0.0\", \"multiplex.0001-001 -> 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25, lambda-example v3.0.0\" ], \"connections\": [ { \"elapsed\": \"25 minutes 12 seconds\", \"created\": \"2023-03-28T17:43:13Z\", \"origin\": \"2023032808d82ebe2c0d4e5aa9ca96b3813bdd25\", \"name\": \"lambda-example\", \"topic\": \"multiplex.0001-001\", \"monitor\": \"2023032896b12f9de149459f9c8b71ad8b6b49fa\", \"type\": \"APP\", \"updated\": \"2023-03-28T18:08:25Z\", \"version\": \"3.0.0\", \"seq\": 65, \"group\": 1 }, { \"elapsed\": \"29 minutes 42 seconds\", \"created\": \"2023-03-28T17:38:47Z\", \"origin\": \"202303282583899cf43a49b98f0522492b9ca178\", \"name\": \"rest-spring-2-example\", \"topic\": \"multiplex.0001-000\", \"monitor\": \"2023032896b12f9de149459f9c8b71ad8b6b49fa\", \"type\": \"WEB\", \"updated\": \"2023-03-28T18:08:29Z\", \"version\": \"3.0.0\", \"seq\": 75, \"group\": 1 } ], \"monitors\": [ \"2023032896b12f9de149459f9c8b71ad8b6b49fa - 2023-03-28T18:08:46Z\" ] }, \"vm\": { \"java_vm_version\": \"18.0.2.1+1\", \"java_runtime_version\": \"18.0.2.1+1\", \"java_version\": \"18.0.2.1\" }, \"origin\": \"2023032896b12f9de149459f9c8b71ad8b6b49fa\", \"time\": { \"current\": \"2023-03-28T18:08:47.613Z\", \"start\": \"2023-03-28T17:31:23.611Z\" } } In this example, it shows that there are two user applications (rest-spring-2-example and lambda-example) connected. Presence monitor health endpoint The presence monitor has a \"/health\" endpoint. You can visit http://127.0.0.1:8080/health and it will show something like this: { \"upstream\": [ { \"route\": \"cloud.connector.health\", \"status_code\": 200, \"service\": \"kafka\", \"topics\": \"on-demand\", \"href\": \"127.0.0.1:9092\", \"message\": \"Loopback test took 3 ms; System contains 2 topics\", \"required\": true } ], \"origin\": \"2023032896b12f9de149459f9c8b71ad8b6b49fa\", \"name\": \"kafka-presence\", \"status\": \"UP\" } User application health endpoint Similarly, you can check the health status of the rest-spring-2-example application with http://127.0.0.1:8083/health { \"upstream\": [ { \"route\": \"cloud.connector.health\", \"status_code\": 200, \"service\": \"kafka\", \"topics\": \"on-demand\", \"href\": \"127.0.0.1:9092\", \"message\": \"Loopback test took 4 ms\", \"required\": true } ], \"origin\": \"202303282583899cf43a49b98f0522492b9ca178\", \"name\": \"rest-spring-example\", \"status\": \"UP\" } It looks similar to the health status of the presence monitor. However, only the presence monitor shows the total number of topics because it handles topic issuance to each user application instance. Actuator endpoints Additional actuator endpoints includes: library endpoint (\"/info/lib\") - you can check the packaged libraries for each application distributed routing table (\"/info/routes\") - this will display the distributed routing table for public functions environment (\"/env\") - it shows all functions (public and private) with number of workers. livenessproble (\"/livenessprobe\") - this should display \"OK\" to indicate the application is running Stop an application You can press \"control-C\" to stop an application. Let's stop the lambda-example application. Once you stopped lamdba-example from the command line, the rest-spring-2-example will detect it: ServiceRegistry:278 - Peer 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 left (lambda-example 3.0.0) ServiceRegistry:401 - hello.world 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 unregistered ServiceRegistry:401 - hello.pojo 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 unregistered The rest-spring-2-example will update its distributed routing table automatically. You will also find log messages in the kafka-presence application like this: MonitorService:120 - Member 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 left TopicController:250 - multiplex.0001-001 released by 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25, lambda-example, 3.0.0 When an application instance stops, the presence monitor will detect the event, remove it from the registry and release the topic associated with the disconnected application instance. The presence monitor is using the \"presence\" feature in websocket, thus we call it \"presence\" monitor. Chapter-7 Home CHAPTER-9 Event over HTTP Table of Contents API Overview","title":"Chapter-8"},{"location":"guides/CHAPTER-8/#service-mesh","text":"Service mesh is a dedicated infrastructure layer to facilitate inter-container communication using \"sidecar\" and \"control plane\". Service mesh systems require additional administrative containers (PODs) for \"control plane\" and \"service discovery.\" The additional infrastructure requirements vary among products.","title":"Service Mesh"},{"location":"guides/CHAPTER-8/#using-kafka-as-a-service-mesh","text":"We will discuss using Kafka as a minimalist service mesh. Note: Service mesh is optional. You can use \"event over HTTP\" for inter-container communication if service mesh is not suitable. Typically, a service mesh system uses a \"side-car\" to sit next to the application container in the same POD to provide service discovery and network proxy services. Kafka is a network event stream system. We have implemented libraries for a few \"cloud connectors\" to support Kafka and Hazelcast as examples. Instead of using a side-car proxy, the system maintains a distributed routing table in each application instance. When a function requests the service of another function which is not in the same memory space, the \"cloud.connector\" module will bridge the event to the peer application through a network event system like Kafka. As shown in the following table, if \"service.1\" and \"service.2\" are in the same memory space of an application, they will communicate using the in-memory event bus. If they are in different applications and the applications are configured with Kafka, the two functions will communicate via the \"cloud.connector\" service. In-memory event bus Network event stream \"service.1\" -> \"service.2\" \"service.1\" -> \"cloud.connector\" -> \"service.2\" The system supports Kafka, Hazelcast out of the box. For example, to select kafka, you can configure application.properties like this: cloud.connector=kafka The \"cloud.connector\" parameter can be set to \"none\", \"kafka\" or \"hazelcast\". The default parameter of \"cloud.connector\" is \"none\". This means the application is not using any network event system \"connector\", thus running independently. Let's set up a minimalist service mesh with Kafka to see how it works.","title":"Using kafka as a service mesh"},{"location":"guides/CHAPTER-8/#set-up-a-standalone-kafka-server-for-development","text":"You need a Kafka cluster as the network event stream system. For development and testing, you can build and run a standalone Kafka server like this. Note that the mvn clean package command is optional because the executable JAR should be available after the mvn clean install command in Chapter-1 . cd connectors/adapters/kafka/kafka-standalone mvn clean package java -jar target/kafka-standalone-3.0.9.jar The standalone Kafka server will start at port 9092. You may adjust the \"server.properties\" in the standalone-kafka project when necessary. When the kafka server is started, it will create two temporary directories in the \"/tmp\" folder: \"/tmp/zookeeper\" \"/tmp/kafka-logs\" The kafka server is designed for development purpose only. The kafka and zookeeper data stores will be cleared when the server is restarted.","title":"Set up a standalone Kafka server for development"},{"location":"guides/CHAPTER-8/#prepare-the-kafka-presence-application","text":"The \"kafka-presence\" is a \"presence monitor\" application. It is a minimalist \"control plane\" in service mesh terminology. What is a presence monitor? A presence monitor is the control plane that assigns unique \"topic\" for each user application instance. It monitors the \"presence\" of each application. If an application fails or stops, the presence monitor will advertise the event to the rest of the system so that each application container will update its corresponding distributed routing table, thus bypassing the failed application and its services. If an application has more than one container instance deployed, they will work together to share load evenly. You will start the presence monitor like this: cd connectors/adapters/kafka/kafka-presence java -jar target/kafka-presence-3.0.9.jar By default, the kafka-connector will run at port 8080. Partial start-up log is shown below: AppStarter:344 - Modules loaded in 2,370 ms AppStarter:334 - Websocket server running on port-8080 ServiceLifeCycle:73 - service.monitor, partition 0 ready HouseKeeper:72 - Registered monitor (me) 2023032896b12f9de149459f9c8b71ad8b6b49fa The presence monitor will use the topic \"service.monitor\" to connect to the Kafka server and register itself as a presence monitor. Presence monitor is resilient. You can run more than one instance to back up each other. If you are not using Docker or Kubernetes, you need to change the \"server.port\" parameter of the second instance to 8081 so that the two application instances can run in the same laptop.","title":"Prepare the kafka-presence application"},{"location":"guides/CHAPTER-8/#launch-the-rest-spring-2-example-and-lambda-example-with-kafka","text":"Let's run the rest-spring-2-example (rest-spring-3-example) and lambda-example applications with Kafka connector turned on. For demo purpose, the rest-spring-2-example and lambda-example are pre-configured with \"kafka-connector\". If you do not need these libraries, please remove them from the pom.xml built script. Since kafka-connector is pre-configured, we can start the two demo applications like this: cd examples/rest-spring-2-example java -Dcloud.connector=kafka -Dmandatory.health.dependencies=cloud.connector.health -jar target/rest-spring-2-example-3.0.9.jar cd examples/lambda-example java -Dcloud.connector=kafka -Dmandatory.health.dependencies=cloud.connector.health -jar target/lambda-example-3.0.9.jar The above command uses the \"-D\" parameters to configure the \"cloud.connector\" and \"mandatory.health.dependencies\". The parameter mandatory.health.dependencies=cloud.connector.health tells the system to turn on the health check endpoint for the application. For the rest-spring-2-example, the start-up log may look like this: AppStarter:344 - Modules loaded in 2,825 ms PresenceConnector:155 - Connected pc.abb4a4de.in, 127.0.0.1:8080, /ws/presence/202303282583899cf43a49b98f0522492b9ca178 EventConsumer:160 - Subscribed multiplex.0001.0 ServiceLifeCycle:73 - multiplex.0001, partition 0 ready This means that the rest-spring-2-example has successfully connected to the presence monitor at port 8080. It has subscribed to the topic \"multiplex.0001\" partition 0. For the lambda-example, the log may look like this: AppStarter:344 - Modules loaded in 2,742 m PresenceConnector:155 - Connected pc.991a2be0.in, 127.0.0.1:8080, /ws/presence/2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 EventConsumer:160 - Subscribed multiplex.0001.1 ServiceLifeCycle:73 - multiplex.0001, partition 1 ready ServiceRegistry:242 - Peer 202303282583899cf43a49b98f0522492b9ca178 joins (rest-spring-2-example 3.0.0) ServiceRegistry:383 - hello.world (rest-spring-2-example, WEB.202303282583899cf43a49b98f0522492b9ca178) registered You notice that the lambda-example has discovered the rest-spring-2-example through Kafka and added the \"hello.world\" to the distributed routing table. At this point, the rest-spring-2-example will find the lambda-example application as well: ServiceRegistry:242 - Peer 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 joins (lambda-example 3.0.0) ServiceRegistry:383 - hello.world (lambda-example, APP.2023032808d82ebe2c0d4e5aa9ca96b3813bdd25) registered ServiceRegistry:383 - hello.pojo (lambda-example, APP.2023032808d82ebe2c0d4e5aa9ca96b3813bdd25) registered This is real-time service discovery coordinated by the \"kafka-presence\" monitor application. Now you have created a minimalist event-driven service mesh.","title":"Launch the rest-spring-2-example and lambda-example with kafka"},{"location":"guides/CHAPTER-8/#send-an-event-request-from-rest-spring-2-example-to-lambda-example","text":"In Chapter-7 , you have sent a request from the rest-spring-2-example to the lambda-example using \"Event over HTTP\" without a service mesh. In this section, you can make the same request using service mesh. Please point your browser to http://127.0.0.1:8083/api/pojo/mesh/1 You will see the following response in your browser. { \"id\": 1, \"name\": \"Simple PoJo class\", \"address\": \"100 World Blvd, Planet Earth\", \"date\": \"2023-03-28T17:53:41.696Z\", \"instance\": 1, \"seq\": 1, \"origin\": \"2023032808d82ebe2c0d4e5aa9ca96b3813bdd25\" }","title":"Send an event request from rest-spring-2-example to lambda-example"},{"location":"guides/CHAPTER-8/#presence-monitor-info-endpoint","text":"You can check the service mesh status from the presence monitor's \"/info\" endpoint. You can visit http://127.0.0.1:8080/info and it will show something like this: { \"app\": { \"name\": \"kafka-presence\", \"description\": \"Presence Monitor\", \"version\": \"3.0.0\" }, \"personality\": \"RESOURCES\", \"additional_info\": { \"total\": { \"topics\": 2, \"virtual_topics\": 2, \"connections\": 2 }, \"topics\": [ \"multiplex.0001 (32)\", \"service.monitor (11)\" ], \"virtual_topics\": [ \"multiplex.0001-000 -> 202303282583899cf43a49b98f0522492b9ca178, rest-spring-2-example v3.0.0\", \"multiplex.0001-001 -> 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25, lambda-example v3.0.0\" ], \"connections\": [ { \"elapsed\": \"25 minutes 12 seconds\", \"created\": \"2023-03-28T17:43:13Z\", \"origin\": \"2023032808d82ebe2c0d4e5aa9ca96b3813bdd25\", \"name\": \"lambda-example\", \"topic\": \"multiplex.0001-001\", \"monitor\": \"2023032896b12f9de149459f9c8b71ad8b6b49fa\", \"type\": \"APP\", \"updated\": \"2023-03-28T18:08:25Z\", \"version\": \"3.0.0\", \"seq\": 65, \"group\": 1 }, { \"elapsed\": \"29 minutes 42 seconds\", \"created\": \"2023-03-28T17:38:47Z\", \"origin\": \"202303282583899cf43a49b98f0522492b9ca178\", \"name\": \"rest-spring-2-example\", \"topic\": \"multiplex.0001-000\", \"monitor\": \"2023032896b12f9de149459f9c8b71ad8b6b49fa\", \"type\": \"WEB\", \"updated\": \"2023-03-28T18:08:29Z\", \"version\": \"3.0.0\", \"seq\": 75, \"group\": 1 } ], \"monitors\": [ \"2023032896b12f9de149459f9c8b71ad8b6b49fa - 2023-03-28T18:08:46Z\" ] }, \"vm\": { \"java_vm_version\": \"18.0.2.1+1\", \"java_runtime_version\": \"18.0.2.1+1\", \"java_version\": \"18.0.2.1\" }, \"origin\": \"2023032896b12f9de149459f9c8b71ad8b6b49fa\", \"time\": { \"current\": \"2023-03-28T18:08:47.613Z\", \"start\": \"2023-03-28T17:31:23.611Z\" } } In this example, it shows that there are two user applications (rest-spring-2-example and lambda-example) connected.","title":"Presence monitor info endpoint"},{"location":"guides/CHAPTER-8/#presence-monitor-health-endpoint","text":"The presence monitor has a \"/health\" endpoint. You can visit http://127.0.0.1:8080/health and it will show something like this: { \"upstream\": [ { \"route\": \"cloud.connector.health\", \"status_code\": 200, \"service\": \"kafka\", \"topics\": \"on-demand\", \"href\": \"127.0.0.1:9092\", \"message\": \"Loopback test took 3 ms; System contains 2 topics\", \"required\": true } ], \"origin\": \"2023032896b12f9de149459f9c8b71ad8b6b49fa\", \"name\": \"kafka-presence\", \"status\": \"UP\" }","title":"Presence monitor health endpoint"},{"location":"guides/CHAPTER-8/#user-application-health-endpoint","text":"Similarly, you can check the health status of the rest-spring-2-example application with http://127.0.0.1:8083/health { \"upstream\": [ { \"route\": \"cloud.connector.health\", \"status_code\": 200, \"service\": \"kafka\", \"topics\": \"on-demand\", \"href\": \"127.0.0.1:9092\", \"message\": \"Loopback test took 4 ms\", \"required\": true } ], \"origin\": \"202303282583899cf43a49b98f0522492b9ca178\", \"name\": \"rest-spring-example\", \"status\": \"UP\" } It looks similar to the health status of the presence monitor. However, only the presence monitor shows the total number of topics because it handles topic issuance to each user application instance.","title":"User application health endpoint"},{"location":"guides/CHAPTER-8/#actuator-endpoints","text":"Additional actuator endpoints includes: library endpoint (\"/info/lib\") - you can check the packaged libraries for each application distributed routing table (\"/info/routes\") - this will display the distributed routing table for public functions environment (\"/env\") - it shows all functions (public and private) with number of workers. livenessproble (\"/livenessprobe\") - this should display \"OK\" to indicate the application is running","title":"Actuator endpoints"},{"location":"guides/CHAPTER-8/#stop-an-application","text":"You can press \"control-C\" to stop an application. Let's stop the lambda-example application. Once you stopped lamdba-example from the command line, the rest-spring-2-example will detect it: ServiceRegistry:278 - Peer 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 left (lambda-example 3.0.0) ServiceRegistry:401 - hello.world 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 unregistered ServiceRegistry:401 - hello.pojo 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 unregistered The rest-spring-2-example will update its distributed routing table automatically. You will also find log messages in the kafka-presence application like this: MonitorService:120 - Member 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25 left TopicController:250 - multiplex.0001-001 released by 2023032808d82ebe2c0d4e5aa9ca96b3813bdd25, lambda-example, 3.0.0 When an application instance stops, the presence monitor will detect the event, remove it from the registry and release the topic associated with the disconnected application instance. The presence monitor is using the \"presence\" feature in websocket, thus we call it \"presence\" monitor. Chapter-7 Home CHAPTER-9 Event over HTTP Table of Contents API Overview","title":"Stop an application"},{"location":"guides/CHAPTER-9/","text":"API Overview Main application Each application has an entry point. You may implement an entry point in a main application like this: @MainApplication public class MainApp implements EntryPoint { public static void main(String[] args) { AutoStart.main(args); } @Override public void start(String[] args) { // your startup logic here log.info(\"Started\"); } } In your main application, you will implement the EntryPoint interface to override the \"start\" method. Typically, a main application is used to initiate some application start up procedure. In some case when your application does not need any start up logic, you can just print a message to indicate that your application has started. You may want to keep the static \"main\" method which can be used to run your application inside an IDE. The pom.xml build script is designed to run the AppStarter start up function that will execute your main application's start method. In some case, your application may have more than one main application module. You can decide the sequence of execution using the \"sequence\" parameter in the MainApplication annotation. The module with the smallest sequence number will run first. Optional environment setup before MainApplication Sometimes, it may be required to set up some environment configuration before your main application starts. You can implement a BeforeApplication module. Its syntax is similar to the MainApplication . @BeforeApplication public class EnvSetup implements EntryPoint { @Override public void start(String[] args) { // your environment setup logic here log.info(\"initialized\"); } } The BeforeApplication logic will run before your MainApplication module. This is useful when you want to do special handling of environment variables. For example, decrypt an environment variable secret, construct an X.509 certificate, and save it in the \"/tmp\" folder before your main application starts. Event envelope Mercury version 3 is an event engine that encapsulates Eclipse Vertx and Kotlin coroutine and suspend function. A composable application is a collection of functions that communicate with each other in events. Each event is transported by an event envelope. Let's examine the envelope. There are 3 elements in an event envelope: Element Type Purpose 1 metadata Includes unique ID, target function name, reply address correlation ID, status, exception, trace ID and path 2 headers User defined key-value pairs 3 body Event payload (primitive, hash map or PoJo) Headers and body are optional, but you must provide at least one of them. If the envelope do not have any headers or body, the system will send your event as a \"ping\" command to the target function. The response acknowledgements that the target function exists. This ping/pong protocol tests the event loop or service mesh. This test mechanism is useful for DevSecOps admin dashboard. Custom exception using AppException To reject an incoming request, you can throw an AppException like this: // example-1 throw new AppException(400, \"My custom error message\"); // example-2 throw new AppException(400, \"My custom error message\", ex); Example-1 - a simple exception with status code (400) and an error message Example-2 - includes a nested exception As a best practice, we recommend using error codes that are compatible with HTTP status codes. Defining a user function in Java You can write a function in Java like this: @PreLoad(route = \"hello.simple\", instances = 10) public class SimpleDemoEndpoint implements TypedLambdaFunction { @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { // business logic here return result; } } By default, a Java function will run as a coroutine. To tell the system that you want to run the function using kernel thread pool, you can add the KernelThreadRunner annotation. The PreLoad annotation tells the system to preload the function into memory and register it into the event loop. You must provide a \"route name\" and configure the number of concurrent workers (\"instances\"). Route name is used by the event loop to find your function in memory. A route name must use lower letters and numbers, and it must have at least one dot as a word separator. e.g. \"hello.simple\" is a proper route name but \"HelloSimple\" is not. You can implement your function using the LambdaFunction or TypedLambdaFunction. The latter allows you to define the input and output classes. The system will map the event body into the input argument and the event headers into the headers argument. The instance argument informs your function which worker is serving the current request. Similarly, you can also write a \"suspend function\" in Kotlin like this: @PreLoad(route = \"hello.world\", instances = 10, isPrivate = false, envInstances = \"instances.hello.world\") class HelloWorld : KotlinLambdaFunction> { @Throws(Exception::class) override suspend fun handleEvent(headers: Map, input: Any?, instance: Int): Map { // business logic here return result; } } In the suspend function example above, you may notice the optional envInstances parameter. This tells the system to use a parameter from the application.properties (or application.yml) to configure the number of workers for the function. When the parameter defined in \"envInstances\" is not found, the \"instances\" parameter is used as the default value. Inspect event metadata There are some reserved metadata for route name (\"my_route\"), trace ID (\"my_trace_id\") and trace path (\"my_trace_path\") in the \"headers\" argument. They do not exist in the incoming event envelope. Instead, the system automatically insert them as read-only metadata. They are used when your code want to obtain an instance of PostOffice or FastRPC. To inspect all metadata, you can declare the input as \"EventEnvelope\". The system will map the whole event envelope into the \"input\" argument. You can retrieve the replyTo address and other useful metadata. Note that the \"replyTo\" address is optional. It only exists when the caller is making an RPC call to your function. If the caller sends an asynchronous request, the \"replyTo\" value is null. Platform API You can obtain a singleton instance of the Platform object to do the following: Register a function We recommend using the PreLoad annotation in a class to declare the function route name, number of worker instances and whether the function is public or private. In some use cases where you want to create and destroy functions on demand, you can register them programmatically. In the following example, it registers \"my.function\" using the MyFunction class as a public function and \"another.function\" with the AnotherFunction class as a private function. It then registers two kotlin functions in public and private scope respectively. Platform platform = Platform.getInstance(); // register a public function platform.register(\"my.function\", new MyFunction(), 10); // register a private function platform.registerPrivate(\"another.function\", new AnotherFunction(), 20); // register a public suspend function platform.registerKoltin(\"my.suspend.function\", new MySuspendFunction(), 10); // register a private suspend function platform.registerKoltinPrivate(\"another.suspend.function\", new AnotherSuspendFunction(), 10); What is a public function? A public function is visible by any application instances in the same network. When a function is declared as \"public\", the function is reachable through the EventAPI REST endpoint or a service mesh. A private function is invisible outside the memory space of the application instance that it resides. This allows application to encapsulate business logic according to domain boundary. You can assemble closely related functions as a composable application that can be deployed independently. Release a function In some use cases, you want to release a function on-demand when it is no longer required. platform.release(\"another.function\"); The above API will unload the function from memory and release it from the \"event loop\". Check if a function is available You can check if a function with the named route has been deployed. if (platform.hasRoute(\"another.function\")) { // do something } Wait for a function to be ready Functions are registered asynchronously. For functions registered using the PreLoad annotation, they are available to your application when the MainApplication starts. For functions that are registered on-demand, you can wait for the function to get ready like this: Future status = platform.waitForProvider(\"cloud.connector\", 10); status.onSuccess(ready -> { // business logic when \"cloud.connector\" is ready }); Note that the \"onFailure\" method is not required. The onSuccess will return true or false. In the above example, your application waits for up to 10 seconds. If the function (i.e. the \"provider\") is available, the API will invoke the \"onSuccess\" method immediately. Obtain the unique application instance ID When an application instance starts, a unique ID is generated. We call this the \"Origin ID\". String originId = po.getOrigin(); When running the application in a minimalist service mesh using Kafka or similar network event stream system, the origin ID is used to uniquely identify the application instance. The origin ID is automatically appended to the \"replyTo\" address when making a RPC call over a network event stream so that the system can send the response event back to the \"originator\" or \"calling\" application instance. Set application personality An application may have one of the following personality: REST - the deployed application is user facing APP - the deployed application serves business logic RESOURCES - this is a resource-tier service. e.g. database service, MQ gateway, legacy service proxy, utility, etc. You can change the application personality like this: // the default value is \"APP\" ServerPersonality.getInstance().setType(ServerPersonality.Type.REST); The personality setting is for documentation purpose only. It does not affect the behavior of your application. It will appear in the application \"/info\" endpoint. PostOffice API You can obtain an instance of the PostOffice from the input \"headers\" and \"instance\" parameters in the input arguments of your function. PostOffice po = new PostOffice(headers, instance); The PostOffice is the event manager that you can use to send asynchronous events or to make RPC requests. The constructor uses the READ only metadata in the \"headers\" argument in the \"handleEvent\" method of your function. Send an asynchronous event to a function You can send an asynchronous event like this. // example-1 po.send(\"another.function\", \"test message\"); // example-2 po.send(\"another.function\", new Kv(\"some_key\", \"some_value\"), new kv(\"another_key\", \"another_value\")); // example-3 po.send(\"another.function\", somePoJo, new Kv(\"some_key\", \"some_value\")); // example-4 EventEnvelope event = new EventEnvelope().setTo(\"another.function\") .setHeader(\"some_key\", \"some_value\").setBody(somePoJo); po.send(event) // example-5 po.sendLater(event, new Date(System.currentTimeMillis() + 5000)); Example-1 sends the text string \"test message\" to the target service named \"another.function\". Example-2 sends two key-values as \"headers\" parameters to the same service. Example-3 sends a PoJo and a key-value pair to the same service. Example-4 is the same as example-3. It is using an EventEnvelope to construct the request. Example-5 schedules an event to be sent 5 seconds later. The first 3 APIs are convenient methods and the system will automatically create an EventEnvelope to hold the target route name, key-values and/or event payload. Make an asynchronous RPC call You can make RPC call like this: // example-1 EventEnvelope request = new EventEnvelope().setTo(\"another.function\") .setHeader(\"some_key\", \"some_value\").setBody(somePoJo); Future response = po.asyncRequest(request, 5000); response.onSuccess(result -> { // result is the response event }); response.onFailure(e -> { // handle timeout exception }); // example-2 Future response = po.asyncRequest(request, 5000, false); response.onSuccess(result -> { // result is the response event // Timeout exception is returned as a response event with status=408 }); // example-3 with the \"rpc\" boolean parameter set to true Future response = po.asyncRequest(request, 5000, \"http://peer/api/event\", true); response.onSuccess(result -> { // result is the response event }); response.onFailure(e -> { // handle timeout exception }); Example-1 makes a RPC call with a 5-second timeout to \"another.function\". Example-2 sets the \"timeoutException\" to false, telling system to return timeout exception as a regular event. Example-3 makes an \"event over HTTP\" RPC call to \"another.function\" in another application instance called \"peer\". \"Event over HTTP\" is an important topic. Please refer to Chapter 7 for more details. Perform a fork-n-join RPC call to multiple functions In a similar fashion, you can make a fork-n-join call that sends request events in parallel to more than one function. // example-1 EventEnvelope request1 = new EventEnvelope().setTo(\"this.function\") .setHeader(\"hello\", \"world\").setBody(\"test message\"); EventEnvelope request2 = new EventEnvelope().setTo(\"that.function\") .setHeader(\"good\", \"day\").setBody(somePoJo); List requests = new ArrayList<>(); requests.add(request1); requests.add(request2); Future> responses = po.asyncRequest(requests, 5000); response.onSuccess(results -> { // results contains the response events }); response.onFailure(e -> { // handle timeout exception }); // example-2 Future> responses = po.asyncRequest(requests, 5000, false); response.onSuccess(results -> { // results contains the response events. // Partial result list is returned if one or more functions did not respond. }); Make a sequential non-blocking RPC call You can make a sequential non-blocking RPC call from one function to another. The FastRPC is similar to the PostOffice. It is the event manager for KotlinLambdaFunction. You can create an instance of the FastRPC using the \"headers\" parameters in the input arguments of your function. val fastRPC = new FastRPC(headers) val request = EventEnvelope().setTo(\"another.function\") .setHeader(\"some_key\", \"some_value\").setBody(somePoJo) // example-1 val response = fastRPC.awaitRequest(request, 5000) // handle the response event // example-2 with the \"rpc\" boolean parameter set to true val response = fastRPC.awaitRequest(request, 5000, \"http://peer/api/event\", true) // handle the response event Example-1 performs a non-blocking RPC call Example-2 makes a non-blocking \"Event Over HTTP\" RPC call Note that timeout exception is returned as a regular event with status 408. Sequential non-blocking code is easier to read. Moreover, it handles more concurrent users and requests without consuming a lot of CPU resources because it is \"suspended\" while waiting for a response from another function. Perform a sequential non-blocking fork-n-join call to multiple functions You can make a sequential non-blocking fork-n-join call using the FastRPC API like this: val fastRPC = FastRPC(headers) val template = EventEnvelope().setTo(\"hello.world\").setHeader(\"someKey\", \"someValue\") val requests = ArrayList() // create a list of 4 request events for (i in 0..3) { requests.add(EventEnvelope(template.toBytes()).setBody(i).setCorrelationId(\"cid-$i\")) } val responses: List = fastRPC.awaitRequest(requests, 5000) // handle the response events In the above example, the function creates a list of request events from a template event with target service \"hello.world\". It sets the number 0 to 3 to the individual events with unique correlation IDs. The response events contain the same set of correlation IDs so that your business logic can decide how to handle individual response event. The result may be a partial list of response events if one or more functions failed to respond on time. Check if a function with a named route exists The PostOffice provides the \"exists()\" method that is similar to the \"platform.hasRoute()\" command. The difference is that the \"exists()\" method can discover functions of another application instance when running in the \"service mesh\" mode. If your application is not deployed in a service mesh, the PostOffice's \"exists\" and Platform's \"hasRoute\" APIs will provide the same result. boolean found = po.exists(\"another.function\"); if (found) { // do something } Retrieve trace ID and path If you want to know the route name and optional trace ID and path, you can use the following APIs. For example, if tracing is enabled, the trace ID will be available. You can put the trace ID in application log messages. This would group log messages of the same transaction together when you search the trace ID from a centralized logging dashboard such as Splunk. String myRoute = po.getRoute(); String traceId = po.getTraceId(); String tracePath = po.getTracePath(); Trace annotation You can use the PostOffice instance to annotate a trace in your function like this: // annotate a trace with the key-value \"hello:world\" po.annotateTrace(\"hello\", \"world\"); This is useful when you want to attach transaction specific information in the performance metrics. For example, the traces may be used in production transaction analytics. IMPORTANT: do not annotate sensitive or secret information such as PII, PHI, PCI data because the trace is visible in application log. It may also be forwarded to a centralized telemetry dashboard. Configuration API Your function can access the main application configuration from the platform like this: AppConfigReader config = AppConfigReader.getInstance(); // the value can be string or a primitive Object value = config.get('my.parameter'); // the return value will be converted to a string String text = config.getProperty('my.parameter'); The system uses the standard dot-bracket format for a parameter name. e.g. \"hello.world\", \"some.key[2]\" You can override the main application configuration at run-time using the Java argument \"-D\". e.g. \"java -Dserver.port=8080 -jar myApp.jar\" Additional configuration files can be added with the ConfigReader API like this: // filePath should have location prefix \"classpath:/\" or \"file:/\" ConfigReader reader = new ConfigReader(); reader.load(filePath); The configuration system supports environment variable or reference to the main application configuration using the dollar-bracket syntax ${reference:default_value} . e.g. \"some.key=${MY_ENV_VARIABLE}\", \"some.key=${my.key}\" Custom serializer We are using GSON as the underlying serializer to handle common use cases. However, there may be situation that you want to use your own custom serialization library. To do that, you may write a serializer that implements the CustomSerializer interface: public interface CustomSerializer { public Map toMap(Object obj); public T toPoJo(Object obj, Class toValueType); } You may configure a user function to use a custom serializer by adding the \"customSerializer\" parameter in the PreLoad annotation. For example, @PreLoad(route=\"my.user.function\", customSerializer = JacksonSerializer.class) public class MyUserFunction implements TypedLambdaFunction { @Override public SimplePoJo handleEvent(Map headers, SimplePoJo input, int instance) { return input; } } If you register your function dynamically in code, you can use the following platform API to assign a custom serializer. public void setCustomSerializer(String route, CustomSerializer mapper); // e.g. // platform.setCustomSerializer(\"my.function\", new JacksonSerializer()); If you use the PostOffice to programmatically send event or make event RPC call and you need custom serializer, you can create a PostOffice instance like this: // this should be the first statement in the \"handleEvent\" method. PostOffice po = new PostOffice(headers, instance, new MyCustomSerializer()); The outgoing event using the PostOffice will use the custom serializer automatically. To interpret an event response from a RPC call, you can use the following PostOffice API: MyPoJo result = po.getResponseBodyAsPoJo(responseEvent, MyPoJo.class); Minimalist API design for event orchestration As a best practice, we advocate a minimalist approach in API integration. To build powerful composable applications, the above set of APIs is sufficient to perform \"event orchestration\" where you write code to coordinate how the various functions work together as a single \"executable\". Please refer to Chapter-4 for more details about event orchestration. Since Mercury is used in production installations, we will exercise the best effort to keep the core API stable. Other APIs in the toolkits are used internally to build the engine itself, and they may change from time to time. They are mostly convenient methods and utilities. The engine is fully encapsulated and any internal API changes are not likely to impact your applications. Optional Event Scripting To further reduce coding effort, you can perform \"event orchestration\" by configuration using \"Event Script\". It is available as an enterprise add-on module from Accenture. Co-existence with other development frameworks Mercury libraries are designed to co-exist with your favorite frameworks and tools. Inside a class implementing the LambdaFunction , TypedLambdaFunction or KotlinLambdaFunction , you can use any coding style and frameworks as you like, including sequential, object-oriented and reactive programming styles. Mercury version 3 has a built-in lightweight non-blocking HTTP server, but you can also use Spring Boot and other application server framework with it. A sample Spring Boot integration is provided in the \"rest-spring\" project. It is an optional feature, and you can decide to use a regular Spring Boot application with Mercury or to pick the customized Spring Boot in the \"rest-spring\" library. Template application for quick start You can use the lambda-example project as a template to start writing your own applications. It is preconfigured to support kernel threads, coroutine and suspend function. Source code update frequency This project is licensed under the Apache 2.0 open sources license. We will update the public codebase after it passes regression tests and meets stability and performance benchmarks in our production systems. Mercury is developed as an engine for you to build the latest cloud native and composable applications. While we are updating the technology frequently, the essential internals and the core APIs are stable. We are monitoring the progress of the upcoming Java 19 Virtual Thread feature and will include it in our API when it becomes officially available. Technical support For enterprise clients, optional technical support is available. Please contact your Accenture representative for details. Chapter-8 Home Chapter-10 Service Mesh Table of Contents Migration Guide","title":"Chapter-9"},{"location":"guides/CHAPTER-9/#api-overview","text":"","title":"API Overview"},{"location":"guides/CHAPTER-9/#main-application","text":"Each application has an entry point. You may implement an entry point in a main application like this: @MainApplication public class MainApp implements EntryPoint { public static void main(String[] args) { AutoStart.main(args); } @Override public void start(String[] args) { // your startup logic here log.info(\"Started\"); } } In your main application, you will implement the EntryPoint interface to override the \"start\" method. Typically, a main application is used to initiate some application start up procedure. In some case when your application does not need any start up logic, you can just print a message to indicate that your application has started. You may want to keep the static \"main\" method which can be used to run your application inside an IDE. The pom.xml build script is designed to run the AppStarter start up function that will execute your main application's start method. In some case, your application may have more than one main application module. You can decide the sequence of execution using the \"sequence\" parameter in the MainApplication annotation. The module with the smallest sequence number will run first.","title":"Main application"},{"location":"guides/CHAPTER-9/#optional-environment-setup-before-mainapplication","text":"Sometimes, it may be required to set up some environment configuration before your main application starts. You can implement a BeforeApplication module. Its syntax is similar to the MainApplication . @BeforeApplication public class EnvSetup implements EntryPoint { @Override public void start(String[] args) { // your environment setup logic here log.info(\"initialized\"); } } The BeforeApplication logic will run before your MainApplication module. This is useful when you want to do special handling of environment variables. For example, decrypt an environment variable secret, construct an X.509 certificate, and save it in the \"/tmp\" folder before your main application starts.","title":"Optional environment setup before MainApplication"},{"location":"guides/CHAPTER-9/#event-envelope","text":"Mercury version 3 is an event engine that encapsulates Eclipse Vertx and Kotlin coroutine and suspend function. A composable application is a collection of functions that communicate with each other in events. Each event is transported by an event envelope. Let's examine the envelope. There are 3 elements in an event envelope: Element Type Purpose 1 metadata Includes unique ID, target function name, reply address correlation ID, status, exception, trace ID and path 2 headers User defined key-value pairs 3 body Event payload (primitive, hash map or PoJo) Headers and body are optional, but you must provide at least one of them. If the envelope do not have any headers or body, the system will send your event as a \"ping\" command to the target function. The response acknowledgements that the target function exists. This ping/pong protocol tests the event loop or service mesh. This test mechanism is useful for DevSecOps admin dashboard.","title":"Event envelope"},{"location":"guides/CHAPTER-9/#custom-exception-using-appexception","text":"To reject an incoming request, you can throw an AppException like this: // example-1 throw new AppException(400, \"My custom error message\"); // example-2 throw new AppException(400, \"My custom error message\", ex); Example-1 - a simple exception with status code (400) and an error message Example-2 - includes a nested exception As a best practice, we recommend using error codes that are compatible with HTTP status codes.","title":"Custom exception using AppException"},{"location":"guides/CHAPTER-9/#defining-a-user-function-in-java","text":"You can write a function in Java like this: @PreLoad(route = \"hello.simple\", instances = 10) public class SimpleDemoEndpoint implements TypedLambdaFunction { @Override public Object handleEvent(Map headers, AsyncHttpRequest input, int instance) { // business logic here return result; } } By default, a Java function will run as a coroutine. To tell the system that you want to run the function using kernel thread pool, you can add the KernelThreadRunner annotation. The PreLoad annotation tells the system to preload the function into memory and register it into the event loop. You must provide a \"route name\" and configure the number of concurrent workers (\"instances\"). Route name is used by the event loop to find your function in memory. A route name must use lower letters and numbers, and it must have at least one dot as a word separator. e.g. \"hello.simple\" is a proper route name but \"HelloSimple\" is not. You can implement your function using the LambdaFunction or TypedLambdaFunction. The latter allows you to define the input and output classes. The system will map the event body into the input argument and the event headers into the headers argument. The instance argument informs your function which worker is serving the current request. Similarly, you can also write a \"suspend function\" in Kotlin like this: @PreLoad(route = \"hello.world\", instances = 10, isPrivate = false, envInstances = \"instances.hello.world\") class HelloWorld : KotlinLambdaFunction> { @Throws(Exception::class) override suspend fun handleEvent(headers: Map, input: Any?, instance: Int): Map { // business logic here return result; } } In the suspend function example above, you may notice the optional envInstances parameter. This tells the system to use a parameter from the application.properties (or application.yml) to configure the number of workers for the function. When the parameter defined in \"envInstances\" is not found, the \"instances\" parameter is used as the default value.","title":"Defining a user function in Java"},{"location":"guides/CHAPTER-9/#inspect-event-metadata","text":"There are some reserved metadata for route name (\"my_route\"), trace ID (\"my_trace_id\") and trace path (\"my_trace_path\") in the \"headers\" argument. They do not exist in the incoming event envelope. Instead, the system automatically insert them as read-only metadata. They are used when your code want to obtain an instance of PostOffice or FastRPC. To inspect all metadata, you can declare the input as \"EventEnvelope\". The system will map the whole event envelope into the \"input\" argument. You can retrieve the replyTo address and other useful metadata. Note that the \"replyTo\" address is optional. It only exists when the caller is making an RPC call to your function. If the caller sends an asynchronous request, the \"replyTo\" value is null.","title":"Inspect event metadata"},{"location":"guides/CHAPTER-9/#platform-api","text":"You can obtain a singleton instance of the Platform object to do the following:","title":"Platform API"},{"location":"guides/CHAPTER-9/#register-a-function","text":"We recommend using the PreLoad annotation in a class to declare the function route name, number of worker instances and whether the function is public or private. In some use cases where you want to create and destroy functions on demand, you can register them programmatically. In the following example, it registers \"my.function\" using the MyFunction class as a public function and \"another.function\" with the AnotherFunction class as a private function. It then registers two kotlin functions in public and private scope respectively. Platform platform = Platform.getInstance(); // register a public function platform.register(\"my.function\", new MyFunction(), 10); // register a private function platform.registerPrivate(\"another.function\", new AnotherFunction(), 20); // register a public suspend function platform.registerKoltin(\"my.suspend.function\", new MySuspendFunction(), 10); // register a private suspend function platform.registerKoltinPrivate(\"another.suspend.function\", new AnotherSuspendFunction(), 10);","title":"Register a function"},{"location":"guides/CHAPTER-9/#what-is-a-public-function","text":"A public function is visible by any application instances in the same network. When a function is declared as \"public\", the function is reachable through the EventAPI REST endpoint or a service mesh. A private function is invisible outside the memory space of the application instance that it resides. This allows application to encapsulate business logic according to domain boundary. You can assemble closely related functions as a composable application that can be deployed independently.","title":"What is a public function?"},{"location":"guides/CHAPTER-9/#release-a-function","text":"In some use cases, you want to release a function on-demand when it is no longer required. platform.release(\"another.function\"); The above API will unload the function from memory and release it from the \"event loop\".","title":"Release a function"},{"location":"guides/CHAPTER-9/#check-if-a-function-is-available","text":"You can check if a function with the named route has been deployed. if (platform.hasRoute(\"another.function\")) { // do something }","title":"Check if a function is available"},{"location":"guides/CHAPTER-9/#wait-for-a-function-to-be-ready","text":"Functions are registered asynchronously. For functions registered using the PreLoad annotation, they are available to your application when the MainApplication starts. For functions that are registered on-demand, you can wait for the function to get ready like this: Future status = platform.waitForProvider(\"cloud.connector\", 10); status.onSuccess(ready -> { // business logic when \"cloud.connector\" is ready }); Note that the \"onFailure\" method is not required. The onSuccess will return true or false. In the above example, your application waits for up to 10 seconds. If the function (i.e. the \"provider\") is available, the API will invoke the \"onSuccess\" method immediately.","title":"Wait for a function to be ready"},{"location":"guides/CHAPTER-9/#obtain-the-unique-application-instance-id","text":"When an application instance starts, a unique ID is generated. We call this the \"Origin ID\". String originId = po.getOrigin(); When running the application in a minimalist service mesh using Kafka or similar network event stream system, the origin ID is used to uniquely identify the application instance. The origin ID is automatically appended to the \"replyTo\" address when making a RPC call over a network event stream so that the system can send the response event back to the \"originator\" or \"calling\" application instance.","title":"Obtain the unique application instance ID"},{"location":"guides/CHAPTER-9/#set-application-personality","text":"An application may have one of the following personality: REST - the deployed application is user facing APP - the deployed application serves business logic RESOURCES - this is a resource-tier service. e.g. database service, MQ gateway, legacy service proxy, utility, etc. You can change the application personality like this: // the default value is \"APP\" ServerPersonality.getInstance().setType(ServerPersonality.Type.REST); The personality setting is for documentation purpose only. It does not affect the behavior of your application. It will appear in the application \"/info\" endpoint.","title":"Set application personality"},{"location":"guides/CHAPTER-9/#postoffice-api","text":"You can obtain an instance of the PostOffice from the input \"headers\" and \"instance\" parameters in the input arguments of your function. PostOffice po = new PostOffice(headers, instance); The PostOffice is the event manager that you can use to send asynchronous events or to make RPC requests. The constructor uses the READ only metadata in the \"headers\" argument in the \"handleEvent\" method of your function.","title":"PostOffice API"},{"location":"guides/CHAPTER-9/#send-an-asynchronous-event-to-a-function","text":"You can send an asynchronous event like this. // example-1 po.send(\"another.function\", \"test message\"); // example-2 po.send(\"another.function\", new Kv(\"some_key\", \"some_value\"), new kv(\"another_key\", \"another_value\")); // example-3 po.send(\"another.function\", somePoJo, new Kv(\"some_key\", \"some_value\")); // example-4 EventEnvelope event = new EventEnvelope().setTo(\"another.function\") .setHeader(\"some_key\", \"some_value\").setBody(somePoJo); po.send(event) // example-5 po.sendLater(event, new Date(System.currentTimeMillis() + 5000)); Example-1 sends the text string \"test message\" to the target service named \"another.function\". Example-2 sends two key-values as \"headers\" parameters to the same service. Example-3 sends a PoJo and a key-value pair to the same service. Example-4 is the same as example-3. It is using an EventEnvelope to construct the request. Example-5 schedules an event to be sent 5 seconds later. The first 3 APIs are convenient methods and the system will automatically create an EventEnvelope to hold the target route name, key-values and/or event payload.","title":"Send an asynchronous event to a function"},{"location":"guides/CHAPTER-9/#make-an-asynchronous-rpc-call","text":"You can make RPC call like this: // example-1 EventEnvelope request = new EventEnvelope().setTo(\"another.function\") .setHeader(\"some_key\", \"some_value\").setBody(somePoJo); Future response = po.asyncRequest(request, 5000); response.onSuccess(result -> { // result is the response event }); response.onFailure(e -> { // handle timeout exception }); // example-2 Future response = po.asyncRequest(request, 5000, false); response.onSuccess(result -> { // result is the response event // Timeout exception is returned as a response event with status=408 }); // example-3 with the \"rpc\" boolean parameter set to true Future response = po.asyncRequest(request, 5000, \"http://peer/api/event\", true); response.onSuccess(result -> { // result is the response event }); response.onFailure(e -> { // handle timeout exception }); Example-1 makes a RPC call with a 5-second timeout to \"another.function\". Example-2 sets the \"timeoutException\" to false, telling system to return timeout exception as a regular event. Example-3 makes an \"event over HTTP\" RPC call to \"another.function\" in another application instance called \"peer\". \"Event over HTTP\" is an important topic. Please refer to Chapter 7 for more details.","title":"Make an asynchronous RPC call"},{"location":"guides/CHAPTER-9/#perform-a-fork-n-join-rpc-call-to-multiple-functions","text":"In a similar fashion, you can make a fork-n-join call that sends request events in parallel to more than one function. // example-1 EventEnvelope request1 = new EventEnvelope().setTo(\"this.function\") .setHeader(\"hello\", \"world\").setBody(\"test message\"); EventEnvelope request2 = new EventEnvelope().setTo(\"that.function\") .setHeader(\"good\", \"day\").setBody(somePoJo); List requests = new ArrayList<>(); requests.add(request1); requests.add(request2); Future> responses = po.asyncRequest(requests, 5000); response.onSuccess(results -> { // results contains the response events }); response.onFailure(e -> { // handle timeout exception }); // example-2 Future> responses = po.asyncRequest(requests, 5000, false); response.onSuccess(results -> { // results contains the response events. // Partial result list is returned if one or more functions did not respond. });","title":"Perform a fork-n-join RPC call to multiple functions"},{"location":"guides/CHAPTER-9/#make-a-sequential-non-blocking-rpc-call","text":"You can make a sequential non-blocking RPC call from one function to another. The FastRPC is similar to the PostOffice. It is the event manager for KotlinLambdaFunction. You can create an instance of the FastRPC using the \"headers\" parameters in the input arguments of your function. val fastRPC = new FastRPC(headers) val request = EventEnvelope().setTo(\"another.function\") .setHeader(\"some_key\", \"some_value\").setBody(somePoJo) // example-1 val response = fastRPC.awaitRequest(request, 5000) // handle the response event // example-2 with the \"rpc\" boolean parameter set to true val response = fastRPC.awaitRequest(request, 5000, \"http://peer/api/event\", true) // handle the response event Example-1 performs a non-blocking RPC call Example-2 makes a non-blocking \"Event Over HTTP\" RPC call Note that timeout exception is returned as a regular event with status 408. Sequential non-blocking code is easier to read. Moreover, it handles more concurrent users and requests without consuming a lot of CPU resources because it is \"suspended\" while waiting for a response from another function.","title":"Make a sequential non-blocking RPC call"},{"location":"guides/CHAPTER-9/#perform-a-sequential-non-blocking-fork-n-join-call-to-multiple-functions","text":"You can make a sequential non-blocking fork-n-join call using the FastRPC API like this: val fastRPC = FastRPC(headers) val template = EventEnvelope().setTo(\"hello.world\").setHeader(\"someKey\", \"someValue\") val requests = ArrayList() // create a list of 4 request events for (i in 0..3) { requests.add(EventEnvelope(template.toBytes()).setBody(i).setCorrelationId(\"cid-$i\")) } val responses: List = fastRPC.awaitRequest(requests, 5000) // handle the response events In the above example, the function creates a list of request events from a template event with target service \"hello.world\". It sets the number 0 to 3 to the individual events with unique correlation IDs. The response events contain the same set of correlation IDs so that your business logic can decide how to handle individual response event. The result may be a partial list of response events if one or more functions failed to respond on time.","title":"Perform a sequential non-blocking fork-n-join call to multiple functions"},{"location":"guides/CHAPTER-9/#check-if-a-function-with-a-named-route-exists","text":"The PostOffice provides the \"exists()\" method that is similar to the \"platform.hasRoute()\" command. The difference is that the \"exists()\" method can discover functions of another application instance when running in the \"service mesh\" mode. If your application is not deployed in a service mesh, the PostOffice's \"exists\" and Platform's \"hasRoute\" APIs will provide the same result. boolean found = po.exists(\"another.function\"); if (found) { // do something }","title":"Check if a function with a named route exists"},{"location":"guides/CHAPTER-9/#retrieve-trace-id-and-path","text":"If you want to know the route name and optional trace ID and path, you can use the following APIs. For example, if tracing is enabled, the trace ID will be available. You can put the trace ID in application log messages. This would group log messages of the same transaction together when you search the trace ID from a centralized logging dashboard such as Splunk. String myRoute = po.getRoute(); String traceId = po.getTraceId(); String tracePath = po.getTracePath();","title":"Retrieve trace ID and path"},{"location":"guides/CHAPTER-9/#trace-annotation","text":"You can use the PostOffice instance to annotate a trace in your function like this: // annotate a trace with the key-value \"hello:world\" po.annotateTrace(\"hello\", \"world\"); This is useful when you want to attach transaction specific information in the performance metrics. For example, the traces may be used in production transaction analytics. IMPORTANT: do not annotate sensitive or secret information such as PII, PHI, PCI data because the trace is visible in application log. It may also be forwarded to a centralized telemetry dashboard.","title":"Trace annotation"},{"location":"guides/CHAPTER-9/#configuration-api","text":"Your function can access the main application configuration from the platform like this: AppConfigReader config = AppConfigReader.getInstance(); // the value can be string or a primitive Object value = config.get('my.parameter'); // the return value will be converted to a string String text = config.getProperty('my.parameter'); The system uses the standard dot-bracket format for a parameter name. e.g. \"hello.world\", \"some.key[2]\" You can override the main application configuration at run-time using the Java argument \"-D\". e.g. \"java -Dserver.port=8080 -jar myApp.jar\" Additional configuration files can be added with the ConfigReader API like this: // filePath should have location prefix \"classpath:/\" or \"file:/\" ConfigReader reader = new ConfigReader(); reader.load(filePath); The configuration system supports environment variable or reference to the main application configuration using the dollar-bracket syntax ${reference:default_value} . e.g. \"some.key=${MY_ENV_VARIABLE}\", \"some.key=${my.key}\"","title":"Configuration API"},{"location":"guides/CHAPTER-9/#custom-serializer","text":"We are using GSON as the underlying serializer to handle common use cases. However, there may be situation that you want to use your own custom serialization library. To do that, you may write a serializer that implements the CustomSerializer interface: public interface CustomSerializer { public Map toMap(Object obj); public T toPoJo(Object obj, Class toValueType); } You may configure a user function to use a custom serializer by adding the \"customSerializer\" parameter in the PreLoad annotation. For example, @PreLoad(route=\"my.user.function\", customSerializer = JacksonSerializer.class) public class MyUserFunction implements TypedLambdaFunction { @Override public SimplePoJo handleEvent(Map headers, SimplePoJo input, int instance) { return input; } } If you register your function dynamically in code, you can use the following platform API to assign a custom serializer. public void setCustomSerializer(String route, CustomSerializer mapper); // e.g. // platform.setCustomSerializer(\"my.function\", new JacksonSerializer()); If you use the PostOffice to programmatically send event or make event RPC call and you need custom serializer, you can create a PostOffice instance like this: // this should be the first statement in the \"handleEvent\" method. PostOffice po = new PostOffice(headers, instance, new MyCustomSerializer()); The outgoing event using the PostOffice will use the custom serializer automatically. To interpret an event response from a RPC call, you can use the following PostOffice API: MyPoJo result = po.getResponseBodyAsPoJo(responseEvent, MyPoJo.class);","title":"Custom serializer"},{"location":"guides/CHAPTER-9/#minimalist-api-design-for-event-orchestration","text":"As a best practice, we advocate a minimalist approach in API integration. To build powerful composable applications, the above set of APIs is sufficient to perform \"event orchestration\" where you write code to coordinate how the various functions work together as a single \"executable\". Please refer to Chapter-4 for more details about event orchestration. Since Mercury is used in production installations, we will exercise the best effort to keep the core API stable. Other APIs in the toolkits are used internally to build the engine itself, and they may change from time to time. They are mostly convenient methods and utilities. The engine is fully encapsulated and any internal API changes are not likely to impact your applications.","title":"Minimalist API design for event orchestration"},{"location":"guides/CHAPTER-9/#optional-event-scripting","text":"To further reduce coding effort, you can perform \"event orchestration\" by configuration using \"Event Script\". It is available as an enterprise add-on module from Accenture.","title":"Optional Event Scripting"},{"location":"guides/CHAPTER-9/#co-existence-with-other-development-frameworks","text":"Mercury libraries are designed to co-exist with your favorite frameworks and tools. Inside a class implementing the LambdaFunction , TypedLambdaFunction or KotlinLambdaFunction , you can use any coding style and frameworks as you like, including sequential, object-oriented and reactive programming styles. Mercury version 3 has a built-in lightweight non-blocking HTTP server, but you can also use Spring Boot and other application server framework with it. A sample Spring Boot integration is provided in the \"rest-spring\" project. It is an optional feature, and you can decide to use a regular Spring Boot application with Mercury or to pick the customized Spring Boot in the \"rest-spring\" library.","title":"Co-existence with other development frameworks"},{"location":"guides/CHAPTER-9/#template-application-for-quick-start","text":"You can use the lambda-example project as a template to start writing your own applications. It is preconfigured to support kernel threads, coroutine and suspend function.","title":"Template application for quick start"},{"location":"guides/CHAPTER-9/#source-code-update-frequency","text":"This project is licensed under the Apache 2.0 open sources license. We will update the public codebase after it passes regression tests and meets stability and performance benchmarks in our production systems. Mercury is developed as an engine for you to build the latest cloud native and composable applications. While we are updating the technology frequently, the essential internals and the core APIs are stable. We are monitoring the progress of the upcoming Java 19 Virtual Thread feature and will include it in our API when it becomes officially available.","title":"Source code update frequency"},{"location":"guides/CHAPTER-9/#technical-support","text":"For enterprise clients, optional technical support is available. Please contact your Accenture representative for details. Chapter-8 Home Chapter-10 Service Mesh Table of Contents Migration Guide","title":"Technical support"},{"location":"guides/TABLE-OF-CONTENTS/","text":"Developer's Guide Mercury version 3 is a toolkit for writing composable applications. Chapter 1 - Getting Started Chapter 2 - Function Execution Strategies Chapter 3 - REST automation Chapter 4 - Event orchestration Chapter 5 - Build, test and deploy Chapter 6 - Spring Boot Chapter 7 - Event over HTTP Chapter 8 - Service mesh Chapter 9 - API overview Chapter 10 - Migration guide Appendix I - application.properties Appendix II - Reserved route names Appendix III - Actuators and HTTP client","title":"Contents"},{"location":"guides/TABLE-OF-CONTENTS/#developers-guide","text":"Mercury version 3 is a toolkit for writing composable applications. Chapter 1 - Getting Started Chapter 2 - Function Execution Strategies Chapter 3 - REST automation Chapter 4 - Event orchestration Chapter 5 - Build, test and deploy Chapter 6 - Spring Boot Chapter 7 - Event over HTTP Chapter 8 - Service mesh Chapter 9 - API overview Chapter 10 - Migration guide Appendix I - application.properties Appendix II - Reserved route names Appendix III - Actuators and HTTP client","title":"Developer's Guide"}]} \ No newline at end of file diff --git a/docs/search/worker.js b/docs/search/worker.js new file mode 100644 index 000000000..8628dbce9 --- /dev/null +++ b/docs/search/worker.js @@ -0,0 +1,133 @@ +var base_path = 'function' === typeof importScripts ? '.' : '/search/'; +var allowSearch = false; +var index; +var documents = {}; +var lang = ['en']; +var data; + +function getScript(script, callback) { + console.log('Loading script: ' + script); + $.getScript(base_path + script).done(function () { + callback(); + }).fail(function (jqxhr, settings, exception) { + console.log('Error: ' + exception); + }); +} + +function getScriptsInOrder(scripts, callback) { + if (scripts.length === 0) { + callback(); + return; + } + getScript(scripts[0], function() { + getScriptsInOrder(scripts.slice(1), callback); + }); +} + +function loadScripts(urls, callback) { + if( 'function' === typeof importScripts ) { + importScripts.apply(null, urls); + callback(); + } else { + getScriptsInOrder(urls, callback); + } +} + +function onJSONLoaded () { + data = JSON.parse(this.responseText); + var scriptsToLoad = ['lunr.js']; + if (data.config && data.config.lang && data.config.lang.length) { + lang = data.config.lang; + } + if (lang.length > 1 || lang[0] !== "en") { + scriptsToLoad.push('lunr.stemmer.support.js'); + if (lang.length > 1) { + scriptsToLoad.push('lunr.multi.js'); + } + if (lang.includes("ja") || lang.includes("jp")) { + scriptsToLoad.push('tinyseg.js'); + } + for (var i=0; i < lang.length; i++) { + if (lang[i] != 'en') { + scriptsToLoad.push(['lunr', lang[i], 'js'].join('.')); + } + } + } + loadScripts(scriptsToLoad, onScriptsLoaded); +} + +function onScriptsLoaded () { + console.log('All search scripts loaded, building Lunr index...'); + if (data.config && data.config.separator && data.config.separator.length) { + lunr.tokenizer.separator = new RegExp(data.config.separator); + } + + if (data.index) { + index = lunr.Index.load(data.index); + data.docs.forEach(function (doc) { + documents[doc.location] = doc; + }); + console.log('Lunr pre-built index loaded, search ready'); + } else { + index = lunr(function () { + if (lang.length === 1 && lang[0] !== "en" && lunr[lang[0]]) { + this.use(lunr[lang[0]]); + } else if (lang.length > 1) { + this.use(lunr.multiLanguage.apply(null, lang)); // spread operator not supported in all browsers: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator#Browser_compatibility + } + this.field('title'); + this.field('text'); + this.ref('location'); + + for (var i=0; i < data.docs.length; i++) { + var doc = data.docs[i]; + this.add(doc); + documents[doc.location] = doc; + } + }); + console.log('Lunr index built, search ready'); + } + allowSearch = true; + postMessage({config: data.config}); + postMessage({allowSearch: allowSearch}); +} + +function init () { + var oReq = new XMLHttpRequest(); + oReq.addEventListener("load", onJSONLoaded); + var index_path = base_path + '/search_index.json'; + if( 'function' === typeof importScripts ){ + index_path = 'search_index.json'; + } + oReq.open("GET", index_path); + oReq.send(); +} + +function search (query) { + if (!allowSearch) { + console.error('Assets for search still loading'); + return; + } + + var resultDocuments = []; + var results = index.search(query); + for (var i=0; i < results.length; i++){ + var result = results[i]; + doc = documents[result.ref]; + doc.summary = doc.text.substring(0, 200); + resultDocuments.push(doc); + } + return resultDocuments; +} + +if( 'function' === typeof importScripts ) { + onmessage = function (e) { + if (e.data.init) { + init(); + } else if (e.data.query) { + postMessage({ results: search(e.data.query) }); + } else { + console.error("Worker - Unrecognized message: " + e); + } + }; +} diff --git a/docs/sitemap.xml b/docs/sitemap.xml new file mode 100644 index 000000000..1805c31fe --- /dev/null +++ b/docs/sitemap.xml @@ -0,0 +1,108 @@ + + + + https://github.com/accenture/mercury/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/CHANGELOG/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/CODE_OF_CONDUCT/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/CONTRIBUTING/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/INCLUSIVITY/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/ROADMAP/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/arch-decisions/DESIGN-NOTES/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/guides/APPENDIX-I/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/guides/APPENDIX-II/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/guides/APPENDIX-III/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-1/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-10/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-2/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-3/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-4/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-5/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-6/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-7/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-8/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-9/ + 2024-08-31 + daily + + + https://github.com/accenture/mercury/guides/TABLE-OF-CONTENTS/ + 2024-08-31 + daily + + \ No newline at end of file diff --git a/docs/sitemap.xml.gz b/docs/sitemap.xml.gz new file mode 100644 index 000000000..2ea62b31f Binary files /dev/null and b/docs/sitemap.xml.gz differ