diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 00000000..f6f17e7e --- /dev/null +++ b/docs/404.html @@ -0,0 +1,177 @@ + + + + + + + + Mercury + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • +
  • +
  • +
+
+
+
+
+ + +

404

+ +

Page not found

+ + +
+
+ +
+
+ +
+ +
+ +
+ + + + + +
+ + + + + + + + + diff --git a/docs/CHANGELOG/index.html b/docs/CHANGELOG/index.html new file mode 100644 index 00000000..6843664b --- /dev/null +++ b/docs/CHANGELOG/index.html @@ -0,0 +1,2560 @@ + + + + + + + + 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 4.1.1, 12/18/2024

+

Added

+

Added "map" constant type in input data mapping.

+

Removed

+

N/A

+

Changed

+

Updated Chapter-4 for the new "map" constant feature.

+
+

Version 4.1.0, 12/11/2024

+

This milestone version achieves ideal event choreography by removing additional event routing +to and from the Event Manager. This would boost internal event routing performance by 50 percent.

+

Added

+

Performance optimization for Event Script

+

Removed

+

N/A

+

Changed

+

The platform-core module uses virtual threads to execute event.script.manager and task.executor +directly to eliminate additional serialization overheads since the two functions are event routers +themselves.

+
+

Version 4.0.33, 12/11/2024

+

Added

+

Support of custom content types in application.yml

+

Removed

+

N/A

+

Changed

+
    +
  1. Improved websocket housekeeping logic
  2. +
  3. Use bench.add to replace bench.offer API
  4. +
+
+

Version 4.0.32, 12/9/2024

+

Added

+
    +
  1. For completeness, added Boolean AND and OR operations for simple type mapping.
  2. +
  3. Added traceId as metadata for a flow instance
  4. +
+

Removed

+

N/A

+

Changed

+
    +
  1. Update Chapter-4 for the new AND/OR type mapping feature
  2. +
  3. Consistent custom HTTP headers for event over http protocol and streaming content
  4. +
+
+

Version 4.0.31, 12/5/2024

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+
    +
  1. The "keep.original" key is renamed as "keep-original" to comply with convention.
  2. +
  3. Continue processing if some preload override config files are missing.
  4. +
+
+

Version 4.0.30, 12/5/2024

+

Added

+

Implemented unique task naming feature for event flow configuration.

+

Removed

+

N/A

+

Changed

+
    +
  1. The "keep_original" key is renamed as "keep.original" in preload override
  2. +
  3. Chapter-4 of developer guide updated with the new task alias feature
  4. +
+
+

Version 4.0.29, 12/3/2024

+

Added

+

Added integer, long, float, double and boolean type matching for state machine.

+

Removed

+

N/A

+

Changed

+

N/A

+
+

Version 4.0.28, 11/29/2024

+

Added

+
    +
  1. Support for simple data type matching processing (text, substring, binary and b64)
  2. +
  3. Optional external state machine
  4. +
+

Removed

+

Removed "http.input." and "http.output." aliases from event script. Instead, use the +generic "input." and "output." namespaces.

+

Changed

+
    +
  1. Bugfix for AsyncHttpClient to allow missing HTTP request body in POST, PUT or PATCH request
  2. +
  3. Mono reactive flow control
  4. +
+
+

Version 4.0.27, 11/27/2024

+

Added

+
    +
  1. Support for Mono/Flux return type for KotlinLambdaFunction
  2. +
  3. Implemented Websocket handshake handler to adjust to API changes in vertx 4.5.11
  4. +
+

Removed

+

N/A

+

Changed

+

N/A

+
+

Version 4.0.26, 11/26/2024

+

Added

+

N/A

+

Removed

+

Remove pom.xml version override for netty and spring framework because +Spring Boot 3.4.0 fetches the correct versions of netty and spring framework.

+

Earlier override was done to avoid security vulnerabilities of older versions +of netty and spring framework.

+

Changed

+
    +
  1. Handle the case that Mono will not return payload if the payload is null
  2. +
  3. OSS update: Classgraph 4.8.179, Vertx 4.5.11, Spring Boot 3.4.0, Kafka Client 3.9.0
  4. +
+
+

Version 4.0.25, 11/21/2024

+

Added

+

Support more than one REST configuration files.

+

When a duplicated REST entry is detected, the system will abort REST endpoint rendering +and print out an error message in application log.

+

If you have unit tests to cover the REST endpoints, the unit tests will fail accordingly.

+

Removed

+

N/A

+

Changed

+

Improved environment variable parsing in config reader. System will skip entries with +invalid environment variable reference syntax.

+
+

Version 4.0.24, 11/20/2024

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+

Bugfix for an edge case in config reader to handle control character of brackets inside +an environment variable reference.

+

e.g. some.key=${ENV_VAR:something/{test1}/{test2}}

+
+

Version 4.0.23, 11/19/2024

+

Added

+

N/A

+

Removed

+

ObjectStreamWriter and AsyncObjectStreamReader are removed

+

Changed

+
    +
  1. Replace ObjectStreamWriter with FluxPublisher
  2. +
  3. Replace AsyncObjectStreamReader with FluxConsumer
  4. +
  5. Bugfix for FluxConsumer expiry - change type from "data" to "exception".
  6. +
+
+

Version 4.0.22, 11/18/2024

+

Added

+

FluxPublisher and FluxConsumer for integration with Flux reactive response object

+

Removed

+

N/A

+

Changed

+
    +
  1. Unit tests in event streaming and post office to support Flux integration
  2. +
  3. Select reactor-core version 3.7.0 using dependency management (reactor-bom version 2024.0.0)
  4. +
+
+

Version 4.0.21, 11/14/2024

+

Added

+

Support for user function to return a Mono reactive response object

+

Removed

+

N/A

+

Changed

+
    +
  1. Update netty to version 4.1.115.Final to address security vulnerability in 4.1.114
  2. +
  3. Move reactor-core library from rest-spring-3 to platform-core
  4. +
+
+

Version 4.0.20, 11/13/2024

+

Added

+

For ease of configuration, added "com.accenture" to the base packages so that user applications +do not need to include it to use the event-script-engine module.

+

Removed

+

if-then-else pipeline feature in event-script

+

Changed

+
    +
  1. Update Event Script syntax for consistency
  2. +
  3. Fix error in counting number of compiled flows
  4. +
+
+

Version 4.0.16, 11/10/2024

+

Added

+

Generate unique flow instance ID as reference during flow execution.

+

Removed

+

N/A

+

Changed

+

Save the original correlation-ID from the calling party in a flow instance and +return this value to the calling party at the end of flow execution.

+
+

Version 4.0.15, 11/7/2024

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+

renamed StartFlow to FlowExecutor

+
+

Version 4.0.14, 11/7/2024

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+
    +
  1. Health check function can return either a text string or a Map
  2. +
  3. StartFlow API updates
  4. +
+
+

Version 4.0.13, 11/5/2024

+

Added

+

Added helper class "StartFlow" to start a flow, including internal flows without HTTP or Kafka.

+

Removed

+

N/A

+

Changed

+
    +
  1. Bugfix for empty YAML file to avoid null pointer exception
  2. +
  3. Sort event scripts for orderly logging in the CompileFlows validation process
  4. +
+
+

Version 4.0.12, 10/31/2024

+

Added

+

New feature to support resolution of more than one environment variable for a parameter +using the ConfigReader

+

Removed

+

N/A

+

Changed

+

Update OSS modules +1. classgraph version 4.8.177 +2. kotlin version 2.0.21 +3. guava version 33.3.1-jre +4. jUnit version 5 jupiter

+

Adjusted all unit tests to use jUnit 5

+
+

Version 4.0.11, 10/28/2024

+

Added

+

New features to support: +1. multiple preload override config file +2. multiple flow list config files

+

Removed

+
    +
  1. unused class "UnauthorizedObj" in platform-core
  2. +
  3. commons-io dependency in Kafka-Standalone subproject
  4. +
+

Changed

+
    +
  1. Unit test for the preload override feature
  2. +
  3. JavaDoc for the MainApplication
  4. +
+
+

Version 4.0.10, 10/24/2024

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+
    +
  1. OSS update - Spring Boot 3.3.5
  2. +
  3. Security patch for CR/LF exploit for HTTP cookie
  4. +
+
+

Version 4.0.9, 10/18/2024

+

Added

+

Added Kafka Raft for the Kafka-standalone app.

+

Removed

+

Removed zookeeper from Kafka-standalone app.

+

Changed

+

Update spring framework verison 6.1.14 to avoid vulnerability in webflux

+
+

Version 4.0.8, 10/9/2024

+

Added

+
    +
  1. Partial support of Active Profile using the "spring.profiles.active" parameter
  2. +
  3. Hierarchy of flows
  4. +
+

Removed

+

N/A

+

Changed

+

N/A

+
+

Version 4.0.7, 10/1/2024

+

Added

+

A generic "no-op" function for use in event scripts.

+

Removed

+

Feature to ping a function without payload and headers.

+

Changed

+

Simplified api-playground application

+
+

Version 4.0.6, 9/27/2024

+

Added

+
    +
  1. HTTP request Cookie value filtering using RFC-6265 strict syntax
  2. +
+

Removed

+
    +
  1. Automatic index page redirection filter for Spring Boot
  2. +
+

Changed

+
    +
  1. Upgrade SHA-1 to SHA-512 algorithm in CryptoAPI utility
  2. +
  3. Fix security vulnerability associated with HTTP request header and cookie manipulation
  4. +
+
+

Version 4.0.5, 9/24/2024

+

Added

+

N/A

+

Removed

+
    +
  1. Feature for automatic PoJo transport in EventEnvelope and MsgPack
  2. +
  3. Feature for safe.data.model deserialization
  4. +
  5. Benchmark-server is no longer required
  6. +
+

Changed

+
    +
  1. Update OSS versions - vertx 4.5.10, kotlin 2.0.20, spring boot 3.3.4
  2. +
+
+

Version 4.0.4, 9/5/2024

+

Added

+

New feature for AsyncHttpClient to render small streaming HTTP response (i.e. chunked binary data) as byte array.

+

For details, Please refer to Appendix III, Developer Guide

+

Removed

+

N/A

+

Changed

+

Bugfix for parsing default value of environment variable in ConfigReader. +This resolves an issue when the special character colon (":") is used more than once in the default value.

+
+

Version 4.0.3, 9/4/2024

+

Added

+

The "preload override" feature is added. This allows overriding a reusable composable library with a set of new +route names that are unique for use in an event flow configuration script.

+

For details, Please refer to Chapter 4, Developer Guide

+

Removed

+

N/A

+

Changed

+

N/A

+
+

Version 4.0.2, 8/31/2024

+

Added

+
    +
  1. New "classpath" namespace for input data mapping
  2. +
  3. Support for input data mapping to handle subset of input request body as a Map or PoJo
  4. +
+

Removed

+

N/A

+

Changed

+
    +
  1. Remove the class "type" variable from AsyncHttpRequest
  2. +
  3. Improve the "removeElement" method in MultiLevelMap
  4. +
  5. Make HTTP input request header labels key-insensitive
  6. +
  7. Update Spring Boot to version 3.3.3
  8. +
+
+

Version 4.0.1, 8/19/2024

+

Added

+

new File read/write feature in Event Script's I/O data mapping

+

Removed

+

N/A

+

Changed

+
    +
  1. Update Spring Boot to version 3.3.2
  2. +
  3. Update Guava to version 33.3.0-jre
  4. +
  5. Update Vertx to version 4.5.9
  6. +
  7. Update Kotlin to version 2.0.10
  8. +
  9. Change "upstream" to "dependency" in the "/health" endpoint
  10. +
+
+

Version 4.0.0, 6/24/2024

+

This version merges Event Script into the Mercury Composable repository.

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+
    +
  1. Update Spring Boot to version 3.3.1
  2. +
  3. Update Guava to version 33.2.1-jre
  4. +
  5. Update Vertx to version 4.5.8
  6. +
  7. Update Kotlin to version 2.0.0
  8. +
  9. Update classgraph to version 4.8.174
  10. +
  11. Optional reply event for a flow configuration
  12. +
+
+

Kafka-standalone is still using Spring Boot 3.2.5 due to compatibility issue

+
+
+

Version 3.1.5, 5/1/2024

+

This version supercedes 3.1.4 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.1.4, 4/28/2024

+

Added

+

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

+

Removed

+

N/A

+

Changed

+

Updated syntax for static-content-filter

+
+

Version 3.1.3, 4/24/2024

+

Added

+

N/A

+

Removed

+

N/A

+

Changed

+

Enhanced OptionalService annotation.

+
+

Version 3.1.2, 4/17/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.5, Vertx 4.5.7)
  2. +
  3. Improve AppConfigReader and ConfigReader to use the app-config-reader.yml file.
  4. +
  5. Enhanced OptionalService annotation.
  6. +
+
+

Version 3.1.1, 2/8/2024

+

Added

+
    +
  1. AutoStart to run application as Spring Boot if the rest-spring-3 library is packaged in app
  2. +
  3. Configurable "Event over HTTP" - automatic forward events over HTTP using a configuration
  4. +
  5. Support user defined serializer with PreLoad annotation and platform API
  6. +
+

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. +
+
+

Version 3.1.0, 1/5/2024

+

Added

+
    +
  1. Full integration with Java 21 Virtual Thread
  2. +
  3. Default execution mode is set to "virtual thread"
  4. +
  5. KernelThreadRunner annotation added to provide optional support of kernel threads
  6. +
+

Removed

+
    +
  1. Retired Spring Boot version 2
  2. +
  3. Hazelcast and ActiveMQ network connectors
  4. +
+

Changed

+

platform-core engine updated with virtual thread

+
+

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 00000000..4ed3198f --- /dev/null +++ b/docs/CODE_OF_CONDUCT/index.html @@ -0,0 +1,260 @@ + + + + + + + + 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 00000000..56a705e9 --- /dev/null +++ b/docs/CONTRIBUTING/index.html @@ -0,0 +1,227 @@ + + + + + + + + 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 + + + +
+ + + + + + + + + diff --git a/docs/INCLUSIVITY/index.html b/docs/INCLUSIVITY/index.html new file mode 100644 index 00000000..694d637b --- /dev/null +++ b/docs/INCLUSIVITY/index.html @@ -0,0 +1,268 @@ + + + + + + + + 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/arch-decisions/DESIGN-NOTES/index.html b/docs/arch-decisions/DESIGN-NOTES/index.html new file mode 100644 index 00000000..6a725760 --- /dev/null +++ b/docs/arch-decisions/DESIGN-NOTES/index.html @@ -0,0 +1,267 @@ + + + + + + + + Design notes - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Design notes

+

Event choreography by configuration

+

The recommended way to write a composable application is event choreography by configuration using "Event Script".

+

This would potentially reduce code size by half.

+

Support sequential synchronous RPC in a non-blocking fashion

+

The foundation library (platform-core) has been integrated with Java 21 virtual thread and +Kotlin suspend function features.

+

When a user function makes a RPC call using virtual thread or suspend function, +the user function appears to be "blocked" so that the code can execute sequentially. +Behind the curtain, the function is actually "suspended".

+

This makes sequential code with RPC performs as good as reactive code. +More importantly, the sequential code represents the intent of the application clearly, +thus making code easier to read and maintain.

+

Low level control of function execution strategies

+

You can precisely control how your functions execute, using virtual threads, suspend functions +or kernel thread pools to yield the highest performance and throughput.

+

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 API functional compatibility with Jackson, we have added the writeValueAsString, +writeValueAsBytes and readValue methods.

+

The convertValue method has been consolidated into the readValue method.

+

MsgPack

+

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

+

User provided serializers

+

This provides more flexibility for user function to take full control of their PoJo serialization needs.

+

Custom JSON and XML serializers

+

For consistency, we have customized 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.

+

In-memory event system

+

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.

+

In Mercury version 3.1, the platform core engine is fully integrated with Java 21 virtual thread.

+

Spring Boot 3

+

The platform-core includes a non-blocking HTTP and websocket server for standalone operation without +Spring Boot. The rest-spring-3 library is designed to turn your code to be a Spring Boot application.

+

You may also use the platform-core library with a regular Spring Boot application without the +rest-spring-3 library if you prefer.

+

Support of Mono and Flux results

+

A user function may return a regular result that can be a PoJo, HashMap or Java primitive.

+

It can also return a Mono or Flux reactive response object for a future result or a future series of +results. Other reactive response objects must be converted to a Mono or Flux object.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « 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 00000000..6cb60000 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 00000000..7059e231 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 00000000..f815f63f 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 00000000..f2c76e5b 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 00000000..e9f60ca9 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 00000000..855c845e --- /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 00000000..35acda2f 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 00000000..400014a4 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 00000000..4d13fc60 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 00000000..88ad05b9 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 00000000..c4e3d804 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 00000000..c6dff51f 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 00000000..bb195043 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 00000000..76114bc0 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 00000000..3404f37e 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 00000000..ae1307ff 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 00000000..3bf98433 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 00000000..ad773009 --- /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 00000000..ab0631a1 --- /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/composable-application.png b/docs/diagrams/composable-application.png new file mode 100644 index 00000000..9116f06f Binary files /dev/null and b/docs/diagrams/composable-application.png differ diff --git a/docs/diagrams/event-flow-diagram.png b/docs/diagrams/event-flow-diagram.png new file mode 100644 index 00000000..22ab23d5 Binary files /dev/null and b/docs/diagrams/event-flow-diagram.png differ diff --git a/docs/guides/APPENDIX-I/index.html b/docs/guides/APPENDIX-I/index.html new file mode 100644 index 00000000..f2197c44 --- /dev/null +++ b/docs/guides/APPENDIX-I/index.html @@ -0,0 +1,609 @@ + + + + + + + + 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
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
protect.info.endpointstrue to disable actuators. Default: trueOptional
trace.http.headercomma separated list. Default "X-Trace-Id"Optional
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.

+

Partial support of Spring Active Profiles

+

When the parameter "spring.profiles.active" is available in application.properties or application.yml, +the AppConfigReader will try to load the additional configuration files.

+

For example, if "spring.profiles.active=dev", the system will load "application-dev.properties" +and "application-dev.yml" accordingly.

+

When more than one active profile is needed, you can use a comma separated list of profiles in +"spring.profiles.active".

+

For Spring Boot compatibility, the filename prefix "application-" is fixed.

+

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.

+

Snake or Camel case serializers

+

Serialization and de-serialization of events are performed automatically.

+

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 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 built-in XML serializer by adding the following HTTP +request header.

+
X-Raw-Xml=true
+
+

Custom content types

+

If you use custom content types in your application, you may add the following section in the application.yml +configuration file:

+
custom.content.types:
+  - 'application/vnd.my.org-v2.0+json -> application/json'
+
+

In the "custom.content.types" section, you can configure a list of content-type mappings. +The left-hand-side is the custom content-type and the right-hand-side is a standard content-type.

+

The content-type mapping tells the system to treat the custom content type as if it is the standard content +type.

+

In the above example, the HTTP payload with the custom content type is treated as a regular JSON content.

+

If you want to put the custom content types in a separate configuration file, please put them in a file named +"custom-content-type.yml" under the "resources" folder.

+


+ + + + + + + + + + + + + + + +
Chapter-9HomeAppendix-II
API OverviewTable of ContentsReserved names and headers
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/APPENDIX-II/index.html b/docs/guides/APPENDIX-II/index.html new file mode 100644 index 00000000..55f33ffc --- /dev/null +++ b/docs/guides/APPENDIX-II/index.html @@ -0,0 +1,477 @@ + + + + + + + + Appendix-II - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Reserved names

+

The system reserves some route names and headers for routing purpose.

+

System 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 overload 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
event.script.managerInstantiate new event flow instanceevent-script
task.executorPerform event choreographyevent-script
http.flow.adapterBuilt-in flow adapterevent-script
no.opno-operation placeholder functionevent-script
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.

+

No-op function

+

The "no.op" function is used as a placeholder for building skeleton or simple decision function for +an event flow use case.

+

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);
+
+

Reserved HTTP header names

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HeaderPurpose
X-Stream-IdTemporal route name for streaming content
X-TTLTime to live in milliseconds for a streaming content
X-Small-Payload-As-BytesThis header, if set to true, tells system to render stream content as bytes
X-Event-ApiThe system uses this header to indicate that the request is sent over HTTP
X-AsyncThis header, if set to true, indicates it is a drop-n-forget request
X-Trace-IdThis allows the system to propagate trace ID
X-Correlation-IdAlternative to X-Trace-Id
X-Content-LengthIf present, it is the expected length of a streaming content
X-Raw-XmlThis header, if set to true, tells to system to skip XML rendering
X-Flow-IdThis tells the event manager to select a flow configuration by ID
X-App-InstanceThis header is used by some protected actuator REST endpoints
+

To support traceId that is stored in X-Correlation-Id HTTP header, set this in application.properties.

+
# list of supported traceId headers where the first one is the default label
+trace.http.header=X-Correlation-Id, X-Trace-Id
+
+


+ + + + + + + + + + + + + + + +
Appendix-IHomeAppendix-III
Application ConfigurationTable of ContentsActuators, HTTP client and More
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/APPENDIX-III/index.html b/docs/guides/APPENDIX-III/index.html new file mode 100644 index 00000000..2f15ffe1 --- /dev/null +++ b/docs/guides/APPENDIX-III/index.html @@ -0,0 +1,533 @@ + + + + + + + + Appendix-III - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Actuators, HTTP client and More

+

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 or a Map of the health check. e.g. read/write test result. + If health check fails, you can throw AppException with status code and error message.
  4. +
+

A sample health service is available in the DemoHealth class of the composable-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.
+         * It must return a Map.
+         */
+        if (INFO.equals(headers.get(TYPE))) {
+            Map<String, Object> about = new HashMap<>();
+            about.put("service", "demo.service");
+            about.put("href", "http://127.0.0.1");
+            return about;
+        }
+        if (HEALTH.equals(headers.get(TYPE))) {
+            /*
+             * This is a place-holder for checking a downstream service.
+             *
+             * Please implement your own logic to test if a downstream service is running fine.
+             * If running, just return health status as a String or a Map.
+             *
+             * Otherwise,
+             *      throw new AppException(status, message)
+             */
+            return Map.of("demo", "I am 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);
+EventEnvelope res = po.request(request, 5000);
+// the result is in res.getBody()
+
+

By default, your user function is running in a virtual thread. +While the RPC call looks like synchronous, the po.request API will run in non-blocking mode in the same fashion +as the "async/await" pattern.

+

For reactive programming, you can use the "asyncRequest" API 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 
+});
+
+

If you prefer writing in Kotlin, you can create 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);
+EventPublisher publisher = new EventPublisher(timeoutInMIlls);
+while ((len = in.read(buffer, 0, buffer.length)) != -1) {
+    publisher.publish(buffer, 0, len);
+}
+// closing the output stream would send a EOF signal to the stream
+publisher.publishCompletion();
+// tell the HTTP client to read the input stream by setting the streamId in the AsyncHttpRequest object
+req.setStreamRoute(publisher.getStreamId());
+
+

Read HTTP response body stream

+

If content length is not given, the response body would arrive as a stream.

+

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

+

You can process the input stream using the FluxConsumer class like this:

+
String streamId = headers.get("stream");
+long ttl = 10000; // anticipated time in milliseconds to stream the content
+FluxConsumer<Map<String, Object>> fc = new FluxConsumer<>(streamId, ttl);
+fc.consume(
+    data -> {
+        // handle incoming message
+    },
+    e -> {
+        // handle exception where e is a Throwable
+    },
+    () -> {
+        // handle stream completion
+    }
+);
+
+

By default, a user function is executed in a virtual thread which effectively is an "async" function and +the PostOffice "request" API operates in the non-blocking "await" mode.

+

If you prefers writing in Kotlin, 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
+        }
+    }
+}
+
+

Rendering a small payload of streaming content

+

If the streaming HTTP response is certain to be a small payload (i.e. Kilobytes), you can optimize +the rendering by adding the HTTP request header (X-Small-Payload-As-Bytes=true) in the AsyncHttpRequest object.

+
AsyncHttpRequest req = new AsyncHttpRequest();
+req.setMethod("GET");
+req.setUrl("/api/some/binary/content");
+req.setTargetHost("https://service_provider_host");
+req.setHeader("X-Small-Payload-As-Bytes", "true");
+
+

Note that the AsyncHttpClient will insert a custom HTTP response header "X-Content-Length" to show the size +of the payload.

+
+

IMPORTANT: This optimization does not validate the size of the streaming content. Therefore, it is possible for + the streaming content to trigger an "out of memory" exception. You must make sure the streaming content + is small enough before using the "X-Small-Payload-As-Bytes" HTTP request header.

+
+

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.

+

Starting a flow programmatically

+

To start an "event" flow from a unit test, you may use the helper class "FlowExecutor" under the "Event Script" module.

+

Examples of some APIs are as follows:

+
// launch a flow asychronously
+public void launch(String originator, String flowId, Map<String, Object> dataset,
+                       String correlationId) throws IOException;
+// launch a flow asychronously with tracing
+public void launch(String originator, String traceId, String tracePath, String flowId,
+                       Map<String, Object> dataset, String correlationId) throws IOException
+// launch a flow asychronously and tracing
+public void launch(PostOffice po, String flowId, Map<String, Object> dataset,
+                        String correlationId) throws IOException;
+// launch a flow with callback and tracing
+public void launch(PostOffice po, String flowId, Map<String, Object> dataset,
+                        String replyTo, String correlationId) throws IOException;
+// launch a flow and expect a future response
+public Future<EventEnvelope> request(PostOffice po, String flowId, Map<String, Object> dataset,
+                                     String correlationId, long timeout) throws IOException;
+
+

The following unit test emulates a HTTP request to the flow named "header-test".

+
@Test
+public void internalFlowTest() throws IOException, ExecutionException, InterruptedException {
+    final long TIMEOUT = 8000;
+    String traceId = Utility.getInstance().getUuid();
+    String cid = Utility.getInstance().getUuid();
+    PostOffice po = new PostOffice("unit.test", traceId, "INTERNAL /flow/test");
+    String flowId = "header-test";
+    Map<String, Object> headers = new HashMap<>();
+    Map<String, Object> dataset = new HashMap<>();
+    dataset.put("header", headers);
+    dataset.put("body", Map.of("hello", "world"));
+    headers.put("user-agent", "internal-flow");
+    headers.put("accept", "application/json");
+    headers.put("x-flow-id", flowId);
+    FlowExecutor flowExecutor = FlowExecutor.getInstance();
+    EventEnvelope result = flowExecutor.request(po, flowId, dataset, cid, TIMEOUT).get();
+    assertInstanceOf(Map.class, result.getBody());
+    Map<String, Object> body = (Map<String, Object>) result.getBody();
+    // verify that input headers are mapped to the function's input body
+    assertEquals("header-test", body.get("x-flow-id"));
+    assertEquals("internal-flow", body.get("user-agent"));
+    assertEquals("application/json", body.get("accept"));
+}
+
+

The dataset must contain at least the "body" key-value so that input data mapping is possible in a flow.

+

For the built-in HTTP flow adapter, the dataset would contain the following:

+
// convert HTTP context to flow "input" dataset
+Map<String, Object> dataset = new HashMap<>();
+dataset.put("header", request.getHeaders());
+dataset.put("body", request.getBody());
+dataset.put("cookie", request.getCookies());
+dataset.put("path_parameter", request.getPathParameters());
+dataset.put("method", request.getMethod());
+dataset.put("uri", request.getUrl());
+dataset.put("query", request.getQueryParameters());
+dataset.put("stream", request.getStreamRoute());
+dataset.put("ip", request.getRemoteIp());
+dataset.put("filename", request.getFileName());
+dataset.put("session", request.getSessionInfo());
+
+

If you write your own Kafka flow adapter, the dataset should contain headers and body mapped with a Kafka event.

+

For other flow adapters, you may use different set of key-values.

+


+ + + + + + + + + + + + + +
Appendix-IIHome
Reserved names and headersTable 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 00000000..1db835cd --- /dev/null +++ b/docs/guides/CHAPTER-1/index.html @@ -0,0 +1,556 @@ + + + + + + + + Chapter-1 - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Introduction

+

Mercury Composable is a software development toolkit for writing composable applications.

+

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 application architecture

+
+

Figure 1 - Composable application architecture

+
+

Composable Application Architecture

+

As shown in Figure 1, a composable application contains the following:

+
    +
  1. Flow adapters: Each flow adapter listens to requests for onwards delivery to an event manager.
  2. +
  3. Event Manager: it sends events to a set of user functions for them to work together as an application.
  4. +
  5. User functions: these are self-contained functions with clear input and output that are immutable.
  6. +
+

HTTP flow adapter

+

A non-blocking HTTP flow adapter is built-in. For other external interface types, you can implement your own +flow adapters. e.g. Adapters for MQ, Kafka, Serverless, File based staging area, etc.

+

The standard HTTP flow adapter leverages the underlying Mercury REST automation system to serve user facing REST +API endpoints. For example, a hypothetical "get profile" endpoint is created like this in the "rest.yaml" +configuration file:

+
  - service: "http.flow.adapter"
+    methods: ['GET']
+    url: "/api/profile/{profile_id}"
+    flow: 'get-profile'
+    timeout: 10s
+    cors: cors_1
+    headers: header_1
+    tracing: true
+
+

In this REST configuration entry, the system creates a REST API endpoint for "GET /api/profile/{profile_id}". +When a request arrives at this endpoint, the HTTP request will be converted to an incoming event by the flow adapter +that routes the event to the "event manager" to execute a new instance of the "get-profile" flow.

+

Flow configuration example

+

The event manager is driven by configuration instead of code. A hypothetical "get profile" flow is defined in +a YAML file like this:

+
flow:
+  id: 'get-profile'
+  description: 'Get a user profile using profile ID'
+  ttl: 10s
+  exception: 'v1.hello.exception'
+
+first.task: 'v1.get.profile'
+
+tasks:
+  - input:
+      - 'input.path_parameter.profile_id -> header.profile_id'
+    process: 'v1.get.profile'
+    output:
+      - 'result -> model.profile'
+    description: 'Retrieve user profile from database using profile_id'
+    execution: sequential
+    next:
+      - 'v1.decrypt.fields'
+
+  - input:
+      - 'model.profile -> dataset'
+      - 'text(telephone, address) -> protected_fields'
+    process: 'v1.decrypt.fields'
+    output:
+      - 'text(application/json) -> output.header.content-type'
+      - 'result -> output.body'
+    description: 'Decrypt fields'
+    execution: end
+
+  - input:
+      - 'error.code -> status'
+      - 'error.message -> message'
+      - 'error.stack -> stack'
+    process: 'v1.hello.exception'
+    output:
+      - 'result.status -> output.status'
+      - 'result -> output.body'
+    description: 'Just a demo exception handler'
+    execution: end
+
+

Note that the flow configuration is referring user functions by their "route" names. It is because all user functions +are self-contained with clearly defined input and output and the event manager would set their inputs and collect their +outputs accordingly. Note that you can map selected key-values or the whole event as a business object and this +decoupling promotes highly reusable user functional software.

+

The event manager will create a "state machine" to manage each transaction flow because all user functions are +stateless. The "state machine" is referenced using the namespace "model".

+

Assigning a route name to a user function

+

You can assign a route name to a Java class using the PreLoad annotation like this:

+
@PreLoad(route="v1.get.profile", instances=100)
+public class GetProfile implements TypedLambdaFunction<Map<String, Object>, Profile> {
+
+    @Override
+    public Profile handleEvent(Map<String, String> headers, Map<String, Object> input, int instance) {
+        // your business logic here
+        return result;
+    }
+}
+
+

Inside the "handleEvent" method, you can write regular Java code using your preferred coding style and +framework. You can define input/output as Map or PoJo.

+

Building the Mercury libraries from source

+

Mercury Composable leverages the best of Java 21 virtual threading technology. Therefore, you would need to +install Java JDK version 21 or higher. You also need maven version 3.9.7 or higher to build the libraries.

+

Assuming you clone the Mercury repository into the "sandbox" directory, you may build the libraries like this.

+
cd sandbox/mercury-composable
+mvn clean install
+
+

The compiled libraries will be saved to your local ".m2" maven repository. For convenience, you may also publish +the Mercury libraries into your enterprise artifactory.

+

We use "maven" build scripts. If your organization uses other build tools such as gradle, please convert them +accordingly.

+

Things to avoid with Java 21

+

By default, user functions are executed using Java 21 virtual threading technology. However, for performance reason, +there are two things that you MUST avoid:

+
    +
  1. Synchronized keyword: This will block the event loop in the Java VM, meaning that your whole application is + blocked when the synchronized block executes.
  2. +
  3. ThreadLocal: Java 21 virtual thread is designed to be very light weight. When you use ThreadLocal variables, + the "virtual thread" becomes heavy weighted and the Garbage Collector may have difficulties catching up.
  4. +
+

Since Mercury provides thread management abstraction, there is no need to use the Synchronized keyword and +ThreadLocal variables. The built-in "state machine" is a better place to keep your runtime variables for each +transaction.

+

Interestingly, the "Thread" and "Future" APIs are safe to use in a virtual thread.

+

If you are putting legacy code inside a new user function and the legacy code runs in blocking mode, you can +annotate the user function with the "KernelThreadRunner" class. This tells the system to turn on compatibility +mode to support the blocking code. The kernel thread would isolate the blocking code from the rest of the +application. However, kernel threads are limited resources. While virtual threads can support tens of thousands of +cooperative concurrent execution, kernel threads are limited to 250, depending on the number of CPU cores that the +target machine has.

+

Composable application example

+

Let's take a test drive of a composable application example in the "examples/composable-example" subproject.

+

You can use your favorite IDE to run the example or execute it from a terminal using command line.

+

To run it from the command line, you may do this:

+
cd sandbox/mercury-composable/examples/composable-example
+java -jar target/composable-example-4.0.9.jar
+
+

If you run the application from the IDE, you may execute the "main" method in the MainApp class under the +"com.accenture.demo.start" package folder.

+

The first step in designing a composable application is to draw an event flow diagram. This is similar to +a data flow diagram where the arrows are labeled with the event objects. Note that event flow diagram is +not a flow chart and thus decision box is not required. If a user function (also known as a "task") contains +decision logic, you can draw two or more output from the task to connect to the next set of functions. +For example, label the arrows as true, false or a number starting from 1.

+

The composable-example application is a hypothetical "profile management system" where you can create a profile, +browse or delete it.

+
+

Figure 2 - Event flow diagram

+
+

Event Flow Diagram

+

As shown in Figure 2, there are three event flows. One for "get profile", one for "delete profile" and the other +one for "create profile".

+

The REST endpoints for the three use cases are shown in the "rest.yaml" configuration file under the "main/resources" +in the example subproject.

+

You also find the following configuration parameters in "application.properties":

+
rest.server.port=8100
+rest.automation=true
+yaml.rest.automation=classpath:/rest.yaml
+yaml.flow.automation=classpath:/flows.yaml
+
+

The flow configuration files are shown in the "main/resources/flows" folder where you will find the flow configuration +files for the three event flows, namely get-profile.yml, delete-profile.yml and create-profile.yml.

+

Starting the application

+

When the application is started, you will see application log like this:

+
CompileFlows:142 - Loaded create-profile
+CompileFlows:142 - Loaded delete-profile
+CompileFlows:142 - Loaded get-profile
+CompileFlows:144 - Event scripts deployed: 3
+...
+ServiceQueue:91 - PRIVATE v1.get.profile with 100 instances started as virtual threads
+...
+RoutingEntry:582 - GET /api/profile/{profile_id} -> [http.flow.adapter], timeout=10s, tracing=true, flow=get-profile
+...
+AppStarter:378 - Modules loaded in 663 ms
+AppStarter:365 - Reactive HTTP server running on port-8100
+
+

Note that the above log is trimmed for presentation purpose.

+

It shows that the 3 flow configuration files are compiled as objects for performance reason. The user functions are +loaded into the event system and the REST endpoints are rendered from the "rest.yaml" file.

+

Testing the application

+

You can create a test user profile with this python code. Alternatively, you can also use PostMan or other means +to do this.

+
>>> import requests, json
+>>> d = { 'id': 12345, 'name': 'Hello World', 'address': '100 World Blvd', 'telephone': '123-456-7890' }
+>>> h = { 'content-type': 'application/json', 'accept': 'application/json' }
+>>> r = requests.post('http://127.0.0.1:8100/api/profile', data=json.dumps(d), headers=h)
+>>> print(r.status_code)
+201
+>>> print(r.text)
+{
+  "profile": {
+    "address": "***",
+    "name": "Hello World",
+    "telephone": "***",
+    "id": 12345
+  },
+  "type": "CREATE",
+  "secure": [
+    "address",
+    "telephone"
+  ]
+}
+
+

To verify that the user profile has been created, you can point your browser to

+
http://127.0.0.1:8100/api/profile/12345
+
+

Your browser will return the following:

+
{
+  "address": "100 World Blvd",
+  "name": "Hello World",
+  "telephone": "123-456-7890",
+  "id": 12345
+}
+
+

You have successfully tested the two REST endpoints. Tracing information in the application log may look like this:

+
DistributedTrace:76 - trace={path=POST /api/profile, service=http.flow.adapter, success=true, 
+                            origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.524Z, 
+                            exec_time=0.284, from=http.request, id=f6a6ae62340e43afb0a6f30445166e08}
+DistributedTrace:76 - trace={path=POST /api/profile, service=event.script.manager, success=true,
+                            origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.525Z,
+                            exec_time=0.57, from=http.flow.adapter, id=f6a6ae62340e43afb0a6f30445166e08}
+DistributedTrace:76 - trace={path=POST /api/profile, service=v1.create.profile, success=true,
+                            origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.526Z,
+                            exec_time=0.342, from=task.executor, id=f6a6ae62340e43afb0a6f30445166e08}
+DistributedTrace:76 - trace={path=POST /api/profile, service=async.http.response, success=true,
+                            origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.528Z,
+                            exec_time=0.294, from=task.executor, id=f6a6ae62340e43afb0a6f30445166e08}
+DistributedTrace:76 - trace={path=POST /api/profile, service=v1.encrypt.fields, success=true,
+                            origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.528Z,
+                            exec_time=3.64, from=task.executor, id=f6a6ae62340e43afb0a6f30445166e08}
+SaveProfile:52 - Profile 12345 saved
+TaskExecutor:186 - Flow create-profile (f6a6ae62340e43afb0a6f30445166e08) completed in 11 ms
+DistributedTrace:76 - trace={path=POST /api/profile, service=v1.save.profile, success=true,
+                            origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.533Z,
+                            exec_time=2.006, from=task.executor, id=f6a6ae62340e43afb0a6f30445166e08}
+
+DistributedTrace:76 - trace={path=GET /api/profile/12345, service=http.flow.adapter, success=true, 
+                            origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:52.089Z,
+                            exec_time=0.152, from=http.request, id=1a29105044e94cc3ac68aee002f6f429}
+DistributedTrace:76 - trace={path=GET /api/profile/12345, service=event.script.manager, success=true,
+                            origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:52.090Z,
+                            exec_time=0.291, from=http.flow.adapter, id=1a29105044e94cc3ac68aee002f6f429}
+DistributedTrace:76 - trace={path=GET /api/profile/12345, service=v1.get.profile, success=true,
+                            origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:52.091Z,
+                            exec_time=1.137, from=task.executor, id=1a29105044e94cc3ac68aee002f6f429}
+DistributedTrace:76 - trace={path=GET /api/profile/12345, service=v1.decrypt.fields, success=true, 
+                            origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:52.093Z,
+                            exec_time=1.22, from=task.executor, id=1a29105044e94cc3ac68aee002f6f429}
+TaskExecutor:186 - Flow get-profile (1a29105044e94cc3ac68aee002f6f429) completed in 4 ms
+DistributedTrace:76 - trace={path=GET /api/profile/12345, service=async.http.response, success=true, 
+                            origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:52.095Z, 
+                            exec_time=0.214, from=task.executor, id=1a29105044e94cc3ac68aee002f6f429}
+
+

Main module

+

Every application has an entry point. The MainApp in the example app contains the entry point 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");
+   }
+}
+
+

Since your application is event driven, the main application does not need any additional code in the above +example. However, this is a good place to put application initialization code if any.

+

There is also a "BeforeApplication" annotation if you want to run some start up code before the event system +is started.

+

Dependency management

+

As a best practice, your user functions should not have any dependencies with other user functions.

+

However, within a single user function, you may use your preferred framework or libraries. +For maintainability, we do recommend to reduce library dependencies as much as you can. For example, you want to +push JDBC or JPA dependency to a small number of user functions (for CRUD operation) so that the rest of the +user functions do not need any DB dependencies.

+

Component scan

+

Please update the following in the application.properties (or application.yml) to include packages of your own functions:

+
web.component.scan=your.package.name
+
+
+

You should replace "your.package.name" with the real package name(s) that you use in your application. + Usually this is your organization software ID or "namespace". + "web.component.scan" is a comma separated list of package names.

+
+

Deploy your application

+

Composable design can be used to create microservices. You can put related functions in a bounded context with +database persistence.

+

Each composable application 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 eclipse-temurin:21.0.1_12-jdk
+EXPOSE 8083
+WORKDIR /app
+COPY target/your-app-name.jar .
+ENTRYPOINT ["java","-jar","your-app-name.jar"]
+
+

The above Dockerfile will fetch Openjdk 21 packaged in "Ubuntu 22.04 LTS".

+

Event choreography by configuration

+

The best practice for composable design is event choreography by configuration (Event Script) discussed above. +We will examine the Event Script syntax in Chapter 4.

+

Generally, you do not need to use Mercury core APIs in your user functions.

+

For composable applications that use Event Script, Mercury core APIs (Platform, PostOffice and FastRPC) are only +required for writing unit tests, "custom flow adapters", "legacy functional wrappers" or "external gateways".

+

Orchestration by code

+

Orchestration by code is strongly discouraged because it would result in tightly coupled code.

+

For example, just an "Import" statement of another function would create tight coupling of two pieces of code, +even when using reactive or event-driven programming styles.

+

However, if there is a use case that you prefer to write orchestration logic by code, you may use the Mercury core APIs +to do event-driven programming. API overview will be covered in Chapter 9.

+


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

Function Execution Strategies

+

Define a function

+

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

+

Only flow adapter, data adapter, notification function or gateway has a single external dependency such as +a network event system, a database or an external REST resource.

+

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<MyPoJo, AnotherPoJo> {
+
+    @Override
+    public AnotherPojo handleEvent(Map<String, String> headers, MyPoJo input, int instance) {
+        // your business logic here
+        return result;
+    }
+}
+
+

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.

+

In the above example, the input is MyPoJo and the output is AnotherPoJo.

+

For event choreography, PoJos are treated as key-value Maps so that you can use the dot-bracket convention +to map subset of a PoJo from one function to another if needed.

+

When the input is used for a PoJo, you may also pass parameters to the user function as headers. We will discuss +this in Chapter 3 "Event Script syntax".

+

Non-blocking design

+

While you can apply sequential, object oriented or reactive programming styles in your functions, you should pay +attention to making your function non-blocking and fast.

+

In a virtual thread, if you use Java Future, the ".get()" method is synchronous but it is non-blocking behind the +curtain. This is like using the "await" keyword in other programming language.

+

Virtual thread execution promotes high performance and high concurrency. However, it would be suboptimal +if you mix blocking code in a user function. It will block the whole event loop, resulting in substantial +degradation of application performance. We therefore recommend your user function to be implemented in non-blocking +or reactive styles.

+

When you are using a reactive libaries in your function, your function can return a "Mono" or "Flux" reactive +response object using the Project-Reactor Core library. This feature is supported in Java and Kotlin.

+

For simplicity, we support only the Mono and Flux reactive response objects. If you use other types of reactive APIs, +please convert them into a Mono or Flux in the return value.

+

User function that returns a Mono object

+

For Mono return value, a reactive user function may look like this:

+
@PreLoad(route = "v1.reactive.mono.function")
+public class MonoUserFunction implements TypedLambdaFunction<Map<String, Object>, Mono<Map<String, Object>>> {
+    private static final Logger log = LoggerFactory.getLogger(MonoUserFunction.class);
+
+    private static final String EXCEPTION = "exception";
+
+    @Override
+    public Mono<Map<String, Object>> handleEvent(Map<String, String> headers, Map<String, Object> input, int instance) {
+        log.info("GOT {} {}", headers, input);
+        return Mono.create(callback -> {
+            if (headers.containsKey(EXCEPTION)) {
+                callback.error(new AppException(400, headers.get(EXCEPTION)));
+            } else {
+                callback.success(input);
+            }
+        });
+    }
+}
+
+

When you use reactive API in your function to connect to external resources such as a database, please ensure that the +reactive API is non-blocking. For example, when subscribing to a Mono publisher, you may need to add a "Scheduler" +before your subscribe statement. It may look something like this:

+
// obtain a virtual thread executor from the platform and apply it with the Mono's scheduler
+mono.subscribeOn(Schedulers.fromExecutor(Platform.getInstance().getVirtualThreadExecutor()))
+    .subscribe(responseConsumer, errorConsumer);
+
+

Without the scheduler, the subscribe statement will be blocked. Your next statement will not be reachable until +the mono has completed with data or exception.

+

User function that returns a Flux object

+

For Flux return value, it may look like this:

+
@PreLoad(route = "v1.reactive.flux.function")
+public class FluxUserFunction implements TypedLambdaFunction<Map<String, Object>, Flux<Map<String, Object>>> {
+    private static final Logger log = LoggerFactory.getLogger(FluxUserFunction.class);
+
+    private static final String EXCEPTION = "exception";
+    @Override
+    public Flux<Map<String, Object>> handleEvent(Map<String, String> headers, Map<String, Object> input, int instance) {
+        log.info("GOT {} {}", headers, input);
+        return Flux.create(emitter -> {
+            if (headers.containsKey(EXCEPTION)) {
+                emitter.error(new AppException(400, headers.get(EXCEPTION)));
+            } else {
+                // just generate two messages
+                emitter.next(Map.of("first", "message"));
+                emitter.next(input);
+                emitter.complete();
+            }
+        });
+    }
+}
+
+
+

Handling a Flux stream

+

When your function returns a Flux stream object, the system will pass the stream ID of the underlying event stream +to the calling function.

+

The input arguments for the event stream ID and time-to-live parameters are provided in the event headers +to your function that implements the TypedLambdaFunction or LambdaFunction.

+

The following event headers will be provided to the calling function:

+
x-stream-id: streamId
+x-ttl: ttl
+
+

In the calling function, you can create a FluxConsumer to handle the incoming event stream like this:

+
String streamId = headers.get("x-stream-id");
+long ttl = Utility.getInstance().str2long(headers.get("x-ttl"));
+FluxConsumer<Map<String, Object>> fc = new FluxConsumer<>(streamId, ttl);
+fc.consume(
+    data -> {
+        // handle incoming message
+    },
+    e -> {
+        // handle exception where e is a Throwable
+    },
+    () -> {
+        // handle stream completion
+    }
+);
+
+

Object serialization consideration

+

The system is designed to deliver Java primitive and HashMap through an event stream. If you pass Java +primitive such as String or byte[], you do not need to do any serialization.

+

If the objects that your function streams over a Mono or Flux channel are not supported, you must perform +custom serialization.

+

This can be achieved using the "map" method of the Mono or Flux class.

+

For example, your function obtains a stream of Flux result objects from a database call. You can serialize +the objects using a custom serializer like this:

+
// "source" is the original Flux object
+Flux<Map<String, Object> serializedStream = source.map(specialPoJo -> {
+    return myCustomSerializer.toMap(specialPoJo);
+});
+return serializedStream;
+
+

Your customSerializer should implement the org.platformlambda.core.models.CustomSerializer interface.

+
public interface CustomSerializer {
+    public Map<String, Object> toMap(Object obj);
+    public <T> T toPoJo(Object obj, Class<T> toValueType);
+}
+
+

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");
+
+

Alternatively, you may implement authentication as a user function in the first step of an event flow. In this case, +the input to the function is defined by the "input data mapping" rules in the event flow configuration.

+

The advantage of this approach is that authentication is shown as part of an event flow so that the application design +intention is clear.

+

A composable application is assembled from a collection of self-contained functions that are highly reusable.

+

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
Virtual threadHighest throughput in terms of
concurrent users. Functionally
similar to a suspend function.
N/A
Suspend functionSequential "non-blocking" for
RPC (request-response) that
makes code easier to read and
maintain
Requires coding in Kotlin language
Kernel threadsHighest performance in terms of
operations per seconds
Lower number of concurrent threads
due to high context switching overheads
+

Virtual thread

+

By default, the system will run your function as a virtual thread because this is the most efficient execution +strategy.

+

The "Thread" object in the standard library will operate in non-blocking mode. This means it is safe to use +the Thread.sleep() method. It will release control to the event loop when your function enters into sleep, thus +freeing CPU resources for other functions.

+

We have added the "request" methods in the PostOffice API to support non-blocking RPC that leverages the virtual +thread resource suspend/resume functionality.

+
Future<EventEnvelope> future = po.request(requestEvent, timeout);
+EventEnvelope result = future.get();
+
+// alternatively, you can do:
+EventEnvelope result = po.request(requestEvent, timeout).get();
+
+
+

The PostOffice API is used when you want to do orchestration by code. If you are using Event Script, you can + manage event flows using one or more configuration files.

+
+

Suspend function

+

If you prefer writing business logic in Kotlin, you may use suspend function.

+

Similar to virtual thread, 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.

+
+

Do not block your function because it may block all coroutines since they run in a single kernel thread

+
+

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.

+
+

The FastRPC API is used when you want to do orchestration by code. If you are using Event Script, you can + manage event flows using one or more configuration files.

+
+

Kernel thread pool

+

When you add the annotation "KernelThreadRunner" in a function declared as LambdaFunction or TypedLambdaFunction, +the function will be executed using a "kernel thread pool" and Java 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 should write "asynchronous" code. i.e. for event-driven programming, +you can use 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 virtual thread or suspend function to handle synchronous RPC calls +in a non-blocking manner.

+

Solving the puzzle of multithreading performance

+

Before the availability of virtual thread technology, Java VM is using kernel threads for code execution. +If you have a lot of users hitting your service concurrently, multiple threads are created to serve concurrent +requests.

+

When your code serving the requests make blocking call to other services, the kernel threads are busy while your +user functions wait for responses. Kernel threads that are in the wait state is consuming CPU time.

+

If the blocking calls finish very quickly, this may not be an issue.

+

However, when the blocking calls take longer to complete, a lot of outstanding kernel threads that are waiting +for responses would compete for CPU resources, resulting in higher internal friction in the JVM that makes your +application running slower. This is not a productive use of computer resources.

+

This type of performance issue caused by internal friction is very difficult to avoid. While event driven and +reactive programming that uses asynchronous processing and callbacks would address this artificial bottleneck, +asynchronous code is harder to implement and maintain when the application complexity increases.

+

It would be ideal if we can write sequential code that does not block. Sequential code is much easier to write +and read because it communicates the intent of the code clearly.

+

Leveraging Java 21 virtual thread, Mercury Composable allows the developer to write code in a sequential manner. +When code in your function makes an RPC call to another service using the PostOffice's "request" API, it returns +a Java Future object but the "Future" object itself is running in a virtual thread. This means when your code +retrieves the RPC result using the "get" method, your code appears "blocked" while waiting for the response +from the target service.

+

Although your code appears to be "blocked", the virtual thread is “suspended”. It will wake up when the response +arrives. When a virtual thread is suspended, it does not consume CPU time and the memory structure for keeping +the thread in suspend mode is very small. Virtual thread technology is designed to support tens of thousands, +if not millions, of concurrent RPC requests in a single compute machine, container or serverless instance.

+

Mercury Composable supports mixed thread management - virtual threads, suspend functions and kernel threads.

+

Functions running in different types of threads are connected loosely in events. This functional isolation +and encapsulation mean that you can precisely control how your application performs for each functional logic block.

+


+ + + + + + + + + + + + + + + +
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 00000000..b48581c4 --- /dev/null +++ b/docs/guides/CHAPTER-3/index.html @@ -0,0 +1,465 @@ + + + + + + + + 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.

+

Support of multiple configuration files

+

You can configure more than one location and the system will search and merge them sequentially. +The following example tells the system to merge the rest.yaml config files in the /tmp/config folder +and the project's resources folder.

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

Duplicated REST endpoints

+

The system will detect duplicated REST endpoint configuation. If there is a duplicated entry, it will +abort the REST endpoint rendering. Your unit tests will fail because REST endpoints are not enabled.

+

The application log may look like this:

+
INFO - Loading config from classpath:/rest.yaml
+INFO - Loading config from classpath:/event-api.yaml
+ERROR - REST endpoint rendering aborted due to duplicated entry 'POST /api/event' in classpath:/event-api.yaml
+
+

Please correct the rest.yaml configuration files and rebuild your application again.

+

Duplicated static content, cors and headers sections

+

When duplicated entry is detected, the subsequent one will replace the prior one. A warning will be +shown in the application log like this:

+
WARN - Duplicated 'static-content' in classpath:/duplicated-endpoint.yaml will override a prior one
+WARN - Duplicated 'cors' in classpath:/duplicated-endpoint.yaml will override a prior one 'cors_1'
+WARN - Duplicated 'headers' in classpath:/duplicated-endpoint.yaml will override a prior one 'header_1'
+
+

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"
+
+

The 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 Script Syntax
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/CHAPTER-4/index.html b/docs/guides/CHAPTER-4/index.html new file mode 100644 index 00000000..30a004dd --- /dev/null +++ b/docs/guides/CHAPTER-4/index.html @@ -0,0 +1,1449 @@ + + + + + + + + Chapter-4 - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Event Script Syntax

+

Event Script uses YAML to represent an end-to-end transaction flow. A transaction is a business use case, and +the flow can be an API service, a batch job or a real-time transaction.

+

Flow list

+

This configuration file sits in the project "resources" project and contains a list of filenames.

+

The default flow list is "flows.yaml" under the "resources" folder. It may look like this.

+
flows:
+  - 'get-profile.yml'
+  - 'create-profile.yml'
+  - 'delete-profile.yml'
+
+location: 'classpath:/flows/'
+
+

The "location" tag is optional. If present, you can tell the system to load the flow config files from +another folder location.

+

Multiple flow lists

+

You can provide more than one flow list to your application and it can become very handy under different +situations. For instance, to achieve better modularity in complex application, flows can be grouped to +multiple categories based on development team's choice and these flows can be managed in multiple flow +lists. Another great place to use multiple flow list is to include external libraries which contain +pre-defined flow lists. The following example demonstrates that an application loads a list of flows +defined in "flows.yaml" and additional flows defined in "more-flows.yaml" file of a composable library.

+
yaml.flow.automation=classpath:/flows.yaml, classpath:/more-flows.yaml
+
+

Writing new REST endpoint and function

+

You can use the "flow-demo" subproject as a template to write your own composable application.

+

For each filename in the flows.yml, you should create a corresponding configuration file under the +"resources/flows" folder.

+

Let's write a new flow called "greetings". You can copy-n-paste the following into a file called "greetings.yml" +under the "resources/flows" folder.

+
flow:
+  id: 'greetings'
+  description: 'Simplest flow'
+  ttl: 10s
+
+first.task: 'greeting.demo'
+
+tasks:
+  - input:
+      - 'input.path_parameter.user -> user'
+    process: 'greeting.demo'
+    output:
+      - 'text(application/json) -> output.header.content-type'
+      - 'result -> output.body'
+    description: 'Hello World'
+    execution: end
+
+

In the application.properties, you can specify the following parameter:

+
yaml.flow.automation=classpath:/flows.yaml
+
+

and update the "flows.yaml" file in the resources folder as follows:

+
flows:
+  - 'get-profile.yml'
+  - 'create-profile.yml'
+  - 'delete-profile.yml'
+  - 'greetings.yml'
+
+

Then, you can add a new REST endpoint in the "rest.yaml" configuration file like this.

+
  - service: "http.flow.adapter"
+    methods: ['GET']
+    url: "/api/greetings/{user}"
+    flow: 'greetings'
+    timeout: 10s
+    cors: cors_1
+    headers: header_1
+
+

The above REST endpoint takes the path parameter "user". The task executor will map the path parameter to the +input arguments (headers and body) in your function. Now you can write your new function with the named route +"greeting.demo". Please copy-n-paste the following into a Java class called "Greetings" and save in the package +under "my.organization.tasks" in the source project.

+
+

Note: "my.organization" package name is an example. Please replace it with your organization package path.

+
+
@PreLoad(route="greeting.demo", instances=10, isPrivate = false)
+public class Greetings implements TypedLambdaFunction<Map<String, Object>, Map<String, Object>> {
+    private static final String USER = "user";
+
+    @Override
+    public Map<String, Object> handleEvent(Map<String, String> headers, Map<String, Object> input, int instance) {
+        if (input.containsKey(USER)) {
+            String user = input.get(USER).toString();
+            Map<String, Object> result = new HashMap<>();
+            result.put(USER, user);
+            result.put("message", "Welcome");
+            result.put("time", new Date());
+            return result;
+        } else {
+            throw new IllegalArgumentException("Missing path parameter 'user'");
+        }
+    }
+}
+
+

For the flow-engine to find your new function, please update the key-value for "web.component.scan" in +application.properties:

+
web.component.scan=my.organization
+
+

To test your new REST endpoint, flow configuration and function, please point your browser to

+
http://127.0.0.1:8100/api/greetings/my_name
+
+

You can replace "my_name" with your first name to see the response to the browser.

+

Flow configuration syntax

+

In your "greetings.yml" file above, you find the following key-values:

+

flow.id - Each flow must have a unique flow ID. The flow ID is usually originated from a user facing endpoint +through an event adapter. For example, you may write an adapter to listen to a cloud event in a serverless deployment. +In The most common one is the HTTP adapter.

+

The flow ID is originated from the "rest.yaml". The flow-engine will find the corresponding flow configuration +and create a new flow instance to process the user request.

+

flow.description - this describes the purpose of the flow

+

flow.ttl - "Time to live (TTL)" timer for each flow. You can define the maximum time for a flow to finish processing. +All events are delivered asynchronously and there is no timeout value for each event. The TTL defines the time budget +for a complete end-to-end flow. Upon expiry, an unfinished flow will be aborted.

+

first.task - this points to the route name of a function (aka "task") to which the flow engine will deliver +the incoming event.

+

The configuration file contains a list of task entries where each task is defined by "input", "process", "output" +and "execution" type. In the above example, the execution type is "end", meaning that it is the end of a transaction +and its result set will be delivered to the user.

+

Underlying Event System

+

The Event Script system uses platform-core as the event system where it encapsulates Java Virtual Threads, +Eclipse Vertx, Kotlin coroutine and suspend function.

+

The integration points are intentionally minimalist. For most use cases, the user application does not need +to make any API calls to the underlying event system.

+

REST automation and HTTP adapter

+

The most common transaction entry point is a REST endpoint. The event flow may look like this:

+
Request -> "http.request" -> "task.executor" -> user defined tasks
+        -> "async.http.response" -> Response
+
+

REST automation is part of the Mercury platform-core library. It contains a non-blocking HTTP server that converts +HTTP requests and responses into events.

+

It routes an HTTP request event to the HTTP adapter if the "flow" tag is provided.

+

In the following example, the REST endpoint definition is declared in a "rest.yaml" configuration. It will route +the URI "/api/decision" to the HTTP adapter that exposes its service route name as "http.flow.adapter".

+
rest:
+  - service: "http.flow.adapter"
+    methods: ['GET']
+    url: "/api/decision?decision=_"
+    flow: 'decision-test'
+    timeout: 10s
+    cors: cors_1
+    headers: header_1
+    tracing: true
+
+

The "cors" and "headers" tags are optional. When specified, the REST endpoint will insert CORS headers and HTTP request +headers accordingly.

+

For rest.yaml syntax, please refer to https://accenture.github.io/mercury-composable/guides/CHAPTER-3

+

The HTTP adapter maps the HTTP request dataset and the flow ID into a standard event envelope for delivery to the +flow engine.

+

The HTTP request dataset, addressable with the "input." namespace, contains the following:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyValues
methodHTTP method
uriURI path
headerHTTP headers
cookieHTTP cookies
path_parameterPath parameters if any
queryHTTP query parameters if any
bodyHTTP request body if any
streaminput stream route ID if any
ipremote IP address
filenamefilename if request is a multipart file upload
sessionauthenticated session key-values if any
+

For easy matching, keys of headers, cookies, query and path parameters are case-insensitive.

+

Regular API uses JSON and XML and they will be converted to a hashmap in the event's body.

+

For special use cases like file upload/download, your application logic may invoke a streaming API to retrieve +the binary payload. Please refer to the following sections for details.

+

https://accenture.github.io/mercury-composable/guides/APPENDIX-III/#send-http-request-body-as-a-stream

+

https://accenture.github.io/mercury-composable/guides/APPENDIX-III/#read-http-response-body-stream

+

Task and its corresponding function

+

Each task in a flow must have a corresponding function. You can assign a task name to the function using the +Preload annotation like this.

+
@PreLoad(route="greeting.demo", instances=10)
+public class Greetings implements TypedLambdaFunction<Map<String, Object>, Map<String, Object>> {
+    @Override
+    public Map<String, Object> handleEvent(Map<String, String> headers, Map<String, Object> input, int instance) {
+        // business logic here
+        return someOutput;
+    }
+}
+
+

The "route" in the Preload annotation is the task name. The "instances" define the maximum number of "workers" that +the function can handle concurrently. The system is designed to be reactive and the function does not consume memory +and CPU resources until an event arrives.

+

You may also define concurrency using environment variable. You can replace the "instances" with envInstances using +standard environment variable syntax like ${SOME_ENV_VARIABLE:default_value}.

+

Unique task naming

+

Composable functions are designed to be reusable. By changing some input data mapping to feed different parameters and +payload, your function can behave differently.

+

Therefore, it is quite common to use the same function ("process") more than once in a single event flow.

+

When a task is not named, the "process" tag is used to name the task.

+

Since each task must have a unique name for event routing, we cannot use the same "process" name more than once in an +event flow. To handle this use case, you can create unique names for the same function (i.e. "process") like this:

+
flow:
+  id: 'greetings'
+  description: 'Simplest flow'
+  ttl: 10s
+
+first.task: 'my.first.task'
+
+tasks:
+  - name: 'my.first.task'
+    input:
+      - 'input.path_parameter.user -> user'
+    process: 'greeting.demo'
+    output:
+      - 'text(application/json) -> output.header.content-type'
+      - 'result -> output.body'
+    description: 'Hello World'
+    execution: sequential
+    next:
+      - 'another.task'
+
+

The above event flow configuration uses "my.first.task" as a named route for "greeting.demo" by adding the +"name" tag to the composable function.

+

For configuration simplicity, the "name" tag is optional. If not provided, the process name is assumed to be +the unique "task" name.

+
+

Important: The Event Manager performs event choreography using the unique task name. + Therefore, when the "process" name for the function is not unique, you must create unique task "names" + for the same function to ensure correct routing.

+
+

Assigning multiple route names to a single function

+

The built-in distributed tracing system tracks the actual composable functions using the "process" name +and not the task names.

+

When there is a need to track the task names in distributed trace, you can tell the system to create +additional instances of the same function with different route names.

+

You can use a comma separated list as the route name like this:

+
@PreLoad(route="greeting.case.1, greeting.case.2", instances=10)
+public class Greetings implements TypedLambdaFunction<Map<String, Object>, Map<String, Object>> {
+
+  @Override
+  public Map<String, Object> handleEvent(Map<String, String> headers, Map<String, Object> input, int instance) {
+      // business logic here
+      return someResult;
+  }
+}
+
+
+

Note: The "unique task naming" method is more memory efficient than creating additional route names

+
+

Preload overrides

+

Once a composable function is published as a reusable library in the artifactory, its route name and +number of instances are fixed using the "PreLoad" annotation in the function class.

+

Without refactoring your libary, you can override its route name and instances using a preload override +file like this:

+
preload:
+  - original: 'greeting.demo'
+    routes:
+      - 'greeting.case.1'
+      - 'greeting.case.2'
+    # the "instances" tag is optional
+    instances: 20
+  - original: 'v1.another.reusable.function'
+    keep-original: true
+    routes:
+      - 'v1.reusable.1'
+      - 'v1.reusable.2'
+
+

In the above example, the function associated with "greeting.demo" will be preloaded as "greeting.case.1" +and "greeting.case.2". The number of maximum concurrent instances is also changed from 10 to 20.

+

In the second example, "v1.another.reusable.function" is updated as "v1.reusable.1" and "v1.reusable.2" +and the number of concurrent instances is not changed. The original route "v1.another.reusable.function" is +preserved when the "keep-original" parameter is set to true.

+

Assuming the above file is "preload-override.yaml" in the "resources" folder of the application source code +project, you should add the following parameter in application.properties to activate this preload override +feature.

+
yaml.preload.override=classpath:/preload-override.yaml
+
+

Multiple preload override config files

+

When you publish a composable function as a library, you may want to ensure the route names of the functions are +merged properly. In this case, you can bundle a library specific preload override config file.

+

For example, your library contains a "preload-kafka.yaml" to override some route names, you can add it to the +yaml.preload.override parameter like this:

+
yaml.preload.override=classpath:/preload-override.yaml, classpath:/preload-kafka.yaml
+
+

The system will then merge the two preload override config files.

+

The concurrency value of a function is overwritten using the "instances" parameter in the first preload override file. +Subsequent override of the "instances" parameter is ignored. i.e. the first preload override file will take precedence.

+

Hierarchy of flows

+

Inside a flow, you can run one or more sub-flows.

+

To do this, you can use the flow protocol identifier (flow://) to indicate that the task is a flow.

+

For example, when running the following task, "flow://my-sub-flow" will be executed like a regular task.

+
tasks:
+  - input:
+      - 'input.path_parameter.user -> header.user'
+      - 'input.body -> body'
+    process: 'flow://my-sub-flow'
+    output:
+      - 'result -> model.pojo'
+    description: 'Execute a sub-flow'
+    execution: sequential
+    next:
+      - 'my.next.function'
+
+

If the sub-flow is not available, the system will throw an error stating that it is not found.

+

Hierarchy of flows would reduce the complexity of a single flow configuration file. The "time-to-live (TTL)" +value of the parent flow should be set to a value that covers the complete flow including the time used in +the sub-flows.

+

For simplicity, the input data mapping for a sub-flow should contain only the "header" and "body" arguments.

+

Tasks and data mapping

+

All tasks for a flow are defined in the "tasks" section.

+

Input/Output data mapping

+

A function is self-contained. This modularity reduces application complexity because the developer only needs +interface contract details for a specific function.

+

To handle this level of modularity, the system provides configurable input/output data mapping.

+

Namespaces for I/O data mapping

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeKeyword and/or namespaceLHS / RHSMappings
Flow input datasetinput.leftinput
Flow output datasetoutput.rightoutput
Function input bodyno namespace requiredrightinput
Function input or output headersheader or header.rightI/O
Function output result setresult.leftoutput
Function output status codestatusleftoutput
Decision valuedecisionrightoutput
State machine datasetmodel.left/rightI/O
External state machine key-valueext:rightI/O
+

Note that external state machine namespace uses ":" to indicate that the key-value is external.

+

Constants for input data mapping

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeKeyword for the left-hand-side argument
Stringtext(example_value)
Integerint(number)
Longlong(number)
Floatfloat(number)
Doubledouble(number)
Booleanboolean(true or false)
Mapmap(k1=v1, k2=v2)
map(base.config.parameter)
Filefile(text:file_path)
file(binary:file_path)
Classpathclasspath(text:file_path)
classpath(binary:file_path)
+

For input data mapping, the "file" constant type is used to load some file content as an argument of a user function. +You can tell the system to render the file as "text" or "binary". Similarly, the "classpath" constant type refers +to static file in the application source code's "resources" folder.

+

The "map" constant type is used for two purposes:

+

1. Map of key-values

+

The following example illustrates creation of a map of key-values. In the first entry, a map of 2 key-values +is set as the input argument "myMap" of a user function. In the second entry, the map's values are retrieved +from the key "some.key" in base configuration and the environment variable "ENV_VAR_ONE".

+
'map(k1=v1, k2=v2) -> myMap'
+'map(k1=${some.key}, k2=${ENV_VAR_ONE}) -> myMap'
+
+

Note that the comma character is used as a separator for each key-value pair. If the value contains a comma, +the system cannot parse the key-values correctly. In this case, please use the 2nd method below.

+

2. Mapping values from application.yml

+

The following input data mapping sets the value of "my.key" from the application.yml base configuration file +to the input argument "myKey" of a user function.

+
'map(my.key) -> myKey'
+
+

Since the system uses both application.properties and application.yml as base configuration files, +you can use either configuration files depending on the data type of the value.

+

For application.properties, "map(my.key)" is the same as "text(${my.key})".

+

For application.yml, "map(my.key)" would set a primitive value (text, integer, float, boolean), +a hash map of key-values or an array of values.

+

Special content type for output data mapping

+ + + + + + + + + + + + + +
TypeKeyword for the right-hand-side argument
Filefile(file_path)
+

For output data mapping, the "file" content type is used to save some data from the output of a user function +to a file in the local file system.

+

Decison value

+

The "decision" keyword applies to "right hand side" of output data mapping statement in a decision task only +(See "Decision" in the task section).

+

Each flow has its own input and output

+

Each function has its input headers, input body and output result set. +Optionally, a function can return an EventEnvelope object to hold its result set in the "body", a "status" code +and one or more header key-values.

+

Since each function is stateless, a state machine (with namespace model.) is available as a temporary memory store +for transaction states that can be passed from one task to another.

+

All variables are addressable using the standard dot-bracket convention.

+

For example, "hello.world" will retrieve the value 100 from this data structure:

+
{
+  "hello":  {
+    "world": 100
+  }
+}
+
+

and "numbers[1]" will retrieve the value 200 below:

+
{ "numbers":  [100, 200] }
+
+

The assignment is done using the -> syntax.

+

In the following example, the HTTP input query parameter 'amount' is passed as input body argument 'amount' +to the task 'simple.decision'. The result (function "return value") from the task will be mapped to the +special "decision" variable that the flow engine will evaluate. This assumes the result is a boolean or +numeric value.

+

The "decision" value is also saved to the state machine (model) for subsequent tasks to evaluate.

+
  - input:
+      - 'input.query.amount -> amount'
+    process: 'simple.decision'
+    output:
+      - 'result -> decision'
+      - 'result -> model.decision'
+
+

Metadata for each flow instance

+

For each flow instance, the state machine in the "model" namespace provides the following metadata that +you can use in the input/output data mapping. For example, you can set this for an exception handler to +log additional information.

+ + + + + + + + + + + + + + + + + + + + + + + + + +
TypeKeywordComment
Flow IDmodel.flowThe ID of the event flow config
Trace IDmodel.traceOptional traceId when tracing is turned on
Correlation IDmodel.cidCorrelation ID of the inbound request
+

Special handling for header

+

When function input keyword header is specified in the "right hand side" of an input data mapping statement, +it refers to the input event envelope's headers. Therefore, it assumes the "left hand side" to resolve into +a Map object of key-values. Otherwise, it will reject the input data mapping statement with an error like this:

+
Invalid input mapping 'text(ok) -> header', expect: Map, Actual: String
+
+

When function input namespace header. is used, the system will map the value resolved from the "left hand side" +statement into the specific header.

+

For example, the input data mapping statement text(ok) -> header.demo will set "demo=ok" into input event +envelope's headers.

+

When function output keyword header is specified in the "left hand side" of an output data mapping statement, +it will resolve as a Map from the function output event envelope's headers.

+

Similarly, when function output namespace header. is used, the system will resolve the value from a specific +key of the function output event envelope's headers.

+

Function input and output

+

To support flexible input data mapping, the input to a function must be either Map<String, Object> or PoJo. +The output (i.e. result set) of a function can be Map, PoJo or Java primitive.

+

Your function can implement the TypedLambdaFunction interface to configure input and output.

+

Since a data structure is passed to your function's input argument as key-values, you may create a PoJo class +to deserialize the data structure.

+

To tell the system that your function is expecting input as a PoJo, you can use the special notation * in +the right hand side.

+

For example, the following entry tells the system to set the value in "model.dataset" as a PoJo input.

+
  - input:
+      - 'model.dataset -> *'
+
+
+

If the value from the left hand side is not a map, the system will ignore the input mapping command and +print out an error message in the application log.

+
+

Setting function input headers

+

When function input body is used to hold a PoJo, we may use function input headers to pass other arguments +to the function without changing the data structure of a user defined PoJo.

+

In the following example, the HTTP query parameter "userid" will be mappped to the function input header +key "user" and the HTTP request body will be mapped to the function input body.

+
  - input:
+      - 'input.query.userid -> header.user'
+      - 'input.body -> *'
+    process: 'my.user.function'
+    output:
+      - 'text(application/json) -> output.header.content-type'
+      - 'result -> output.body'
+
+

Task types

+

Decision task

+

A decision task makes decision to select the next task to execute. It has the tag execution=decision.

+

In the output data mapping section, it must map the corresponding result set or its key-value to the decision object.

+

The "next" tag contains a list of tasks to be selected based on the decision value.

+

If decision value is boolean, a true value will select the first task. Otherwise, the second task will be selected.

+

If decision value is an integer, the number should start from 1 where the corresponding "next" task +will be selected.

+
tasks:
+  - input:
+      - 'input.query.decision -> decision'
+    process: 'simple.decision'
+    output:
+      - 'result -> model.decision'
+      - 'result -> decision'
+    description: 'Simple decision test'
+    execution: decision
+    next:
+      - 'decision.case.one'
+      - 'decision.case.two'
+
+

Response task

+

A response task will provide result set as a flow output or "response". A response task allows the flow to respond +to the user or caller immediately and then move on to the next task asynchronously. For example, telling the user +that it has accepted a request and then moving on to process the request that may take longer time to run.

+

A response task has the tag execution=response and a "next" task.

+
tasks:
+  - input:
+      - 'input.path_parameter.user -> user'
+      - 'input.query.seq -> sequence'
+    process: 'sequential.one'
+    output:
+      - 'result -> model.pojo'
+      - 'result -> output.body'
+    description: 'Pass a pojo to another task'
+    execution: response
+    next:
+      - 'sequential.two'
+
+

End task

+

An end task indicates that it is the last task of the transaction processing in a flow. If the flow has not executed +a response task, the end task will generate the response. Response is defined by output data mapping.

+

This task has the tag execution=end.

+

For example, the greeting task in the unit tests is an end task.

+
    - input:
+        - 'input.path_parameter.user -> user'
+      process: 'greeting.demo'
+      output:
+        - 'text(application/json) -> output.header.content-type'
+        - 'result -> output.body'
+      description: 'Hello World'
+      execution: end
+
+

Sequential task

+

Upon completion of a sequential task, the next task will be executed.

+

This task has the tag execution=sequential.

+

In the following example, sequential.two will be executed after sequential.one.

+
tasks:
+  - input:
+      - 'input.path_parameter.user -> user'
+      - 'input.query.seq -> sequence'
+    process: 'sequential.one'
+    output:
+      - 'result -> model.pojo'
+    description: 'Pass a pojo to another task'
+    execution: sequential
+    next:
+      - 'sequential.two'
+
+

Parallel task

+

Upon completion of a parallel task, all tasks in the "next" task list will be executed in parallel.

+

This task has the tag execution=parallel.

+

In this example, parallel.one and parallel.two will run after begin.parallel.test

+
tasks:
+  - input:
+      - 'int(2) -> count'
+    process: 'begin.parallel.test'
+    output: []
+    description: 'Setup counter for two parallel tasks'
+    execution: parallel
+    next:
+      - 'parallel.one'
+      - 'parallel.two'
+
+

Fork-n-join task

+

Fork-n-join is a parallel processing pattern.

+

A "fork" task will execute multiple "next" tasks in parallel and then consolidate the result sets before running +the "join" task.

+

This task has the tag execution=fork. It must have a list of "next" tasks and a "join" task.

+

It may look like this:

+
tasks:
+  - input:
+      - 'input.path_parameter.user -> user'
+      - 'input.query.seq -> sequence'
+    process: 'sequential.one'
+    output:
+      - 'result -> model.pojo'
+    description: 'Pass a pojo to another task'
+    execution: fork
+    next:
+      - 'echo.one'
+      - 'echo.two'
+    join: 'join.task'
+
+

Sink task

+

A sink task is a task without any next tasks. Sink tasks are used by fork-n-join and pipeline tasks as reusable modules.

+

This task has the tag execution=sink.

+
  - input:
+      - 'text(hello-world-two) -> key2'
+    process: 'echo.two'
+    output:
+      - 'result.key2 -> model.key2'
+    description: 'Hello world'
+    execution: sink
+
+

Pipeline feature

+

Pipeline is an advanced feature of Event Script.

+

Pipeline task

+

A pipeline is a list of tasks that will be executed orderly within the current task.

+

When the pipeline is done, the system will execute the "next" task.

+

This task has the tag execution=pipeline.

+
tasks:
+  - input:
+      - 'input.path_parameter.user -> user'
+      - 'input.query.seq -> sequence'
+    process: 'sequential.one'
+    output:
+      - 'result -> model.pojo'
+    description: 'Pass a pojo to another task'
+    execution: pipeline
+    pipeline:
+      - 'echo.one'
+      - 'echo.two'
+    next:
+      - 'echo.three'
+
+

Some special uses of pipelines include "for/while-loop" and "continue/break" features.

+

Simple for-loop

+

In the following example, the loop.statement contains a for-loop that uses a variable in the state machine to +evaluate the loop.

+

In this example, the pipeline will be executed three times before passing control to the "next" task.

+
tasks:
+  - input:
+      - 'input.path_parameter.user -> user'
+      - 'input.query.seq -> sequence'
+    process: 'sequential.one'
+    output:
+      - 'result -> model.pojo'
+    description: 'Pass a pojo to another task'
+    execution: pipeline
+    loop:
+      statement: 'for (model.n = 0; model.n < 3; model.n++)'
+    pipeline:
+      - 'echo.one'
+      - 'echo.two'
+      - 'echo.three'
+    next:
+      - 'echo.four'
+
+

Simple while loop

+

The loop.statement may use a "while loop" syntax like this:

+
    loop:
+      statement: 'while (model.running)'
+
+

To exit the above while loop, one of the functions in the pipeline should return a boolean "false" value with +output "data mapping" to the model.running variable.

+

For loop with break/continue decision

+

In the following example, the system will evaluate if the model.quit variable is true. +If yes, the break or continue condition will be executed.

+

The state variable is obtained after output data mapping and any task in the pipeline can set a key-value for +mapping into the state variable.

+
tasks:
+  - input:
+      - 'input.path_parameter.user -> user'
+      - 'input.query.seq -> sequence'
+    process: 'sequential.one'
+    output:
+      - 'result -> model.pojo'
+    description: 'Pass a pojo to another task'
+    execution: pipeline
+    loop:
+      statement: 'for (model.n = 0; model.n < 3; model.n++)'
+      condition:
+        - 'if (model.quit) break'
+    pipeline:
+      - 'echo.one'
+      - 'echo.two'
+      - 'echo.three'
+    next:
+      - 'echo.four'
+
+

Handling exception

+

You can define exception handler at the top level or at the task level.

+

Exception is said to occur when a user function throws exception or returns an EventEnvelope object with +a status code equals to or larger than 400.

+

The event status uses the same numbering scheme as HTTP exception status code. +Therefore, status code less than 400 is not considered an exception.

+

Top-level exception handler

+

Top-level exception handler is a "catch-all" handler. You can define it like this:

+
flow:
+  id: 'greetings'
+  description: 'Simplest flow of one task'
+  ttl: 10s
+  exception: 'v1.my.exception.handler'
+
+

In this example, the v1.my.exception.handler should point to a corresponding exception handler that you provide.

+

The following input arguments will be delivered to your function when exception happens.

+ + + + + + + + + + + + + + + + + + + + + +
KeyDescription
statusException status code
messageError message
stackStack trace in a text string
+

The exception handler function can be an "end" task to abort the transaction or a decision task +to take care of the exception. For example, the exception handler can be a "circuit-breaker" to retry a request.

+

Task-level exception handler

+

You can attach an exception handler to a task. One typical use is the "circuit breaker" pattern. +In the following example, the user function "breakable.function" may throw an exception for some error condition. +The exception will be caught by the "v1.circuit.breaker" function.

+
  - input:
+      - 'input.path_parameter.accept -> accept'
+      - 'model.attempt -> attempt'
+    process: 'breakable.function'
+    output:
+      - 'int(0) -> model.attempt'
+      - 'text(application/json) -> output.header.content-type'
+      - 'result -> output.body'
+    description: 'This demo function will break until the "accept" number is reached'
+    execution: end
+    exception: 'v1.circuit.breaker'
+
+

The configuration for the circuit breaker function may look like this:

+
  - input:
+      - 'model.attempt -> attempt'
+      - 'int(2) -> max_attempts'
+      - 'error.code -> status'
+      - 'error.message -> message'
+      - 'error.stack -> stack'
+    process: 'v1.circuit.breaker'
+    output:
+      - 'result.attempt -> model.attempt'
+      - 'result.decision -> decision'
+      - 'result.status -> model.status'
+      - 'result.message -> model.message'
+    description: 'Just a demo circuit breaker'
+    execution: decision
+    next:
+      - 'breakable.function'
+      - 'abort.request'
+
+

An exception handler will be provided with the "error" object that contains error code, error message and an exception +stack trace. The exception handler can inspect the error object to make decision of the next step.

+

For circuit breaker, we can keep the number of retry attempts in the state machine under "model.attempt" or any +key name that you prefer. In the above example, it sets an integer constant of 2 for the maximum attempts.

+

The circuit breaker can then evaluate if the number of attempts is less than the maximum attempts. If yes, it will +return a decision of "true" value to tell the system to route to the "breakable.function" again. Otherwise, it will +return a decision of "false" value to abort the request.

+

A more sophisticated circuit breaker may be configured with "alternative execution paths" depending on the error +status and stack trace. In this case, the decision value can be a number from 1 to n that corresponds to the "next" +task list.

+

Exception handlers may be used in both queries and transactions. For a complex transaction, the exception handler +may implement some data rollback logic or recovery mechanism.

+

Best practice

+

When a task-level exception handler throws exception, it will be caught by the top-level exception handler, if any.

+

A top-level exception handler should not throw exception. Otherwise it may go into an exception loop.

+

Therefore, we recommend that an exception handler should return regular result set in a PoJo or a Map object.

+

An example of task-level exception handler is shown in the "HelloException.class" in the unit test section of +the event script engine where it set the status code in the result set so that the system can map the status code +from the result set to the next task or to the HTTP output status code.

+

Advanced features

+

Simple type matching and conversion

+

Event script's state machine supports simple type matching and conversion. This "impedance matching" feature +allows us to accommodate minor interface contract changes without refactoring business logic of a user function.

+

This is supported in both the left-hand-side and right-hand-side of both input and output data mappings.

+

For the left-hand-side, the state machine's model value is matched or converted to the target data type before +setting the value of the right-hand-side. The state machine values are unchanged.

+

For the right-hand-side, the matched or converted value is applied to the state machine's model value.

+

The syntax is model.somekey:type where "type" is one of the following:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeMatch value asExample
texttext stringmodel.someKey:text
binarybyte arraymodel.someKey:binary
intinteger or -1 if not numericmodel.someKey:int
longlong or -1 if not numericmodel.someKey:long
floatfloat or -1 if not numericmodel.someKey:float
doubledouble or -1 if not numericmodel.someKey:double
booleantrue or falsemodel.someKey:boolean
boolean(value)true if value matchesmodel.someKey:boolean(positive)
boolean(value=true)true if value matchesmodel.someKey:boolean(positive=true)
boolean(value=false)false if value matchesmodel.someKey:boolean(negative=false)
and(model.key)boolean AND of 2 model keysmodel.someKey:and(model.another)
or(model.key)boolean OR of 2 model keysmodel.someKey:or(model.another)
substring(start, end)extract a substringmodel.someKey:substring(0, 5)
substring(start)extract a substringmodel.someKey:substring(5)
b64byte-array to Base64 textmodel.someKey:b64
b64Base64 text to byte-arraymodel.someKey:b64
+

For boolean with value matching, the value can be null. This allows your function to test if the +key-value in the left-hand-side is a null value.

+

For Base64 type matching, if the key-value is a text string, the system will assume it is a +Base64 text string and convert it to a byte-array. If the key-value is a byte-array, the system +will encode it into a Base64 text string.

+

An interesting use case of type matching is a simple decision task using the built-in no-op function. +For example, when a control file for the application is not available, your application will switch +to run in dev mode.

+

A sample task may look like this:

+
first.task: 'no.op'
+
+tasks:
+- input:
+    - 'file(binary:/tmp/interesting-config-file) -> model.is-local:boolean(null=true)'
+  process: 'no.op'
+  output:
+    - 'model.is-local -> decision'
+  execution: decision
+  next:
+    - 'start.in.dev.mode'
+    - 'start.in.cloud'
+
+

External state machine

+

The in-memory state machine is created for each query or transaction flow and it is temporal.

+

For complex transactions or long running work flows, you would typically want to externalize some transaction +states to a persistent store such as a distributed cache system or a high performance key-value data store.

+

In these use cases, you can implement an external state machine function and configure it in a flow.

+

Below is an example from a unit test. When you externalize a key-value to an external state machine, +you must configure the route name (aka level-3 functional topic) of the external state machine.

+

Note that when passing a null value to a key of an external state machine means "removal".

+
external.state.machine: 'v1.ext.state.machine'
+
+tasks:
+  - input:
+      # A function can call an external state machine using input or output mapping.
+      # In this example, it calls external state machine from input data mapping.
+      - 'input.path_parameter.user -> ext:/${app.id}/user'
+      - 'input.body -> model.body'
+      # demonstrate saving constant to state machine and remove it using model.none
+      - 'text(world) -> ext:hello'
+      - 'model.none -> ext:hello'
+    process: 'no.op'
+    output:
+      - 'text(application/json) -> output.header.content-type'
+      # It calls external state machine again from output data mapping
+      - 'input.body -> ext:/${app.id}/body'
+      - 'input.body -> output.body'
+      - 'text(message) -> ext:test'
+      - 'model.none -> ext:test'
+    description: 'Hello World'
+    execution: end
+
+

The "external.state.machine" parameter is optional.

+

When present, the system will send a key-value from the current flow instance's state machine +to the function implementing the external state machine. The system uses the "ext:" namespace +to externalize a state machine's key-value.

+

Note that the delivery of key-values to the external state machine is asynchronous. +Therefore, please assume eventual consistency.

+

You should implement a user function as the external state machine.

+

The input interface contract to the external state machine for saving a key-value is:

+
header.type = 'put'
+header.key = key
+body = value
+
+

Your function should save the input key-value to a persistent store.

+

In another flow that requires the key-value, you can add an initial task +to retrieve from the persistent store and do "output data mapping" to +save to the in-memory state machine so that your transaction flow can +use the persisted key-values to continue processing.

+

In the unit tests of the event-script-engine subproject, these two flows work together:

+
externalize-put-key-value
+externalize-get-key-value
+
+

IMPORTANT: Events to an external state machine are delivered asynchronously. If you want to guarantee +message sequencing, please do not set the "instances" parameter in the PreLoad annotation.

+

To illustrate a minimalist implementation, below is an example of an external state machine in the +event-script-engine's unit test section.

+
@PreLoad(route = "v1.ext.state.machine")
+public class ExternalStateMachine implements LambdaFunction {
+    private static final Logger log = LoggerFactory.getLogger(ExternalStateMachine.class);
+
+    private static final ManagedCache store = ManagedCache.createCache("state.machine", 5000);
+    private static final String TYPE = "type";
+    private static final String PUT = "put";
+    private static final String GET = "get";
+    private static final String REMOVE = "remove";
+    private static final String KEY = "key";
+
+    @Override
+    public Object handleEvent(Map<String, String> headers, Object input, int instance) {
+        if (!headers.containsKey(KEY)) {
+            throw new IllegalArgumentException("Missing key in headers");
+        }
+        String type = headers.get(TYPE);
+        String key = headers.get(KEY);
+        if (PUT.equals(type) && input != null) {
+            log.info("Saving {} to store", key);
+            store.put(key, input);
+            return true;
+        }
+        if (GET.equals(type)) {
+            Object v = store.get(key);
+            if (v != null) {
+                log.info("Retrieve {} from store", key);
+                return v;
+            } else {
+                return null;
+            }
+        }
+        if (REMOVE.equals(type)) {
+            if (store.exists(key)) {
+                store.remove(key);
+                log.info("Removed {} from store", key);
+                return true;
+            } else {
+                return false;
+            }
+        }
+        return false;
+    }
+}
+
+

Future task scheduling

+

You may add a “delay” tag in a task so that it will be executed later. +This feature is usually used for unit tests or "future task scheduling".

+

Since the system is event-driven and non-blocking, the delay is simulated by event scheduling. +It does not block the processing flow.

+ + + + + + + + + + + + + + + + + + + + +
TypeValueExample
Fixed delayMillisecondsdelay=1000
Variable delayState machine variabledelay=model.delay
+

When delay is set to a state variable that its value is not configured by a prior data mapping, +the delay command will be ignored.

+

An example task that has an artificial delay of 2 seconds:

+
tasks:
+  - input:
+      - 'input.path_parameter.user -> user'
+      - 'input.query.ex -> exception'
+      - 'text(hello world) -> greeting'
+    process: 'greeting.test'
+    output:
+      - 'text(application/json) -> output.header.content-type'
+      - 'result -> output.body'
+    description: 'Hello World'
+    execution: end
+    delay: 2000
+
+


+ + + + + + + + + + + + + + + +
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 00000000..42151222 --- /dev/null +++ b/docs/guides/CHAPTER-5/index.html @@ -0,0 +1,550 @@ + + + + + + + + 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-composable/examples/lambda-example
+mvn clean package
+java -jar target/lambda-example-4.0.16.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-2, 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::add);
+    EventEnvelope response = bench.poll(10, TimeUnit.SECONDS);
+    assert response != null;
+    assertEquals(HashMap.class, response.getBody().getClass());
+    MultiLevelMap map = new MultiLevelMap((Map<String, Object>) response.getBody());
+    assertEquals("b", map.getElement("headers.a"));
+    assertEquals(name, map.getElement("body.name"));
+    assertEquals(address, map.getElement("body.address"));
+    assertEquals(telephone, map.getElement("body.telephone"));
+    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::add);
+    EventEnvelope response = bench.poll(10, TimeUnit.SECONDS);
+    assert response != null;
+    assertEquals(SamplePoJo.class, response.getBody().getClass());
+    SamplePoJo pojo = response.getBody(SamplePoJo.class);
+    assertEquals(ID, pojo.getId());
+    assertEquals(NAME, pojo.getName());
+    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();
+    String traceId = Utility.getInstance().getUuid();
+    PostOffice po = new PostOffice("unit.test", traceId, "/stream/upload/test");
+    int len = 0;
+    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+    EventPublisher publisher = new EventPublisher(10000);
+    for (int i=0; i < 10; i++) {
+        String line = "hello world "+i+"\n";
+        byte[] d = util.getUTF(line);
+        publisher.publish(d);
+        bytes.write(d);
+        len += d.length;
+    }
+    publisher.publishCompletion();
+    // emulate a multipart 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(publisher.getStreamId());
+    // send the HTTP request event to the "hello.upload" function
+    EventEnvelope request = new EventEnvelope().setTo("hello.upload")
+            .setBody(req).setTrace("12345", "/api/upload/demo").setFrom("unit.test");
+    po.asyncRequest(request, 8000).onSuccess(bench::add);
+    EventEnvelope response = bench.poll(10, TimeUnit.SECONDS);
+    assert response != null;
+    assertEquals(HashMap.class, response.getBody().getClass());
+    Map<String, Object> map = (Map<String, Object>) response.getBody();
+    System.out.println(response.getBody());
+    assertEquals(len, map.get("expected_size"));
+    assertEquals(len, map.get("actual_size"));
+    assertEquals(FILENAME, map.get("filename"));
+    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);
+    assertTrue(file.exists());
+    assertEquals(len, file.length());
+    // compare file content
+    byte[] b = Utility.getInstance().file2bytes(file);
+    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:21-ubuntu
+EXPOSE 8083
+WORKDIR /app
+COPY target/rest-spring-3-example-3.1.2.jar .
+ENTRYPOINT ["java","-jar","rest-spring-3-example-3.1.2.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 Script SyntaxTable 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 00000000..d547d572 --- /dev/null +++ b/docs/guides/CHAPTER-6/index.html @@ -0,0 +1,393 @@ + + + + + + + + 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 3.2.1 and implement your Spring Boot main application
  2. +
  3. Add the 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-3 library in your application and turn it into a pre-configured +Spring Boot 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-3-example demo application

+

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

+

You can use the rest-spring-3-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. Spring RestControllers with Mono/Flux
  2. +
  3. Servlet 3.1 WebServlets
  4. +
+

We will examine asynchronous REST endpoint with the AsyncHelloWorld class.

+
@RestController
+public class AsyncHelloWorld {
+  private static final AtomicInteger seq = new AtomicInteger(0);
+
+  @GetMapping(value = "/api/hello/world", produces={"application/json", "application/xml"})
+  public Mono<Map<String, Object>> hello(HttpServletRequest request) {
+    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 a parameter showing the sequence counter.
+    // The echo service will return both.
+    int n = seq.incrementAndGet();
+    EventEnvelope req = new EventEnvelope();
+    req.setTo("hello.world").setBody(forward).setHeader("seq", n);
+    return Mono.create(callback -> {
+      try {
+        po.asyncRequest(req, 3000)
+                .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());
+                  callback.success(result);
+                })
+                .onFailure(ex -> callback.error(new AppException(408, ex.getMessage())));
+      } catch (IOException e) {
+        callback.error(e);
+      }
+    });
+  }
+}
+
+

In this hello world REST endpoint, Spring Reactor 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 00000000..8a08d2f0 --- /dev/null +++ b/docs/guides/CHAPTER-7/index.html @@ -0,0 +1,411 @@ + + + + + + + + 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.1.2.jar
+
+

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

+
cd examples/rest-spring-example-3
+java -jar target/rest-spring-3-example-3.1.2.jar
+
+

The rest-spring-3-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:

+
@RestController
+public class HelloPoJoEventOverHttp {
+
+    @GetMapping("/api/pojo/http/{id}")
+    public Mono<SamplePoJo> getPoJo(@PathVariable("id") Integer id) {
+        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);
+        return Mono.create(callback -> {
+            try {
+                EventEnvelope response = po.request(req, 3000, Collections.emptyMap(), remoteEndpoint, true).get();
+                if (response.getBody() instanceof SamplePoJo result) {
+                    callback.success(result);
+                } else {
+                    callback.error(new AppException(response.getStatus(), response.getError()));
+                }
+            } catch (IOException | ExecutionException | InterruptedException e) {
+                callback.error(e);
+            }
+        });
+    }
+}
+
+

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

+

Asynchronous API (Java)

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

Sequential non-blocking API (virtual thread function)

+
// java.util.concurrent.Future
+public Future<EventEnvelope> request(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: 'hello.pojo2'
+    target: 'http://127.0.0.1:${lambda.example.port}/api/event'
+  - 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 three routes (hello.pojo2, 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.

+

An example in the rest-spring-3-example subproject is shown below to illustrate this service abstraction. +In this example, the remote Event-over-HTTP endpoint address is resolved from the event-over-http.yaml +configuration.

+
@RestController
+public class HelloPoJoEventOverHttpByConfig {
+
+    @GetMapping("/api/pojo2/http/{id}")
+    public Mono<SamplePoJo> getPoJo(@PathVariable("id") Integer id) {
+        String traceId = Utility.getInstance().getUuid();
+        PostOffice po = new PostOffice("hello.pojo.endpoint", traceId, "GET /api/pojo2/http");
+        /*
+         * "hello.pojo2" resides in the lambda-example and is reachable by "Event-over-HTTP".
+         * In HelloPojoEventOverHttp.java, it demonstrates the use of Event-over-HTTP API.
+         * In this example, it illustrates the use of the "Event-over-HTTP by configuration" feature.
+         * Please see application.properties and event-over-http.yaml files for more details.
+         */
+        EventEnvelope req = new EventEnvelope().setTo("hello.pojo2").setHeader("id", id);
+        return Mono.create(callback -> {
+            try {
+                EventEnvelope response = po.request(req, 3000, false).get();
+                if (response.getBody() instanceof SamplePoJo result) {
+                    callback.success(result);
+                } else {
+                    callback.error(new AppException(response.getStatus(), response.getError()));
+                }
+            } catch (IOException | ExecutionException | InterruptedException e) {
+                callback.error(e);
+            }
+        });
+    }
+}
+
+
+

Note: The configuration based "event-over-HTTP" feature does not support fork-n-join request API. + You can achieve similar parallel processing using multiple calls to "po.request API" + where each call returns a Java "Future".

+
+

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 ContentsMinimalist Service Mesh
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/guides/CHAPTER-8/index.html b/docs/guides/CHAPTER-8/index.html new file mode 100644 index 00000000..7f35bdd2 --- /dev/null +++ b/docs/guides/CHAPTER-8/index.html @@ -0,0 +1,483 @@ + + + + + + + + Chapter-8 - Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Minimalist 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 minimalist 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.

+

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 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" or "kafka". +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.1.2.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 a temporary directory "/tmp/kafka-logs".

+
+

The kafka server is designed for development purpose only. The kafka message log store + 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.1.2.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.1.2.jar
+
+
cd examples/lambda-example
+java -Dcloud.connector=kafka -Dmandatory.health.dependencies=cloud.connector.health 
+     -jar target/lambda-example-3.1.2.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:

+
{
+  "dependency": [
+    {
+      "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

+
{
+  "dependency": [
+    {
+      "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 00000000..c469dff7 --- /dev/null +++ b/docs/guides/CHAPTER-9/index.html @@ -0,0 +1,786 @@ + + + + + + + + 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 must 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 AutoStart class 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. Duplicated sequence numbers are allowed. Normal startup sequence must be +between 1 and 999.

+

Note: It is the "start" method of each EntryPoint implementation that follows the execution sequence of the +MainApplication annotation. The optional "main" method is used only to kick off the application bootstrap and +it must include only the following statement:

+
public static void main(String[] args) {
+    AutoStart.main(args);
+}
+
+

Therefore, even when the default sequence of the MainApplication annotation is 10 and you invoke the "main" +method from an IDE, the "start" method of each MainApplication modules will execute orderly.

+

Setup before the Main Application

+

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(s). 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.

+

Normal startup sequence must be between 6 and 999. Sequence 5 is reserved by the AsyncHttpClientLoader. +If your startup code does not need the async HTTP client service and you want it to run first, you may use +sequence from 1 to 4.

+

Event envelope

+

Mercury 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.

+

PoJo transport

+

Your function can implement the TypedLambdaFunction interface if you want to use PoJo as input and output.

+

If you use the EventEnvelope as input, PoJo payload is provided as a HashMap in the event's body.

+

The original class name of the PoJo payload is saved in the event's type attribute. +You can compare and restore the PoJo like this:

+
if (SamplePoJo.class.getName().equals(input.getType())) {
+    SamplePoJo pojo = input.getBody(SamplePoJo.class);
+    // do something with your input PoJo
+}
+
+

If you use the "untyped" LambdaFunction, the input "Object" is a HashMap and you would need to convert it back +to a PoJo using the SimpleMapper or a serializer of your choice.

+

For example,

+
SamplePoJo pojo = SimpleMapper.getInstance().getMapper().readValue((Map<String, Object>) input, SamplePoJo.class);
+
+

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;
+    }
+}
+
+

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 most convenient method to make a sequential non-blocking RPC call is to use the PostOffice's request API.

+
// for a single RPC call
+PostOffice po = new PostOffice(headers, instance);
+EventEnvelope result = po.request(requestEvent, timeoutInMills).get();
+
+// for a fork-n-join call
+PostOffice po = new PostOffice(headers, instance);
+List<EventEnvelope> result = po.request(requestEvents, timeoutInMills).get();
+
+

If you prefer the Kotlin programming language, you may use the FastRPC API.

+

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}
+another.key=${my.key:12345}
+complex.key=first ${FIRST_ENV_VAR}, second ${SECOND_ENV_VAR}
+
+

In the above example, a parameter may contain references to more than one environment variable.

+

Default value, if not given, will be assumed to be an empty string.

+

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

+

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.

+

Event Scripting

+

To further reduce coding effort, you can perform "event choreography" by configuration using "Event Script". +Please refer to Event Script syntax in Chapter 4

+

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.

+

The core-engine 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

+

We recommend using the composable-example project as a template to start writing your Composable applications. +You can follow the Composable methodology where you draw event flow diagrams to represent various use cases, +convert them into event scripts that carry out event chorerography for your self-contained functions.

+

For more information, please refer to Event Script syntax in Chapter 4.

+

If you prefer to do low-level event-driven programming, you can use the lambda-example project as a template. +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.

+

Technical support

+

For enterprise clients, optional technical support is available. Please contact your Accenture representative +for details. +

+ + + + + + + + + + + + + +
Chapter-8Home
Minimalist Service MeshTable of Contents
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « 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 00000000..071e8873 --- /dev/null +++ b/docs/guides/TABLE-OF-CONTENTS/index.html @@ -0,0 +1,204 @@ + + + + + + + + Contents - Mercury + + + + + + + + + + + + + +
+ + +
+ + + +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/docs/img/favicon.ico b/docs/img/favicon.ico new file mode 100644 index 00000000..e85006a3 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 00000000..59a7229c --- /dev/null +++ b/docs/index.html @@ -0,0 +1,257 @@ + + + + + + + + Mercury + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Mercury Composable

+

Since version 4.0, we have merged our enterprise extension ("Event Script") with the Mercury v3.1 foundation +codebase. It is a comprehensive toolkit to write composable applications including microservices and +serverless. This technology was filed under US Patent application 18/459,307. The source code is provided +as is under the Apache 2.0 license.

+

August 2024

+

Getting Started

+

A composable application is designed in 3 steps:

+
    +
  1. Describe your use case as an event flow diagram
  2. +
  3. Create a configuration file to represent the event flow
  4. +
  5. Write a user story for each user function
  6. +
+

To get started, please visit Chapter 1, Developer Guide.

+

We will illustrate the methodology with a composable application example.

+

Conquer Complexity: Embrace Composable Design

+

Introduction

+

Software development is an ongoing battle against complexity. Over time, codebases can become tangled and unwieldy, +hindering innovation and maintenance. This article introduces composable design patterns, a powerful approach to +build applications that are modular, maintainable, and scalable.

+

The Perils of Spaghetti Code

+

We have all encountered it: code that resembles a plate of spaghetti – tangled dependencies, hidden logic, +and a general sense of dread when approaching modifications. These codebases are difficult to test, debug, +and update. Composable design patterns offer a solution.

+

Evolution of Design Patterns

+

Software development methodologies have evolved alongside hardware advancements. In the early days, developers +prized efficiency, writing code from scratch due to limited libraries. The rise of frameworks brought structure +and boilerplate code, but also introduced potential rigidity.

+

Functional Programming and Event-Driven Architecture

+

Functional programming, with its emphasis on pure functions and immutable data, paved the way for composable design. +This approach encourages building applications as chains of well-defined functions, each with a clear input and output.

+

Event-driven architecture complements this approach by using events to trigger functions. This loose coupling +promotes modularity and scalability.

+

The Power of Composable Design

+

At its core, composable design emphasizes two principles:

+
    +
  1. Self-Contained Functions: Each function is a well-defined unit, handling its own logic and transformations + with minimal dependencies.
  2. +
  3. Event Choreography: Functions communicate through events, allowing for loose coupling and independent + execution.
  4. +
+

Benefits of Composable Design

+
    +
  • Enhanced Maintainability: Isolated functions are easier to understand, test, and modify.
  • +
  • Improved Reusability: Self-contained functions can be easily reused across different parts of your application.
  • +
  • Superior Performance: Loose coupling reduces bottlenecks and encourages asynchronous execution.
  • +
  • Streamlined Testing: Well-defined functions facilitate unit testing and isolate potential issues.
  • +
  • Simplified Debugging: Independent functions make it easier to pinpoint the source of errors.
  • +
  • Technology Agnostic: You may use your preferred frameworks and tools to write composable code, + allowing for easier future adaptations.
  • +
+

Implementing Composable Design

+

While seemingly simple, implementing composable design can involve some initial complexity.

+

Here's a breakdown of the approach:

+
    +
  • Function Design: Each function serves a specific purpose, with clearly defined inputs and outputs.
  • +
  • Event Communication: Functions communicate through well-defined events, avoiding direct dependencies.
  • +
  • Choreography: An event manager, with a state machine and event flow configuration, sequences and triggers functions + based on events.
  • +
+

Conclusion

+

Composable design patterns offer a powerful paradigm for building maintainable, scalable, and future-proof applications. +By embracing the principles of self-contained functions and event-driven communication, you can conquer complexity and +write code that is a joy to work with.

+

Are you ready to take your development practices to the next level? Embrace composable design now!

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + + Next » + + +
+ + + + + + + + + + + diff --git a/docs/js/html5shiv.min.js b/docs/js/html5shiv.min.js new file mode 100644 index 00000000..1a01c94b --- /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 00000000..c4c6022f --- /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 00000000..aca0a167 --- /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 00000000..a5e469d7 --- /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 00000000..3e608029 --- /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":"Mercury Composable Since version 4.0, we have merged our enterprise extension (\"Event Script\") with the Mercury v3.1 foundation codebase. It is a comprehensive toolkit to write composable applications including microservices and serverless. This technology was filed under US Patent application 18/459,307. The source code is provided as is under the Apache 2.0 license. August 2024 Getting Started A composable application is designed in 3 steps: Describe your use case as an event flow diagram Create a configuration file to represent the event flow Write a user story for each user function To get started, please visit Chapter 1, Developer Guide . We will illustrate the methodology with a composable application example. Conquer Complexity: Embrace Composable Design Introduction Software development is an ongoing battle against complexity. Over time, codebases can become tangled and unwieldy, hindering innovation and maintenance. This article introduces composable design patterns, a powerful approach to build applications that are modular, maintainable, and scalable. The Perils of Spaghetti Code We have all encountered it: code that resembles a plate of spaghetti \u2013 tangled dependencies, hidden logic, and a general sense of dread when approaching modifications. These codebases are difficult to test, debug, and update. Composable design patterns offer a solution. Evolution of Design Patterns Software development methodologies have evolved alongside hardware advancements. In the early days, developers prized efficiency, writing code from scratch due to limited libraries. The rise of frameworks brought structure and boilerplate code, but also introduced potential rigidity. Functional Programming and Event-Driven Architecture Functional programming, with its emphasis on pure functions and immutable data, paved the way for composable design. This approach encourages building applications as chains of well-defined functions, each with a clear input and output. Event-driven architecture complements this approach by using events to trigger functions. This loose coupling promotes modularity and scalability. The Power of Composable Design At its core, composable design emphasizes two principles: Self-Contained Functions : Each function is a well-defined unit, handling its own logic and transformations with minimal dependencies. Event Choreography : Functions communicate through events, allowing for loose coupling and independent execution. Benefits of Composable Design Enhanced Maintainability : Isolated functions are easier to understand, test, and modify. Improved Reusability : Self-contained functions can be easily reused across different parts of your application. Superior Performance : Loose coupling reduces bottlenecks and encourages asynchronous execution. Streamlined Testing : Well-defined functions facilitate unit testing and isolate potential issues. Simplified Debugging : Independent functions make it easier to pinpoint the source of errors. Technology Agnostic : You may use your preferred frameworks and tools to write composable code, allowing for easier future adaptations. Implementing Composable Design While seemingly simple, implementing composable design can involve some initial complexity. Here's a breakdown of the approach: Function Design : Each function serves a specific purpose, with clearly defined inputs and outputs. Event Communication : Functions communicate through well-defined events, avoiding direct dependencies. Choreography : An event manager, with a state machine and event flow configuration, sequences and triggers functions based on events. Conclusion Composable design patterns offer a powerful paradigm for building maintainable, scalable, and future-proof applications. By embracing the principles of self-contained functions and event-driven communication, you can conquer complexity and write code that is a joy to work with. Are you ready to take your development practices to the next level? Embrace composable design now!","title":"Home"},{"location":"#mercury-composable","text":"Since version 4.0, we have merged our enterprise extension (\"Event Script\") with the Mercury v3.1 foundation codebase. It is a comprehensive toolkit to write composable applications including microservices and serverless. This technology was filed under US Patent application 18/459,307. The source code is provided as is under the Apache 2.0 license. August 2024","title":"Mercury Composable"},{"location":"#getting-started","text":"A composable application is designed in 3 steps: Describe your use case as an event flow diagram Create a configuration file to represent the event flow Write a user story for each user function To get started, please visit Chapter 1, Developer Guide . We will illustrate the methodology with a composable application example.","title":"Getting Started"},{"location":"#conquer-complexity-embrace-composable-design","text":"","title":"Conquer Complexity: Embrace Composable Design"},{"location":"#introduction","text":"Software development is an ongoing battle against complexity. Over time, codebases can become tangled and unwieldy, hindering innovation and maintenance. This article introduces composable design patterns, a powerful approach to build applications that are modular, maintainable, and scalable.","title":"Introduction"},{"location":"#the-perils-of-spaghetti-code","text":"We have all encountered it: code that resembles a plate of spaghetti \u2013 tangled dependencies, hidden logic, and a general sense of dread when approaching modifications. These codebases are difficult to test, debug, and update. Composable design patterns offer a solution.","title":"The Perils of Spaghetti Code"},{"location":"#evolution-of-design-patterns","text":"Software development methodologies have evolved alongside hardware advancements. In the early days, developers prized efficiency, writing code from scratch due to limited libraries. The rise of frameworks brought structure and boilerplate code, but also introduced potential rigidity.","title":"Evolution of Design Patterns"},{"location":"#functional-programming-and-event-driven-architecture","text":"Functional programming, with its emphasis on pure functions and immutable data, paved the way for composable design. This approach encourages building applications as chains of well-defined functions, each with a clear input and output. Event-driven architecture complements this approach by using events to trigger functions. This loose coupling promotes modularity and scalability.","title":"Functional Programming and Event-Driven Architecture"},{"location":"#the-power-of-composable-design","text":"At its core, composable design emphasizes two principles: Self-Contained Functions : Each function is a well-defined unit, handling its own logic and transformations with minimal dependencies. Event Choreography : Functions communicate through events, allowing for loose coupling and independent execution.","title":"The Power of Composable Design"},{"location":"#benefits-of-composable-design","text":"Enhanced Maintainability : Isolated functions are easier to understand, test, and modify. Improved Reusability : Self-contained functions can be easily reused across different parts of your application. Superior Performance : Loose coupling reduces bottlenecks and encourages asynchronous execution. Streamlined Testing : Well-defined functions facilitate unit testing and isolate potential issues. Simplified Debugging : Independent functions make it easier to pinpoint the source of errors. Technology Agnostic : You may use your preferred frameworks and tools to write composable code, allowing for easier future adaptations.","title":"Benefits of Composable Design"},{"location":"#implementing-composable-design","text":"While seemingly simple, implementing composable design can involve some initial complexity. Here's a breakdown of the approach: Function Design : Each function serves a specific purpose, with clearly defined inputs and outputs. Event Communication : Functions communicate through well-defined events, avoiding direct dependencies. Choreography : An event manager, with a state machine and event flow configuration, sequences and triggers functions based on events.","title":"Implementing Composable Design"},{"location":"#conclusion","text":"Composable design patterns offer a powerful paradigm for building maintainable, scalable, and future-proof applications. By embracing the principles of self-contained functions and event-driven communication, you can conquer complexity and write code that is a joy to work with. Are you ready to take your development practices to the next level? Embrace composable design now!","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 4.1.1, 12/18/2024 Added Added \"map\" constant type in input data mapping. Removed N/A Changed Updated Chapter-4 for the new \"map\" constant feature. Version 4.1.0, 12/11/2024 This milestone version achieves ideal event choreography by removing additional event routing to and from the Event Manager. This would boost internal event routing performance by 50 percent. Added Performance optimization for Event Script Removed N/A Changed The platform-core module uses virtual threads to execute event.script.manager and task.executor directly to eliminate additional serialization overheads since the two functions are event routers themselves. Version 4.0.33, 12/11/2024 Added Support of custom content types in application.yml Removed N/A Changed Improved websocket housekeeping logic Use bench.add to replace bench.offer API Version 4.0.32, 12/9/2024 Added For completeness, added Boolean AND and OR operations for simple type mapping. Added traceId as metadata for a flow instance Removed N/A Changed Update Chapter-4 for the new AND/OR type mapping feature Consistent custom HTTP headers for event over http protocol and streaming content Version 4.0.31, 12/5/2024 Added N/A Removed N/A Changed The \"keep.original\" key is renamed as \"keep-original\" to comply with convention. Continue processing if some preload override config files are missing. Version 4.0.30, 12/5/2024 Added Implemented unique task naming feature for event flow configuration. Removed N/A Changed The \"keep_original\" key is renamed as \"keep.original\" in preload override Chapter-4 of developer guide updated with the new task alias feature Version 4.0.29, 12/3/2024 Added Added integer, long, float, double and boolean type matching for state machine. Removed N/A Changed N/A Version 4.0.28, 11/29/2024 Added Support for simple data type matching processing (text, substring, binary and b64) Optional external state machine Removed Removed \"http.input.\" and \"http.output.\" aliases from event script. Instead, use the generic \"input.\" and \"output.\" namespaces. Changed Bugfix for AsyncHttpClient to allow missing HTTP request body in POST, PUT or PATCH request Mono reactive flow control Version 4.0.27, 11/27/2024 Added Support for Mono/Flux return type for KotlinLambdaFunction Implemented Websocket handshake handler to adjust to API changes in vertx 4.5.11 Removed N/A Changed N/A Version 4.0.26, 11/26/2024 Added N/A Removed Remove pom.xml version override for netty and spring framework because Spring Boot 3.4.0 fetches the correct versions of netty and spring framework. Earlier override was done to avoid security vulnerabilities of older versions of netty and spring framework. Changed Handle the case that Mono will not return payload if the payload is null OSS update: Classgraph 4.8.179, Vertx 4.5.11, Spring Boot 3.4.0, Kafka Client 3.9.0 Version 4.0.25, 11/21/2024 Added Support more than one REST configuration files. When a duplicated REST entry is detected, the system will abort REST endpoint rendering and print out an error message in application log. If you have unit tests to cover the REST endpoints, the unit tests will fail accordingly. Removed N/A Changed Improved environment variable parsing in config reader. System will skip entries with invalid environment variable reference syntax. Version 4.0.24, 11/20/2024 Added N/A Removed N/A Changed Bugfix for an edge case in config reader to handle control character of brackets inside an environment variable reference. e.g. some.key=${ENV_VAR:something/{test1}/{test2}} Version 4.0.23, 11/19/2024 Added N/A Removed ObjectStreamWriter and AsyncObjectStreamReader are removed Changed Replace ObjectStreamWriter with FluxPublisher Replace AsyncObjectStreamReader with FluxConsumer Bugfix for FluxConsumer expiry - change type from \"data\" to \"exception\". Version 4.0.22, 11/18/2024 Added FluxPublisher and FluxConsumer for integration with Flux reactive response object Removed N/A Changed Unit tests in event streaming and post office to support Flux integration Select reactor-core version 3.7.0 using dependency management (reactor-bom version 2024.0.0) Version 4.0.21, 11/14/2024 Added Support for user function to return a Mono reactive response object Removed N/A Changed Update netty to version 4.1.115.Final to address security vulnerability in 4.1.114 Move reactor-core library from rest-spring-3 to platform-core Version 4.0.20, 11/13/2024 Added For ease of configuration, added \"com.accenture\" to the base packages so that user applications do not need to include it to use the event-script-engine module. Removed if-then-else pipeline feature in event-script Changed Update Event Script syntax for consistency Fix error in counting number of compiled flows Version 4.0.16, 11/10/2024 Added Generate unique flow instance ID as reference during flow execution. Removed N/A Changed Save the original correlation-ID from the calling party in a flow instance and return this value to the calling party at the end of flow execution. Version 4.0.15, 11/7/2024 Added N/A Removed N/A Changed renamed StartFlow to FlowExecutor Version 4.0.14, 11/7/2024 Added N/A Removed N/A Changed Health check function can return either a text string or a Map StartFlow API updates Version 4.0.13, 11/5/2024 Added Added helper class \"StartFlow\" to start a flow, including internal flows without HTTP or Kafka. Removed N/A Changed Bugfix for empty YAML file to avoid null pointer exception Sort event scripts for orderly logging in the CompileFlows validation process Version 4.0.12, 10/31/2024 Added New feature to support resolution of more than one environment variable for a parameter using the ConfigReader Removed N/A Changed Update OSS modules 1. classgraph version 4.8.177 2. kotlin version 2.0.21 3. guava version 33.3.1-jre 4. jUnit version 5 jupiter Adjusted all unit tests to use jUnit 5 Version 4.0.11, 10/28/2024 Added New features to support: 1. multiple preload override config file 2. multiple flow list config files Removed unused class \"UnauthorizedObj\" in platform-core commons-io dependency in Kafka-Standalone subproject Changed Unit test for the preload override feature JavaDoc for the MainApplication Version 4.0.10, 10/24/2024 Added N/A Removed N/A Changed OSS update - Spring Boot 3.3.5 Security patch for CR/LF exploit for HTTP cookie Version 4.0.9, 10/18/2024 Added Added Kafka Raft for the Kafka-standalone app. Removed Removed zookeeper from Kafka-standalone app. Changed Update spring framework verison 6.1.14 to avoid vulnerability in webflux Version 4.0.8, 10/9/2024 Added Partial support of Active Profile using the \"spring.profiles.active\" parameter Hierarchy of flows Removed N/A Changed N/A Version 4.0.7, 10/1/2024 Added A generic \"no-op\" function for use in event scripts. Removed Feature to ping a function without payload and headers. Changed Simplified api-playground application Version 4.0.6, 9/27/2024 Added HTTP request Cookie value filtering using RFC-6265 strict syntax Removed Automatic index page redirection filter for Spring Boot Changed Upgrade SHA-1 to SHA-512 algorithm in CryptoAPI utility Fix security vulnerability associated with HTTP request header and cookie manipulation Version 4.0.5, 9/24/2024 Added N/A Removed Feature for automatic PoJo transport in EventEnvelope and MsgPack Feature for safe.data.model deserialization Benchmark-server is no longer required Changed Update OSS versions - vertx 4.5.10, kotlin 2.0.20, spring boot 3.3.4 Version 4.0.4, 9/5/2024 Added New feature for AsyncHttpClient to render small streaming HTTP response (i.e. chunked binary data) as byte array. For details, Please refer to Appendix III, Developer Guide Removed N/A Changed Bugfix for parsing default value of environment variable in ConfigReader. This resolves an issue when the special character colon (\":\") is used more than once in the default value. Version 4.0.3, 9/4/2024 Added The \"preload override\" feature is added. This allows overriding a reusable composable library with a set of new route names that are unique for use in an event flow configuration script. For details, Please refer to Chapter 4, Developer Guide Removed N/A Changed N/A Version 4.0.2, 8/31/2024 Added New \"classpath\" namespace for input data mapping Support for input data mapping to handle subset of input request body as a Map or PoJo Removed N/A Changed Remove the class \"type\" variable from AsyncHttpRequest Improve the \"removeElement\" method in MultiLevelMap Make HTTP input request header labels key-insensitive Update Spring Boot to version 3.3.3 Version 4.0.1, 8/19/2024 Added new File read/write feature in Event Script's I/O data mapping Removed N/A Changed Update Spring Boot to version 3.3.2 Update Guava to version 33.3.0-jre Update Vertx to version 4.5.9 Update Kotlin to version 2.0.10 Change \"upstream\" to \"dependency\" in the \"/health\" endpoint Version 4.0.0, 6/24/2024 This version merges Event Script into the Mercury Composable repository. Added N/A Removed N/A Changed Update Spring Boot to version 3.3.1 Update Guava to version 33.2.1-jre Update Vertx to version 4.5.8 Update Kotlin to version 2.0.0 Update classgraph to version 4.8.174 Optional reply event for a flow configuration Kafka-standalone is still using Spring Boot 3.2.5 due to compatibility issue Version 3.1.5, 5/1/2024 This version supercedes 3.1.4 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.1.4, 4/28/2024 Added Added optional static content HTTP-GET request filter in rest.yaml Removed N/A Changed Updated syntax for static-content-filter Version 3.1.3, 4/24/2024 Added N/A Removed N/A Changed Enhanced OptionalService annotation. Version 3.1.2, 4/17/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.5, Vertx 4.5.7) Improve AppConfigReader and ConfigReader to use the app-config-reader.yml file. Enhanced OptionalService annotation. Version 3.1.1, 2/8/2024 Added AutoStart to run application as Spring Boot if the rest-spring-3 library is packaged in app Configurable \"Event over HTTP\" - automatic forward events over HTTP using a configuration Support user defined serializer with PreLoad annotation and platform API 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\" Version 3.1.0, 1/5/2024 Added Full integration with Java 21 Virtual Thread Default execution mode is set to \"virtual thread\" KernelThreadRunner annotation added to provide optional support of kernel threads Removed Retired Spring Boot version 2 Hazelcast and ActiveMQ network connectors Changed platform-core engine updated with virtual thread 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-411-12182024","text":"","title":"Version 4.1.1, 12/18/2024"},{"location":"CHANGELOG/#added","text":"Added \"map\" constant type in input data mapping.","title":"Added"},{"location":"CHANGELOG/#removed","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed","text":"Updated Chapter-4 for the new \"map\" constant feature.","title":"Changed"},{"location":"CHANGELOG/#version-410-12112024","text":"This milestone version achieves ideal event choreography by removing additional event routing to and from the Event Manager. This would boost internal event routing performance by 50 percent.","title":"Version 4.1.0, 12/11/2024"},{"location":"CHANGELOG/#added_1","text":"Performance optimization for Event Script","title":"Added"},{"location":"CHANGELOG/#removed_1","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_1","text":"The platform-core module uses virtual threads to execute event.script.manager and task.executor directly to eliminate additional serialization overheads since the two functions are event routers themselves.","title":"Changed"},{"location":"CHANGELOG/#version-4033-12112024","text":"","title":"Version 4.0.33, 12/11/2024"},{"location":"CHANGELOG/#added_2","text":"Support of custom content types in application.yml","title":"Added"},{"location":"CHANGELOG/#removed_2","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_2","text":"Improved websocket housekeeping logic Use bench.add to replace bench.offer API","title":"Changed"},{"location":"CHANGELOG/#version-4032-1292024","text":"","title":"Version 4.0.32, 12/9/2024"},{"location":"CHANGELOG/#added_3","text":"For completeness, added Boolean AND and OR operations for simple type mapping. Added traceId as metadata for a flow instance","title":"Added"},{"location":"CHANGELOG/#removed_3","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_3","text":"Update Chapter-4 for the new AND/OR type mapping feature Consistent custom HTTP headers for event over http protocol and streaming content","title":"Changed"},{"location":"CHANGELOG/#version-4031-1252024","text":"","title":"Version 4.0.31, 12/5/2024"},{"location":"CHANGELOG/#added_4","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_4","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_4","text":"The \"keep.original\" key is renamed as \"keep-original\" to comply with convention. Continue processing if some preload override config files are missing.","title":"Changed"},{"location":"CHANGELOG/#version-4030-1252024","text":"","title":"Version 4.0.30, 12/5/2024"},{"location":"CHANGELOG/#added_5","text":"Implemented unique task naming feature for event flow configuration.","title":"Added"},{"location":"CHANGELOG/#removed_5","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_5","text":"The \"keep_original\" key is renamed as \"keep.original\" in preload override Chapter-4 of developer guide updated with the new task alias feature","title":"Changed"},{"location":"CHANGELOG/#version-4029-1232024","text":"","title":"Version 4.0.29, 12/3/2024"},{"location":"CHANGELOG/#added_6","text":"Added integer, long, float, double and boolean type matching for state machine.","title":"Added"},{"location":"CHANGELOG/#removed_6","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_6","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-4028-11292024","text":"","title":"Version 4.0.28, 11/29/2024"},{"location":"CHANGELOG/#added_7","text":"Support for simple data type matching processing (text, substring, binary and b64) Optional external state machine","title":"Added"},{"location":"CHANGELOG/#removed_7","text":"Removed \"http.input.\" and \"http.output.\" aliases from event script. Instead, use the generic \"input.\" and \"output.\" namespaces.","title":"Removed"},{"location":"CHANGELOG/#changed_7","text":"Bugfix for AsyncHttpClient to allow missing HTTP request body in POST, PUT or PATCH request Mono reactive flow control","title":"Changed"},{"location":"CHANGELOG/#version-4027-11272024","text":"","title":"Version 4.0.27, 11/27/2024"},{"location":"CHANGELOG/#added_8","text":"Support for Mono/Flux return type for KotlinLambdaFunction Implemented Websocket handshake handler to adjust to API changes in vertx 4.5.11","title":"Added"},{"location":"CHANGELOG/#removed_8","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_8","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-4026-11262024","text":"","title":"Version 4.0.26, 11/26/2024"},{"location":"CHANGELOG/#added_9","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_9","text":"Remove pom.xml version override for netty and spring framework because Spring Boot 3.4.0 fetches the correct versions of netty and spring framework. Earlier override was done to avoid security vulnerabilities of older versions of netty and spring framework.","title":"Removed"},{"location":"CHANGELOG/#changed_9","text":"Handle the case that Mono will not return payload if the payload is null OSS update: Classgraph 4.8.179, Vertx 4.5.11, Spring Boot 3.4.0, Kafka Client 3.9.0","title":"Changed"},{"location":"CHANGELOG/#version-4025-11212024","text":"","title":"Version 4.0.25, 11/21/2024"},{"location":"CHANGELOG/#added_10","text":"Support more than one REST configuration files. When a duplicated REST entry is detected, the system will abort REST endpoint rendering and print out an error message in application log. If you have unit tests to cover the REST endpoints, the unit tests will fail accordingly.","title":"Added"},{"location":"CHANGELOG/#removed_10","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_10","text":"Improved environment variable parsing in config reader. System will skip entries with invalid environment variable reference syntax.","title":"Changed"},{"location":"CHANGELOG/#version-4024-11202024","text":"","title":"Version 4.0.24, 11/20/2024"},{"location":"CHANGELOG/#added_11","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_11","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_11","text":"Bugfix for an edge case in config reader to handle control character of brackets inside an environment variable reference. e.g. some.key=${ENV_VAR:something/{test1}/{test2}}","title":"Changed"},{"location":"CHANGELOG/#version-4023-11192024","text":"","title":"Version 4.0.23, 11/19/2024"},{"location":"CHANGELOG/#added_12","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_12","text":"ObjectStreamWriter and AsyncObjectStreamReader are removed","title":"Removed"},{"location":"CHANGELOG/#changed_12","text":"Replace ObjectStreamWriter with FluxPublisher Replace AsyncObjectStreamReader with FluxConsumer Bugfix for FluxConsumer expiry - change type from \"data\" to \"exception\".","title":"Changed"},{"location":"CHANGELOG/#version-4022-11182024","text":"","title":"Version 4.0.22, 11/18/2024"},{"location":"CHANGELOG/#added_13","text":"FluxPublisher and FluxConsumer for integration with Flux reactive response object","title":"Added"},{"location":"CHANGELOG/#removed_13","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_13","text":"Unit tests in event streaming and post office to support Flux integration Select reactor-core version 3.7.0 using dependency management (reactor-bom version 2024.0.0)","title":"Changed"},{"location":"CHANGELOG/#version-4021-11142024","text":"","title":"Version 4.0.21, 11/14/2024"},{"location":"CHANGELOG/#added_14","text":"Support for user function to return a Mono reactive response object","title":"Added"},{"location":"CHANGELOG/#removed_14","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_14","text":"Update netty to version 4.1.115.Final to address security vulnerability in 4.1.114 Move reactor-core library from rest-spring-3 to platform-core","title":"Changed"},{"location":"CHANGELOG/#version-4020-11132024","text":"","title":"Version 4.0.20, 11/13/2024"},{"location":"CHANGELOG/#added_15","text":"For ease of configuration, added \"com.accenture\" to the base packages so that user applications do not need to include it to use the event-script-engine module.","title":"Added"},{"location":"CHANGELOG/#removed_15","text":"if-then-else pipeline feature in event-script","title":"Removed"},{"location":"CHANGELOG/#changed_15","text":"Update Event Script syntax for consistency Fix error in counting number of compiled flows","title":"Changed"},{"location":"CHANGELOG/#version-4016-11102024","text":"","title":"Version 4.0.16, 11/10/2024"},{"location":"CHANGELOG/#added_16","text":"Generate unique flow instance ID as reference during flow execution.","title":"Added"},{"location":"CHANGELOG/#removed_16","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_16","text":"Save the original correlation-ID from the calling party in a flow instance and return this value to the calling party at the end of flow execution.","title":"Changed"},{"location":"CHANGELOG/#version-4015-1172024","text":"","title":"Version 4.0.15, 11/7/2024"},{"location":"CHANGELOG/#added_17","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_17","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_17","text":"renamed StartFlow to FlowExecutor","title":"Changed"},{"location":"CHANGELOG/#version-4014-1172024","text":"","title":"Version 4.0.14, 11/7/2024"},{"location":"CHANGELOG/#added_18","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_18","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_18","text":"Health check function can return either a text string or a Map StartFlow API updates","title":"Changed"},{"location":"CHANGELOG/#version-4013-1152024","text":"","title":"Version 4.0.13, 11/5/2024"},{"location":"CHANGELOG/#added_19","text":"Added helper class \"StartFlow\" to start a flow, including internal flows without HTTP or Kafka.","title":"Added"},{"location":"CHANGELOG/#removed_19","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_19","text":"Bugfix for empty YAML file to avoid null pointer exception Sort event scripts for orderly logging in the CompileFlows validation process","title":"Changed"},{"location":"CHANGELOG/#version-4012-10312024","text":"","title":"Version 4.0.12, 10/31/2024"},{"location":"CHANGELOG/#added_20","text":"New feature to support resolution of more than one environment variable for a parameter using the ConfigReader","title":"Added"},{"location":"CHANGELOG/#removed_20","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_20","text":"Update OSS modules 1. classgraph version 4.8.177 2. kotlin version 2.0.21 3. guava version 33.3.1-jre 4. jUnit version 5 jupiter Adjusted all unit tests to use jUnit 5","title":"Changed"},{"location":"CHANGELOG/#version-4011-10282024","text":"","title":"Version 4.0.11, 10/28/2024"},{"location":"CHANGELOG/#added_21","text":"New features to support: 1. multiple preload override config file 2. multiple flow list config files","title":"Added"},{"location":"CHANGELOG/#removed_21","text":"unused class \"UnauthorizedObj\" in platform-core commons-io dependency in Kafka-Standalone subproject","title":"Removed"},{"location":"CHANGELOG/#changed_21","text":"Unit test for the preload override feature JavaDoc for the MainApplication","title":"Changed"},{"location":"CHANGELOG/#version-4010-10242024","text":"","title":"Version 4.0.10, 10/24/2024"},{"location":"CHANGELOG/#added_22","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_22","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_22","text":"OSS update - Spring Boot 3.3.5 Security patch for CR/LF exploit for HTTP cookie","title":"Changed"},{"location":"CHANGELOG/#version-409-10182024","text":"","title":"Version 4.0.9, 10/18/2024"},{"location":"CHANGELOG/#added_23","text":"Added Kafka Raft for the Kafka-standalone app.","title":"Added"},{"location":"CHANGELOG/#removed_23","text":"Removed zookeeper from Kafka-standalone app.","title":"Removed"},{"location":"CHANGELOG/#changed_23","text":"Update spring framework verison 6.1.14 to avoid vulnerability in webflux","title":"Changed"},{"location":"CHANGELOG/#version-408-1092024","text":"","title":"Version 4.0.8, 10/9/2024"},{"location":"CHANGELOG/#added_24","text":"Partial support of Active Profile using the \"spring.profiles.active\" parameter Hierarchy of flows","title":"Added"},{"location":"CHANGELOG/#removed_24","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_24","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-407-1012024","text":"","title":"Version 4.0.7, 10/1/2024"},{"location":"CHANGELOG/#added_25","text":"A generic \"no-op\" function for use in event scripts.","title":"Added"},{"location":"CHANGELOG/#removed_25","text":"Feature to ping a function without payload and headers.","title":"Removed"},{"location":"CHANGELOG/#changed_25","text":"Simplified api-playground application","title":"Changed"},{"location":"CHANGELOG/#version-406-9272024","text":"","title":"Version 4.0.6, 9/27/2024"},{"location":"CHANGELOG/#added_26","text":"HTTP request Cookie value filtering using RFC-6265 strict syntax","title":"Added"},{"location":"CHANGELOG/#removed_26","text":"Automatic index page redirection filter for Spring Boot","title":"Removed"},{"location":"CHANGELOG/#changed_26","text":"Upgrade SHA-1 to SHA-512 algorithm in CryptoAPI utility Fix security vulnerability associated with HTTP request header and cookie manipulation","title":"Changed"},{"location":"CHANGELOG/#version-405-9242024","text":"","title":"Version 4.0.5, 9/24/2024"},{"location":"CHANGELOG/#added_27","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_27","text":"Feature for automatic PoJo transport in EventEnvelope and MsgPack Feature for safe.data.model deserialization Benchmark-server is no longer required","title":"Removed"},{"location":"CHANGELOG/#changed_27","text":"Update OSS versions - vertx 4.5.10, kotlin 2.0.20, spring boot 3.3.4","title":"Changed"},{"location":"CHANGELOG/#version-404-952024","text":"","title":"Version 4.0.4, 9/5/2024"},{"location":"CHANGELOG/#added_28","text":"New feature for AsyncHttpClient to render small streaming HTTP response (i.e. chunked binary data) as byte array. For details, Please refer to Appendix III, Developer Guide","title":"Added"},{"location":"CHANGELOG/#removed_28","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_28","text":"Bugfix for parsing default value of environment variable in ConfigReader. This resolves an issue when the special character colon (\":\") is used more than once in the default value.","title":"Changed"},{"location":"CHANGELOG/#version-403-942024","text":"","title":"Version 4.0.3, 9/4/2024"},{"location":"CHANGELOG/#added_29","text":"The \"preload override\" feature is added. This allows overriding a reusable composable library with a set of new route names that are unique for use in an event flow configuration script. For details, Please refer to Chapter 4, Developer Guide","title":"Added"},{"location":"CHANGELOG/#removed_29","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_29","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-402-8312024","text":"","title":"Version 4.0.2, 8/31/2024"},{"location":"CHANGELOG/#added_30","text":"New \"classpath\" namespace for input data mapping Support for input data mapping to handle subset of input request body as a Map or PoJo","title":"Added"},{"location":"CHANGELOG/#removed_30","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_30","text":"Remove the class \"type\" variable from AsyncHttpRequest Improve the \"removeElement\" method in MultiLevelMap Make HTTP input request header labels key-insensitive Update Spring Boot to version 3.3.3","title":"Changed"},{"location":"CHANGELOG/#version-401-8192024","text":"","title":"Version 4.0.1, 8/19/2024"},{"location":"CHANGELOG/#added_31","text":"new File read/write feature in Event Script's I/O data mapping","title":"Added"},{"location":"CHANGELOG/#removed_31","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_31","text":"Update Spring Boot to version 3.3.2 Update Guava to version 33.3.0-jre Update Vertx to version 4.5.9 Update Kotlin to version 2.0.10 Change \"upstream\" to \"dependency\" in the \"/health\" endpoint","title":"Changed"},{"location":"CHANGELOG/#version-400-6242024","text":"This version merges Event Script into the Mercury Composable repository.","title":"Version 4.0.0, 6/24/2024"},{"location":"CHANGELOG/#added_32","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_32","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_32","text":"Update Spring Boot to version 3.3.1 Update Guava to version 33.2.1-jre Update Vertx to version 4.5.8 Update Kotlin to version 2.0.0 Update classgraph to version 4.8.174 Optional reply event for a flow configuration Kafka-standalone is still using Spring Boot 3.2.5 due to compatibility issue","title":"Changed"},{"location":"CHANGELOG/#version-315-512024","text":"This version supercedes 3.1.4 due to updated data structure for static content handling.","title":"Version 3.1.5, 5/1/2024"},{"location":"CHANGELOG/#added_33","text":"Added optional static-content.no-cache-pages in rest.yaml AsyncHttpClientLoader","title":"Added"},{"location":"CHANGELOG/#removed_33","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_33","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-314-4282024","text":"","title":"Version 3.1.4, 4/28/2024"},{"location":"CHANGELOG/#added_34","text":"Added optional static content HTTP-GET request filter in rest.yaml","title":"Added"},{"location":"CHANGELOG/#removed_34","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_34","text":"Updated syntax for static-content-filter","title":"Changed"},{"location":"CHANGELOG/#version-313-4242024","text":"","title":"Version 3.1.3, 4/24/2024"},{"location":"CHANGELOG/#added_35","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_35","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_35","text":"Enhanced OptionalService annotation.","title":"Changed"},{"location":"CHANGELOG/#version-312-4172024","text":"","title":"Version 3.1.2, 4/17/2024"},{"location":"CHANGELOG/#added_36","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_36","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_36","text":"Open sources library update (Spring Boot 3.2.5, Vertx 4.5.7) Improve AppConfigReader and ConfigReader to use the app-config-reader.yml file. Enhanced OptionalService annotation.","title":"Changed"},{"location":"CHANGELOG/#version-311-282024","text":"","title":"Version 3.1.1, 2/8/2024"},{"location":"CHANGELOG/#added_37","text":"AutoStart to run application as Spring Boot if the rest-spring-3 library is packaged in app Configurable \"Event over HTTP\" - automatic forward events over HTTP using a configuration Support user defined serializer with PreLoad annotation and platform API","title":"Added"},{"location":"CHANGELOG/#removed_37","text":"Bugfix: removed websocket client connection timeout that causes the first connection to drop after one minute","title":"Removed"},{"location":"CHANGELOG/#changed_37","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\"","title":"Changed"},{"location":"CHANGELOG/#version-310-152024","text":"","title":"Version 3.1.0, 1/5/2024"},{"location":"CHANGELOG/#added_38","text":"Full integration with Java 21 Virtual Thread Default execution mode is set to \"virtual thread\" KernelThreadRunner annotation added to provide optional support of kernel threads","title":"Added"},{"location":"CHANGELOG/#removed_38","text":"Retired Spring Boot version 2 Hazelcast and ActiveMQ network connectors","title":"Removed"},{"location":"CHANGELOG/#changed_38","text":"platform-core engine updated with virtual thread","title":"Changed"},{"location":"CHANGELOG/#version-307-12232023","text":"","title":"Version 3.0.7, 12/23/2023"},{"location":"CHANGELOG/#added_39","text":"Print out basic JVM information before startup for verification of base container image.","title":"Added"},{"location":"CHANGELOG/#removed_39","text":"Removed Maven Shade packager","title":"Removed"},{"location":"CHANGELOG/#changed_39","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_40","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_40","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_40","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_41","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_41","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_41","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_42","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_42","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_42","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_43","text":"File extension to MIME type mapping for static HTML file handling","title":"Added"},{"location":"CHANGELOG/#removed_43","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_43","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_44","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_44","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_44","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_45","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_45","text":"Retire Google HTTP client","title":"Removed"},{"location":"CHANGELOG/#changed_45","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_46","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_46","text":"Remove blocking APIs from platform-core Retire PM2 process manager sample script due to compatibility issue","title":"Removed"},{"location":"CHANGELOG/#changed_46","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_47","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_47","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_47","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_48","text":"standalone benchmark report app client and server benchmark apps add timeout tag to RPC events","title":"Added"},{"location":"CHANGELOG/#removed_48","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_48","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_49","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_49","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_49","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_50","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_50","text":"Simplify REST automation system by removing websocket notification example in REST automation.","title":"Removed"},{"location":"CHANGELOG/#changed_50","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_51","text":"New Preload annotation class to automate pre-registration of LambdaFunction.","title":"Added"},{"location":"CHANGELOG/#removed_51","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_51","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_52","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_52","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_52","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_53","text":"Add tagging feature to handle language connector's routing and exception handling","title":"Added"},{"location":"CHANGELOG/#removed_53","text":"Remove language pack's pub/sub broadcast feature","title":"Removed"},{"location":"CHANGELOG/#changed_53","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_54","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_54","text":"Remove swagger-ui distribution from api-playground such that developer can clone the latest version","title":"Removed"},{"location":"CHANGELOG/#changed_54","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_55","text":"Enhanced AsyncRequest to handle non-blocking fork-n-join","title":"Added"},{"location":"CHANGELOG/#removed_55","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_55","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_56","text":"Add support of queue API in native pub/sub module for improved ESB compatibility","title":"Added"},{"location":"CHANGELOG/#removed_56","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_56","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-231-2192022","text":"","title":"Version 2.3.1, 2/19/2022"},{"location":"CHANGELOG/#added_57","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_57","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_57","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_58","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_58","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_58","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_59","text":"Transaction journaling Add parameter distributed.trace.aggregation in application.properties such that trace aggregation may be disabled.","title":"Added"},{"location":"CHANGELOG/#removed_59","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_59","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_60","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_60","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_60","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_61","text":"Callback function can implement ServiceExceptionHandler to catch exception. It adds the onError() method.","title":"Added"},{"location":"CHANGELOG/#removed_61","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_61","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_62","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_62","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_62","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_63","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_63","text":"\"object.streams.io\" route is removed from platform-core","title":"Removed"},{"location":"CHANGELOG/#changed_63","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_64","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_64","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_64","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_65","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_65","text":"Legacy websocket notification example application","title":"Removed"},{"location":"CHANGELOG/#changed_65","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-11265-1292020","text":"","title":"Version 1.12.65, 12/9/2020"},{"location":"CHANGELOG/#added_66","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_66","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_66","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_67","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_67","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_67","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-11263-8272020","text":"","title":"Version 1.12.63, 8/27/2020"},{"location":"CHANGELOG/#added_68","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_68","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_68","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_69","text":"New presence monitor's admin endpoint for the operator to force routing table synchronization (\"/api/ping/now\")","title":"Added"},{"location":"CHANGELOG/#removed_69","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_69","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_70","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_70","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_70","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-11256-842020","text":"","title":"Version 1.12.56, 8/4/2020"},{"location":"CHANGELOG/#added_71","text":"Automate trace for fork-n-join use case","title":"Added"},{"location":"CHANGELOG/#removed_71","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_71","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-11255-7192020","text":"","title":"Version 1.12.55, 7/19/2020"},{"location":"CHANGELOG/#added_72","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_72","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_72","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_73","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_73","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_73","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_74","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_74","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_74","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_75","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_75","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_75","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_76","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_76","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_76","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_77","text":"Use RestEasy JAX-RS library","title":"Added"},{"location":"CHANGELOG/#removed_77","text":"For security reason, removed Jersey JAX-RS library","title":"Removed"},{"location":"CHANGELOG/#changed_77","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_78","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_78","text":"For simplicity, retire route-substitution admin endpoint. Route substitution uses a simple static table in route-substitution.yaml.","title":"Removed"},{"location":"CHANGELOG/#changed_78","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-11235-4122020","text":"","title":"Version 1.12.35, 4/12/2020"},{"location":"CHANGELOG/#added_79","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_79","text":"SimpleRBAC class is retired","title":"Removed"},{"location":"CHANGELOG/#changed_79","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_80","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_80","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_80","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_81","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_81","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_81","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_82","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_82","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_82","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_83","text":"Expose \"async.http.request\" as a PUBLIC function (\"HttpClient as a service\")","title":"Added"},{"location":"CHANGELOG/#removed_83","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_83","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_84","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_84","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_84","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_85","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_85","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_85","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_86","text":"Added HTTP relay feature in rest-automation project","title":"Added"},{"location":"CHANGELOG/#removed_86","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_86","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_87","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_87","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_87","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_88","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_88","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_88","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_89","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_89","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_89","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_90","text":"Distributed tracing implemented in platform-core and rest-automation Improved HTTP header transformation for rest-automation","title":"Added"},{"location":"CHANGELOG/#removed_90","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_90","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_91","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_91","text":"rest-core subproject has been merged with rest-spring","title":"Removed"},{"location":"CHANGELOG/#changed_91","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-1127-7152019","text":"","title":"Version 1.12.7, 7/15/2019"},{"location":"CHANGELOG/#added_92","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_92","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_92","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_93","text":"Add inactivity expiry timer to ObjectStreamIO so that house-keeper can clean up resources that are idle","title":"Added"},{"location":"CHANGELOG/#removed_93","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_93","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_94","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_94","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_94","text":"N/A","title":"Changed"},{"location":"CHANGELOG/#version-11140-5252019","text":"","title":"Version 1.11.40, 5/25/2019"},{"location":"CHANGELOG/#added_95","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_95","text":"System log service","title":"Removed"},{"location":"CHANGELOG/#changed_95","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_96","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_96","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_96","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_97","text":"N/A","title":"Added"},{"location":"CHANGELOG/#removed_97","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_97","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_98","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_98","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_98","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_99","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_99","text":"N/A","title":"Removed"},{"location":"CHANGELOG/#changed_99","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_100","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_100","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_100","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":"arch-decisions/DESIGN-NOTES/","text":"Design notes Event choreography by configuration The recommended way to write a composable application is event choreography by configuration using \"Event Script\". This would potentially reduce code size by half. Support sequential synchronous RPC in a non-blocking fashion The foundation library (platform-core) has been integrated with Java 21 virtual thread and Kotlin suspend function features. When a user function makes a RPC call using virtual thread or suspend function, the user function appears to be \"blocked\" so that the code can execute sequentially. Behind the curtain, the function is actually \"suspended\". This makes sequential code with RPC performs as good as reactive code. More importantly, the sequential code represents the intent of the application clearly, thus making code easier to read and maintain. Low level control of function execution strategies You can precisely control how your functions execute, using virtual threads, suspend functions or kernel thread pools to yield the highest performance and throughput. 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 API functional compatibility with Jackson, we have added the writeValueAsString, writeValueAsBytes and readValue methods. The convertValue method has been consolidated into the readValue method. MsgPack For efficient and serialization performance, we use MsgPack as schemaless binary transport for EventEnvelope that contains event metadata, headers and payload. User provided serializers This provides more flexibility for user function to take full control of their PoJo serialization needs. Custom JSON and XML serializers For consistency, we have customized 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. In-memory event system 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. In Mercury version 3.1, the platform core engine is fully integrated with Java 21 virtual thread. Spring Boot 3 The platform-core includes a non-blocking HTTP and websocket server for standalone operation without Spring Boot. The rest-spring-3 library is designed to turn your code to be a Spring Boot application. You may also use the platform-core library with a regular Spring Boot application without the rest-spring-3 library if you prefer. Support of Mono and Flux results A user function may return a regular result that can be a PoJo, HashMap or Java primitive. It can also return a Mono or Flux reactive response object for a future result or a future series of results. Other reactive response objects must be converted to a Mono or Flux object.","title":"Design notes"},{"location":"arch-decisions/DESIGN-NOTES/#design-notes","text":"","title":"Design notes"},{"location":"arch-decisions/DESIGN-NOTES/#event-choreography-by-configuration","text":"The recommended way to write a composable application is event choreography by configuration using \"Event Script\". This would potentially reduce code size by half.","title":"Event choreography by configuration"},{"location":"arch-decisions/DESIGN-NOTES/#support-sequential-synchronous-rpc-in-a-non-blocking-fashion","text":"The foundation library (platform-core) has been integrated with Java 21 virtual thread and Kotlin suspend function features. When a user function makes a RPC call using virtual thread or suspend function, the user function appears to be \"blocked\" so that the code can execute sequentially. Behind the curtain, the function is actually \"suspended\". This makes sequential code with RPC performs as good as reactive code. More importantly, the sequential code represents the intent of the application clearly, thus making code easier to read and maintain.","title":"Support sequential synchronous RPC in a non-blocking fashion"},{"location":"arch-decisions/DESIGN-NOTES/#low-level-control-of-function-execution-strategies","text":"You can precisely control how your functions execute, using virtual threads, suspend functions or kernel thread pools to yield the highest performance and throughput.","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 API functional compatibility with Jackson, we have added the writeValueAsString, writeValueAsBytes and readValue methods. The convertValue method has been consolidated into the readValue method.","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/#user-provided-serializers","text":"This provides more flexibility for user function to take full control of their PoJo serialization needs.","title":"User provided serializers"},{"location":"arch-decisions/DESIGN-NOTES/#custom-json-and-xml-serializers","text":"For consistency, we have customized 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/#in-memory-event-system","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. In Mercury version 3.1, the platform core engine is fully integrated with Java 21 virtual thread.","title":"In-memory event system"},{"location":"arch-decisions/DESIGN-NOTES/#spring-boot-3","text":"The platform-core includes a non-blocking HTTP and websocket server for standalone operation without Spring Boot. The rest-spring-3 library is designed to turn your code to be a Spring Boot application. You may also use the platform-core library with a regular Spring Boot application without the rest-spring-3 library if you prefer.","title":"Spring Boot 3"},{"location":"arch-decisions/DESIGN-NOTES/#support-of-mono-and-flux-results","text":"A user function may return a regular result that can be a PoJo, HashMap or Java primitive. It can also return a Mono or Flux reactive response object for a future result or a future series of results. Other reactive response objects must be converted to a Mono or Flux object.","title":"Support of Mono and Flux results"},{"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 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 protect.info.endpoints true to disable actuators. Default: true Optional trace.http.header comma separated list. Default \"X-Trace-Id\" 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. Partial support of Spring Active Profiles When the parameter \"spring.profiles.active\" is available in application.properties or application.yml, the AppConfigReader will try to load the additional configuration files. For example, if \"spring.profiles.active=dev\", the system will load \"application-dev.properties\" and \"application-dev.yml\" accordingly. When more than one active profile is needed, you can use a comma separated list of profiles in \"spring.profiles.active\". For Spring Boot compatibility, the filename prefix \"application-\" is fixed. 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. Snake or Camel case serializers Serialization and de-serialization of events are performed automatically. 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 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 built-in XML serializer by adding the following HTTP request header. X-Raw-Xml=true Custom content types If you use custom content types in your application, you may add the following section in the application.yml configuration file: custom.content.types: - 'application/vnd.my.org-v2.0+json -> application/json' In the \"custom.content.types\" section, you can configure a list of content-type mappings. The left-hand-side is the custom content-type and the right-hand-side is a standard content-type. The content-type mapping tells the system to treat the custom content type as if it is the standard content type. In the above example, the HTTP payload with the custom content type is treated as a regular JSON content. If you want to put the custom content types in a separate configuration file, please put them in a file named \"custom-content-type.yml\" under the \"resources\" folder. Chapter-9 Home Appendix-II API Overview Table of Contents Reserved names and headers","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 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 protect.info.endpoints true to disable actuators. Default: true Optional trace.http.header comma separated list. Default \"X-Trace-Id\" 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/#partial-support-of-spring-active-profiles","text":"When the parameter \"spring.profiles.active\" is available in application.properties or application.yml, the AppConfigReader will try to load the additional configuration files. For example, if \"spring.profiles.active=dev\", the system will load \"application-dev.properties\" and \"application-dev.yml\" accordingly. When more than one active profile is needed, you can use a comma separated list of profiles in \"spring.profiles.active\". For Spring Boot compatibility, the filename prefix \"application-\" is fixed.","title":"Partial support of Spring Active Profiles"},{"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/#snake-or-camel-case-serializers","text":"Serialization and de-serialization of events are performed automatically. 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":"Snake or Camel case serializers"},{"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 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 built-in XML serializer by adding the following HTTP request header. X-Raw-Xml=true","title":"Built-in XML serializer"},{"location":"guides/APPENDIX-I/#custom-content-types","text":"If you use custom content types in your application, you may add the following section in the application.yml configuration file: custom.content.types: - 'application/vnd.my.org-v2.0+json -> application/json' In the \"custom.content.types\" section, you can configure a list of content-type mappings. The left-hand-side is the custom content-type and the right-hand-side is a standard content-type. The content-type mapping tells the system to treat the custom content type as if it is the standard content type. In the above example, the HTTP payload with the custom content type is treated as a regular JSON content. If you want to put the custom content types in a separate configuration file, please put them in a file named \"custom-content-type.yml\" under the \"resources\" folder. Chapter-9 Home Appendix-II API Overview Table of Contents Reserved names and headers","title":"Custom content types"},{"location":"guides/APPENDIX-II/","text":"Reserved names The system reserves some route names and headers for routing purpose. System 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 overload 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 event.script.manager Instantiate new event flow instance event-script task.executor Perform event choreography event-script http.flow.adapter Built-in flow adapter event-script no.op no-operation placeholder function event-script 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. No-op function The \"no.op\" function is used as a placeholder for building skeleton or simple decision function for an event flow use case. 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); Reserved HTTP header names Header Purpose X-Stream-Id Temporal route name for streaming content X-TTL Time to live in milliseconds for a streaming content X-Small-Payload-As-Bytes This header, if set to true, tells system to render stream content as bytes X-Event-Api The system uses this header to indicate that the request is sent over HTTP X-Async This header, if set to true, indicates it is a drop-n-forget request X-Trace-Id This allows the system to propagate trace ID X-Correlation-Id Alternative to X-Trace-Id X-Content-Length If present, it is the expected length of a streaming content X-Raw-Xml This header, if set to true, tells to system to skip XML rendering X-Flow-Id This tells the event manager to select a flow configuration by ID X-App-Instance This header is used by some protected actuator REST endpoints To support traceId that is stored in X-Correlation-Id HTTP header, set this in application.properties. # list of supported traceId headers where the first one is the default label trace.http.header=X-Correlation-Id, X-Trace-Id Appendix-I Home Appendix-III Application Configuration Table of Contents Actuators, HTTP client and More","title":"Appendix-II"},{"location":"guides/APPENDIX-II/#reserved-names","text":"The system reserves some route names and headers for routing purpose.","title":"Reserved names"},{"location":"guides/APPENDIX-II/#system-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 overload 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 event.script.manager Instantiate new event flow instance event-script task.executor Perform event choreography event-script http.flow.adapter Built-in flow adapter event-script no.op no-operation placeholder function event-script 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":"System 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/#no-op-function","text":"The \"no.op\" function is used as a placeholder for building skeleton or simple decision function for an event flow use case.","title":"No-op function"},{"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);","title":"Reserved event header names"},{"location":"guides/APPENDIX-II/#reserved-http-header-names","text":"Header Purpose X-Stream-Id Temporal route name for streaming content X-TTL Time to live in milliseconds for a streaming content X-Small-Payload-As-Bytes This header, if set to true, tells system to render stream content as bytes X-Event-Api The system uses this header to indicate that the request is sent over HTTP X-Async This header, if set to true, indicates it is a drop-n-forget request X-Trace-Id This allows the system to propagate trace ID X-Correlation-Id Alternative to X-Trace-Id X-Content-Length If present, it is the expected length of a streaming content X-Raw-Xml This header, if set to true, tells to system to skip XML rendering X-Flow-Id This tells the event manager to select a flow configuration by ID X-App-Instance This header is used by some protected actuator REST endpoints To support traceId that is stored in X-Correlation-Id HTTP header, set this in application.properties. # list of supported traceId headers where the first one is the default label trace.http.header=X-Correlation-Id, X-Trace-Id Appendix-I Home Appendix-III Application Configuration Table of Contents Actuators, HTTP client and More","title":"Reserved HTTP header names"},{"location":"guides/APPENDIX-III/","text":"Actuators, HTTP client and More 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 or a Map of the health check. e.g. read/write test result. If health check fails, you can throw AppException with status code and error message. A sample health service is available in the DemoHealth class of the composable-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. * It must return a Map. */ if (INFO.equals(headers.get(TYPE))) { Map about = new HashMap<>(); about.put(\"service\", \"demo.service\"); about.put(\"href\", \"http://127.0.0.1\"); return about; } if (HEALTH.equals(headers.get(TYPE))) { /* * This is a place-holder for checking a downstream service. * * Please implement your own logic to test if a downstream service is running fine. * If running, just return health status as a String or a Map. * * Otherwise, * throw new AppException(status, message) */ return Map.of(\"demo\", \"I am 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); EventEnvelope res = po.request(request, 5000); // the result is in res.getBody() By default, your user function is running in a virtual thread. While the RPC call looks like synchronous, the po.request API will run in non-blocking mode in the same fashion as the \"async/await\" pattern. For reactive programming, you can use the \"asyncRequest\" API 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 }); If you prefer writing in Kotlin, you can create 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); EventPublisher publisher = new EventPublisher(timeoutInMIlls); while ((len = in.read(buffer, 0, buffer.length)) != -1) { publisher.publish(buffer, 0, len); } // closing the output stream would send a EOF signal to the stream publisher.publishCompletion(); // tell the HTTP client to read the input stream by setting the streamId in the AsyncHttpRequest object req.setStreamRoute(publisher.getStreamId()); Read HTTP response body stream If content length is not given, the response body would arrive as a stream. Your application should check if the HTTP response header \"stream\" exists. Its value is the input \"streamId\". You can process the input stream using the FluxConsumer class like this: String streamId = headers.get(\"stream\"); long ttl = 10000; // anticipated time in milliseconds to stream the content FluxConsumer> fc = new FluxConsumer<>(streamId, ttl); fc.consume( data -> { // handle incoming message }, e -> { // handle exception where e is a Throwable }, () -> { // handle stream completion } ); By default, a user function is executed in a virtual thread which effectively is an \"async\" function and the PostOffice \"request\" API operates in the non-blocking \"await\" mode. If you prefers writing in Kotlin, 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 } } } Rendering a small payload of streaming content If the streaming HTTP response is certain to be a small payload (i.e. Kilobytes), you can optimize the rendering by adding the HTTP request header (X-Small-Payload-As-Bytes=true) in the AsyncHttpRequest object. AsyncHttpRequest req = new AsyncHttpRequest(); req.setMethod(\"GET\"); req.setUrl(\"/api/some/binary/content\"); req.setTargetHost(\"https://service_provider_host\"); req.setHeader(\"X-Small-Payload-As-Bytes\", \"true\"); Note that the AsyncHttpClient will insert a custom HTTP response header \"X-Content-Length\" to show the size of the payload. IMPORTANT: This optimization does not validate the size of the streaming content. Therefore, it is possible for the streaming content to trigger an \"out of memory\" exception. You must make sure the streaming content is small enough before using the \"X-Small-Payload-As-Bytes\" HTTP request header. 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. Starting a flow programmatically To start an \"event\" flow from a unit test, you may use the helper class \"FlowExecutor\" under the \"Event Script\" module. Examples of some APIs are as follows: // launch a flow asychronously public void launch(String originator, String flowId, Map dataset, String correlationId) throws IOException; // launch a flow asychronously with tracing public void launch(String originator, String traceId, String tracePath, String flowId, Map dataset, String correlationId) throws IOException // launch a flow asychronously and tracing public void launch(PostOffice po, String flowId, Map dataset, String correlationId) throws IOException; // launch a flow with callback and tracing public void launch(PostOffice po, String flowId, Map dataset, String replyTo, String correlationId) throws IOException; // launch a flow and expect a future response public Future request(PostOffice po, String flowId, Map dataset, String correlationId, long timeout) throws IOException; The following unit test emulates a HTTP request to the flow named \"header-test\". @Test public void internalFlowTest() throws IOException, ExecutionException, InterruptedException { final long TIMEOUT = 8000; String traceId = Utility.getInstance().getUuid(); String cid = Utility.getInstance().getUuid(); PostOffice po = new PostOffice(\"unit.test\", traceId, \"INTERNAL /flow/test\"); String flowId = \"header-test\"; Map headers = new HashMap<>(); Map dataset = new HashMap<>(); dataset.put(\"header\", headers); dataset.put(\"body\", Map.of(\"hello\", \"world\")); headers.put(\"user-agent\", \"internal-flow\"); headers.put(\"accept\", \"application/json\"); headers.put(\"x-flow-id\", flowId); FlowExecutor flowExecutor = FlowExecutor.getInstance(); EventEnvelope result = flowExecutor.request(po, flowId, dataset, cid, TIMEOUT).get(); assertInstanceOf(Map.class, result.getBody()); Map body = (Map) result.getBody(); // verify that input headers are mapped to the function's input body assertEquals(\"header-test\", body.get(\"x-flow-id\")); assertEquals(\"internal-flow\", body.get(\"user-agent\")); assertEquals(\"application/json\", body.get(\"accept\")); } The dataset must contain at least the \"body\" key-value so that input data mapping is possible in a flow. For the built-in HTTP flow adapter, the dataset would contain the following: // convert HTTP context to flow \"input\" dataset Map dataset = new HashMap<>(); dataset.put(\"header\", request.getHeaders()); dataset.put(\"body\", request.getBody()); dataset.put(\"cookie\", request.getCookies()); dataset.put(\"path_parameter\", request.getPathParameters()); dataset.put(\"method\", request.getMethod()); dataset.put(\"uri\", request.getUrl()); dataset.put(\"query\", request.getQueryParameters()); dataset.put(\"stream\", request.getStreamRoute()); dataset.put(\"ip\", request.getRemoteIp()); dataset.put(\"filename\", request.getFileName()); dataset.put(\"session\", request.getSessionInfo()); If you write your own Kafka flow adapter, the dataset should contain headers and body mapped with a Kafka event. For other flow adapters, you may use different set of key-values. Appendix-II Home Reserved names and headers Table of Contents","title":"Appendix-III"},{"location":"guides/APPENDIX-III/#actuators-http-client-and-more","text":"","title":"Actuators, HTTP client and More"},{"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 or a Map of the health check. e.g. read/write test result. If health check fails, you can throw AppException with status code and error message. A sample health service is available in the DemoHealth class of the composable-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. * It must return a Map. */ if (INFO.equals(headers.get(TYPE))) { Map about = new HashMap<>(); about.put(\"service\", \"demo.service\"); about.put(\"href\", \"http://127.0.0.1\"); return about; } if (HEALTH.equals(headers.get(TYPE))) { /* * This is a place-holder for checking a downstream service. * * Please implement your own logic to test if a downstream service is running fine. * If running, just return health status as a String or a Map. * * Otherwise, * throw new AppException(status, message) */ return Map.of(\"demo\", \"I am 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); EventEnvelope res = po.request(request, 5000); // the result is in res.getBody() By default, your user function is running in a virtual thread. While the RPC call looks like synchronous, the po.request API will run in non-blocking mode in the same fashion as the \"async/await\" pattern. For reactive programming, you can use the \"asyncRequest\" API 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 }); If you prefer writing in Kotlin, you can create 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); EventPublisher publisher = new EventPublisher(timeoutInMIlls); while ((len = in.read(buffer, 0, buffer.length)) != -1) { publisher.publish(buffer, 0, len); } // closing the output stream would send a EOF signal to the stream publisher.publishCompletion(); // tell the HTTP client to read the input stream by setting the streamId in the AsyncHttpRequest object req.setStreamRoute(publisher.getStreamId());","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 would arrive as a stream. Your application should check if the HTTP response header \"stream\" exists. Its value is the input \"streamId\". You can process the input stream using the FluxConsumer class like this: String streamId = headers.get(\"stream\"); long ttl = 10000; // anticipated time in milliseconds to stream the content FluxConsumer> fc = new FluxConsumer<>(streamId, ttl); fc.consume( data -> { // handle incoming message }, e -> { // handle exception where e is a Throwable }, () -> { // handle stream completion } ); By default, a user function is executed in a virtual thread which effectively is an \"async\" function and the PostOffice \"request\" API operates in the non-blocking \"await\" mode. If you prefers writing in Kotlin, 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/#rendering-a-small-payload-of-streaming-content","text":"If the streaming HTTP response is certain to be a small payload (i.e. Kilobytes), you can optimize the rendering by adding the HTTP request header (X-Small-Payload-As-Bytes=true) in the AsyncHttpRequest object. AsyncHttpRequest req = new AsyncHttpRequest(); req.setMethod(\"GET\"); req.setUrl(\"/api/some/binary/content\"); req.setTargetHost(\"https://service_provider_host\"); req.setHeader(\"X-Small-Payload-As-Bytes\", \"true\"); Note that the AsyncHttpClient will insert a custom HTTP response header \"X-Content-Length\" to show the size of the payload. IMPORTANT: This optimization does not validate the size of the streaming content. Therefore, it is possible for the streaming content to trigger an \"out of memory\" exception. You must make sure the streaming content is small enough before using the \"X-Small-Payload-As-Bytes\" HTTP request header.","title":"Rendering a small payload of streaming content"},{"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.","title":"Content length for HTTP request"},{"location":"guides/APPENDIX-III/#starting-a-flow-programmatically","text":"To start an \"event\" flow from a unit test, you may use the helper class \"FlowExecutor\" under the \"Event Script\" module. Examples of some APIs are as follows: // launch a flow asychronously public void launch(String originator, String flowId, Map dataset, String correlationId) throws IOException; // launch a flow asychronously with tracing public void launch(String originator, String traceId, String tracePath, String flowId, Map dataset, String correlationId) throws IOException // launch a flow asychronously and tracing public void launch(PostOffice po, String flowId, Map dataset, String correlationId) throws IOException; // launch a flow with callback and tracing public void launch(PostOffice po, String flowId, Map dataset, String replyTo, String correlationId) throws IOException; // launch a flow and expect a future response public Future request(PostOffice po, String flowId, Map dataset, String correlationId, long timeout) throws IOException; The following unit test emulates a HTTP request to the flow named \"header-test\". @Test public void internalFlowTest() throws IOException, ExecutionException, InterruptedException { final long TIMEOUT = 8000; String traceId = Utility.getInstance().getUuid(); String cid = Utility.getInstance().getUuid(); PostOffice po = new PostOffice(\"unit.test\", traceId, \"INTERNAL /flow/test\"); String flowId = \"header-test\"; Map headers = new HashMap<>(); Map dataset = new HashMap<>(); dataset.put(\"header\", headers); dataset.put(\"body\", Map.of(\"hello\", \"world\")); headers.put(\"user-agent\", \"internal-flow\"); headers.put(\"accept\", \"application/json\"); headers.put(\"x-flow-id\", flowId); FlowExecutor flowExecutor = FlowExecutor.getInstance(); EventEnvelope result = flowExecutor.request(po, flowId, dataset, cid, TIMEOUT).get(); assertInstanceOf(Map.class, result.getBody()); Map body = (Map) result.getBody(); // verify that input headers are mapped to the function's input body assertEquals(\"header-test\", body.get(\"x-flow-id\")); assertEquals(\"internal-flow\", body.get(\"user-agent\")); assertEquals(\"application/json\", body.get(\"accept\")); } The dataset must contain at least the \"body\" key-value so that input data mapping is possible in a flow. For the built-in HTTP flow adapter, the dataset would contain the following: // convert HTTP context to flow \"input\" dataset Map dataset = new HashMap<>(); dataset.put(\"header\", request.getHeaders()); dataset.put(\"body\", request.getBody()); dataset.put(\"cookie\", request.getCookies()); dataset.put(\"path_parameter\", request.getPathParameters()); dataset.put(\"method\", request.getMethod()); dataset.put(\"uri\", request.getUrl()); dataset.put(\"query\", request.getQueryParameters()); dataset.put(\"stream\", request.getStreamRoute()); dataset.put(\"ip\", request.getRemoteIp()); dataset.put(\"filename\", request.getFileName()); dataset.put(\"session\", request.getSessionInfo()); If you write your own Kafka flow adapter, the dataset should contain headers and body mapped with a Kafka event. For other flow adapters, you may use different set of key-values. Appendix-II Home Reserved names and headers Table of Contents","title":"Starting a flow programmatically"},{"location":"guides/CHAPTER-1/","text":"Introduction Mercury Composable is a software development toolkit for writing composable applications. 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 application architecture Figure 1 - Composable application architecture As shown in Figure 1, a composable application contains the following: Flow adapters : Each flow adapter listens to requests for onwards delivery to an event manager. Event Manager : it sends events to a set of user functions for them to work together as an application. User functions : these are self-contained functions with clear input and output that are immutable. HTTP flow adapter A non-blocking HTTP flow adapter is built-in. For other external interface types, you can implement your own flow adapters. e.g. Adapters for MQ, Kafka, Serverless, File based staging area, etc. The standard HTTP flow adapter leverages the underlying Mercury REST automation system to serve user facing REST API endpoints. For example, a hypothetical \"get profile\" endpoint is created like this in the \"rest.yaml\" configuration file: - service: \"http.flow.adapter\" methods: ['GET'] url: \"/api/profile/{profile_id}\" flow: 'get-profile' timeout: 10s cors: cors_1 headers: header_1 tracing: true In this REST configuration entry, the system creates a REST API endpoint for \"GET /api/profile/{profile_id}\". When a request arrives at this endpoint, the HTTP request will be converted to an incoming event by the flow adapter that routes the event to the \"event manager\" to execute a new instance of the \"get-profile\" flow. Flow configuration example The event manager is driven by configuration instead of code. A hypothetical \"get profile\" flow is defined in a YAML file like this: flow: id: 'get-profile' description: 'Get a user profile using profile ID' ttl: 10s exception: 'v1.hello.exception' first.task: 'v1.get.profile' tasks: - input: - 'input.path_parameter.profile_id -> header.profile_id' process: 'v1.get.profile' output: - 'result -> model.profile' description: 'Retrieve user profile from database using profile_id' execution: sequential next: - 'v1.decrypt.fields' - input: - 'model.profile -> dataset' - 'text(telephone, address) -> protected_fields' process: 'v1.decrypt.fields' output: - 'text(application/json) -> output.header.content-type' - 'result -> output.body' description: 'Decrypt fields' execution: end - input: - 'error.code -> status' - 'error.message -> message' - 'error.stack -> stack' process: 'v1.hello.exception' output: - 'result.status -> output.status' - 'result -> output.body' description: 'Just a demo exception handler' execution: end Note that the flow configuration is referring user functions by their \"route\" names. It is because all user functions are self-contained with clearly defined input and output and the event manager would set their inputs and collect their outputs accordingly. Note that you can map selected key-values or the whole event as a business object and this decoupling promotes highly reusable user functional software. The event manager will create a \"state machine\" to manage each transaction flow because all user functions are stateless. The \"state machine\" is referenced using the namespace \"model\". Assigning a route name to a user function You can assign a route name to a Java class using the PreLoad annotation like this: @PreLoad(route=\"v1.get.profile\", instances=100) public class GetProfile implements TypedLambdaFunction, Profile> { @Override public Profile handleEvent(Map headers, Map input, int instance) { // your business logic here return result; } } Inside the \"handleEvent\" method, you can write regular Java code using your preferred coding style and framework. You can define input/output as Map or PoJo. Building the Mercury libraries from source Mercury Composable leverages the best of Java 21 virtual threading technology. Therefore, you would need to install Java JDK version 21 or higher. You also need maven version 3.9.7 or higher to build the libraries. Assuming you clone the Mercury repository into the \"sandbox\" directory, you may build the libraries like this. cd sandbox/mercury-composable mvn clean install The compiled libraries will be saved to your local \".m2\" maven repository. For convenience, you may also publish the Mercury libraries into your enterprise artifactory. We use \"maven\" build scripts. If your organization uses other build tools such as gradle, please convert them accordingly. Things to avoid with Java 21 By default, user functions are executed using Java 21 virtual threading technology. However, for performance reason, there are two things that you MUST avoid: Synchronized keyword : This will block the event loop in the Java VM, meaning that your whole application is blocked when the synchronized block executes. ThreadLocal : Java 21 virtual thread is designed to be very light weight. When you use ThreadLocal variables, the \"virtual thread\" becomes heavy weighted and the Garbage Collector may have difficulties catching up. Since Mercury provides thread management abstraction, there is no need to use the Synchronized keyword and ThreadLocal variables. The built-in \"state machine\" is a better place to keep your runtime variables for each transaction. Interestingly, the \"Thread\" and \"Future\" APIs are safe to use in a virtual thread. If you are putting legacy code inside a new user function and the legacy code runs in blocking mode, you can annotate the user function with the \"KernelThreadRunner\" class. This tells the system to turn on compatibility mode to support the blocking code. The kernel thread would isolate the blocking code from the rest of the application. However, kernel threads are limited resources. While virtual threads can support tens of thousands of cooperative concurrent execution, kernel threads are limited to 250, depending on the number of CPU cores that the target machine has. Composable application example Let's take a test drive of a composable application example in the \"examples/composable-example\" subproject. You can use your favorite IDE to run the example or execute it from a terminal using command line. To run it from the command line, you may do this: cd sandbox/mercury-composable/examples/composable-example java -jar target/composable-example-4.0.9.jar If you run the application from the IDE, you may execute the \"main\" method in the MainApp class under the \"com.accenture.demo.start\" package folder. The first step in designing a composable application is to draw an event flow diagram. This is similar to a data flow diagram where the arrows are labeled with the event objects. Note that event flow diagram is not a flow chart and thus decision box is not required. If a user function (also known as a \"task\") contains decision logic, you can draw two or more output from the task to connect to the next set of functions. For example, label the arrows as true, false or a number starting from 1. The composable-example application is a hypothetical \"profile management system\" where you can create a profile, browse or delete it. Figure 2 - Event flow diagram As shown in Figure 2, there are three event flows. One for \"get profile\", one for \"delete profile\" and the other one for \"create profile\". The REST endpoints for the three use cases are shown in the \"rest.yaml\" configuration file under the \"main/resources\" in the example subproject. You also find the following configuration parameters in \"application.properties\": rest.server.port=8100 rest.automation=true yaml.rest.automation=classpath:/rest.yaml yaml.flow.automation=classpath:/flows.yaml The flow configuration files are shown in the \"main/resources/flows\" folder where you will find the flow configuration files for the three event flows, namely get-profile.yml, delete-profile.yml and create-profile.yml. Starting the application When the application is started, you will see application log like this: CompileFlows:142 - Loaded create-profile CompileFlows:142 - Loaded delete-profile CompileFlows:142 - Loaded get-profile CompileFlows:144 - Event scripts deployed: 3 ... ServiceQueue:91 - PRIVATE v1.get.profile with 100 instances started as virtual threads ... RoutingEntry:582 - GET /api/profile/{profile_id} -> [http.flow.adapter], timeout=10s, tracing=true, flow=get-profile ... AppStarter:378 - Modules loaded in 663 ms AppStarter:365 - Reactive HTTP server running on port-8100 Note that the above log is trimmed for presentation purpose. It shows that the 3 flow configuration files are compiled as objects for performance reason. The user functions are loaded into the event system and the REST endpoints are rendered from the \"rest.yaml\" file. Testing the application You can create a test user profile with this python code. Alternatively, you can also use PostMan or other means to do this. >>> import requests, json >>> d = { 'id': 12345, 'name': 'Hello World', 'address': '100 World Blvd', 'telephone': '123-456-7890' } >>> h = { 'content-type': 'application/json', 'accept': 'application/json' } >>> r = requests.post('http://127.0.0.1:8100/api/profile', data=json.dumps(d), headers=h) >>> print(r.status_code) 201 >>> print(r.text) { \"profile\": { \"address\": \"***\", \"name\": \"Hello World\", \"telephone\": \"***\", \"id\": 12345 }, \"type\": \"CREATE\", \"secure\": [ \"address\", \"telephone\" ] } To verify that the user profile has been created, you can point your browser to http://127.0.0.1:8100/api/profile/12345 Your browser will return the following: { \"address\": \"100 World Blvd\", \"name\": \"Hello World\", \"telephone\": \"123-456-7890\", \"id\": 12345 } You have successfully tested the two REST endpoints. Tracing information in the application log may look like this: DistributedTrace:76 - trace={path=POST /api/profile, service=http.flow.adapter, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.524Z, exec_time=0.284, from=http.request, id=f6a6ae62340e43afb0a6f30445166e08} DistributedTrace:76 - trace={path=POST /api/profile, service=event.script.manager, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.525Z, exec_time=0.57, from=http.flow.adapter, id=f6a6ae62340e43afb0a6f30445166e08} DistributedTrace:76 - trace={path=POST /api/profile, service=v1.create.profile, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.526Z, exec_time=0.342, from=task.executor, id=f6a6ae62340e43afb0a6f30445166e08} DistributedTrace:76 - trace={path=POST /api/profile, service=async.http.response, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.528Z, exec_time=0.294, from=task.executor, id=f6a6ae62340e43afb0a6f30445166e08} DistributedTrace:76 - trace={path=POST /api/profile, service=v1.encrypt.fields, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.528Z, exec_time=3.64, from=task.executor, id=f6a6ae62340e43afb0a6f30445166e08} SaveProfile:52 - Profile 12345 saved TaskExecutor:186 - Flow create-profile (f6a6ae62340e43afb0a6f30445166e08) completed in 11 ms DistributedTrace:76 - trace={path=POST /api/profile, service=v1.save.profile, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.533Z, exec_time=2.006, from=task.executor, id=f6a6ae62340e43afb0a6f30445166e08} DistributedTrace:76 - trace={path=GET /api/profile/12345, service=http.flow.adapter, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:52.089Z, exec_time=0.152, from=http.request, id=1a29105044e94cc3ac68aee002f6f429} DistributedTrace:76 - trace={path=GET /api/profile/12345, service=event.script.manager, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:52.090Z, exec_time=0.291, from=http.flow.adapter, id=1a29105044e94cc3ac68aee002f6f429} DistributedTrace:76 - trace={path=GET /api/profile/12345, service=v1.get.profile, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:52.091Z, exec_time=1.137, from=task.executor, id=1a29105044e94cc3ac68aee002f6f429} DistributedTrace:76 - trace={path=GET /api/profile/12345, service=v1.decrypt.fields, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:52.093Z, exec_time=1.22, from=task.executor, id=1a29105044e94cc3ac68aee002f6f429} TaskExecutor:186 - Flow get-profile (1a29105044e94cc3ac68aee002f6f429) completed in 4 ms DistributedTrace:76 - trace={path=GET /api/profile/12345, service=async.http.response, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:52.095Z, exec_time=0.214, from=task.executor, id=1a29105044e94cc3ac68aee002f6f429} Main module Every application has an entry point. The MainApp in the example app contains the entry point 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\"); } } Since your application is event driven, the main application does not need any additional code in the above example. However, this is a good place to put application initialization code if any. There is also a \"BeforeApplication\" annotation if you want to run some start up code before the event system is started. Dependency management As a best practice, your user functions should not have any dependencies with other user functions. However, within a single user function, you may use your preferred framework or libraries. For maintainability, we do recommend to reduce library dependencies as much as you can. For example, you want to push JDBC or JPA dependency to a small number of user functions (for CRUD operation) so that the rest of the user functions do not need any DB dependencies. Component scan Please update the following in the application.properties (or application.yml) to include packages of your own functions: web.component.scan=your.package.name You should replace \"your.package.name\" with the real package name(s) that you use in your application. Usually this is your organization software ID or \"namespace\". \"web.component.scan\" is a comma separated list of package names. Deploy your application Composable design can be used to create microservices. You can put related functions in a bounded context with database persistence. Each composable application 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 eclipse-temurin:21.0.1_12-jdk EXPOSE 8083 WORKDIR /app COPY target/your-app-name.jar . ENTRYPOINT [\"java\",\"-jar\",\"your-app-name.jar\"] The above Dockerfile will fetch Openjdk 21 packaged in \"Ubuntu 22.04 LTS\". Event choreography by configuration The best practice for composable design is event choreography by configuration ( Event Script ) discussed above. We will examine the Event Script syntax in Chapter 4 . Generally, you do not need to use Mercury core APIs in your user functions. For composable applications that use Event Script, Mercury core APIs (Platform, PostOffice and FastRPC) are only required for writing unit tests, \"custom flow adapters\", \"legacy functional wrappers\" or \"external gateways\". Orchestration by code Orchestration by code is strongly discouraged because it would result in tightly coupled code . For example, just an \"Import\" statement of another function would create tight coupling of two pieces of code, even when using reactive or event-driven programming styles. However, if there is a use case that you prefer to write orchestration logic by code, you may use the Mercury core APIs to do event-driven programming. API overview will be covered in Chapter 9 . Home Chapter-2 Table of Contents Function Execution Strategy","title":"Chapter-1"},{"location":"guides/CHAPTER-1/#introduction","text":"Mercury Composable is a software development toolkit for writing composable applications. 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.","title":"Introduction"},{"location":"guides/CHAPTER-1/#composable-application-architecture","text":"Figure 1 - Composable application architecture As shown in Figure 1, a composable application contains the following: Flow adapters : Each flow adapter listens to requests for onwards delivery to an event manager. Event Manager : it sends events to a set of user functions for them to work together as an application. User functions : these are self-contained functions with clear input and output that are immutable.","title":"Composable application architecture"},{"location":"guides/CHAPTER-1/#http-flow-adapter","text":"A non-blocking HTTP flow adapter is built-in. For other external interface types, you can implement your own flow adapters. e.g. Adapters for MQ, Kafka, Serverless, File based staging area, etc. The standard HTTP flow adapter leverages the underlying Mercury REST automation system to serve user facing REST API endpoints. For example, a hypothetical \"get profile\" endpoint is created like this in the \"rest.yaml\" configuration file: - service: \"http.flow.adapter\" methods: ['GET'] url: \"/api/profile/{profile_id}\" flow: 'get-profile' timeout: 10s cors: cors_1 headers: header_1 tracing: true In this REST configuration entry, the system creates a REST API endpoint for \"GET /api/profile/{profile_id}\". When a request arrives at this endpoint, the HTTP request will be converted to an incoming event by the flow adapter that routes the event to the \"event manager\" to execute a new instance of the \"get-profile\" flow.","title":"HTTP flow adapter"},{"location":"guides/CHAPTER-1/#flow-configuration-example","text":"The event manager is driven by configuration instead of code. A hypothetical \"get profile\" flow is defined in a YAML file like this: flow: id: 'get-profile' description: 'Get a user profile using profile ID' ttl: 10s exception: 'v1.hello.exception' first.task: 'v1.get.profile' tasks: - input: - 'input.path_parameter.profile_id -> header.profile_id' process: 'v1.get.profile' output: - 'result -> model.profile' description: 'Retrieve user profile from database using profile_id' execution: sequential next: - 'v1.decrypt.fields' - input: - 'model.profile -> dataset' - 'text(telephone, address) -> protected_fields' process: 'v1.decrypt.fields' output: - 'text(application/json) -> output.header.content-type' - 'result -> output.body' description: 'Decrypt fields' execution: end - input: - 'error.code -> status' - 'error.message -> message' - 'error.stack -> stack' process: 'v1.hello.exception' output: - 'result.status -> output.status' - 'result -> output.body' description: 'Just a demo exception handler' execution: end Note that the flow configuration is referring user functions by their \"route\" names. It is because all user functions are self-contained with clearly defined input and output and the event manager would set their inputs and collect their outputs accordingly. Note that you can map selected key-values or the whole event as a business object and this decoupling promotes highly reusable user functional software. The event manager will create a \"state machine\" to manage each transaction flow because all user functions are stateless. The \"state machine\" is referenced using the namespace \"model\".","title":"Flow configuration example"},{"location":"guides/CHAPTER-1/#assigning-a-route-name-to-a-user-function","text":"You can assign a route name to a Java class using the PreLoad annotation like this: @PreLoad(route=\"v1.get.profile\", instances=100) public class GetProfile implements TypedLambdaFunction, Profile> { @Override public Profile handleEvent(Map headers, Map input, int instance) { // your business logic here return result; } } Inside the \"handleEvent\" method, you can write regular Java code using your preferred coding style and framework. You can define input/output as Map or PoJo.","title":"Assigning a route name to a user function"},{"location":"guides/CHAPTER-1/#building-the-mercury-libraries-from-source","text":"Mercury Composable leverages the best of Java 21 virtual threading technology. Therefore, you would need to install Java JDK version 21 or higher. You also need maven version 3.9.7 or higher to build the libraries. Assuming you clone the Mercury repository into the \"sandbox\" directory, you may build the libraries like this. cd sandbox/mercury-composable mvn clean install The compiled libraries will be saved to your local \".m2\" maven repository. For convenience, you may also publish the Mercury libraries into your enterprise artifactory. We use \"maven\" build scripts. If your organization uses other build tools such as gradle, please convert them accordingly.","title":"Building the Mercury libraries from source"},{"location":"guides/CHAPTER-1/#things-to-avoid-with-java-21","text":"By default, user functions are executed using Java 21 virtual threading technology. However, for performance reason, there are two things that you MUST avoid: Synchronized keyword : This will block the event loop in the Java VM, meaning that your whole application is blocked when the synchronized block executes. ThreadLocal : Java 21 virtual thread is designed to be very light weight. When you use ThreadLocal variables, the \"virtual thread\" becomes heavy weighted and the Garbage Collector may have difficulties catching up. Since Mercury provides thread management abstraction, there is no need to use the Synchronized keyword and ThreadLocal variables. The built-in \"state machine\" is a better place to keep your runtime variables for each transaction. Interestingly, the \"Thread\" and \"Future\" APIs are safe to use in a virtual thread. If you are putting legacy code inside a new user function and the legacy code runs in blocking mode, you can annotate the user function with the \"KernelThreadRunner\" class. This tells the system to turn on compatibility mode to support the blocking code. The kernel thread would isolate the blocking code from the rest of the application. However, kernel threads are limited resources. While virtual threads can support tens of thousands of cooperative concurrent execution, kernel threads are limited to 250, depending on the number of CPU cores that the target machine has.","title":"Things to avoid with Java 21"},{"location":"guides/CHAPTER-1/#composable-application-example","text":"Let's take a test drive of a composable application example in the \"examples/composable-example\" subproject. You can use your favorite IDE to run the example or execute it from a terminal using command line. To run it from the command line, you may do this: cd sandbox/mercury-composable/examples/composable-example java -jar target/composable-example-4.0.9.jar If you run the application from the IDE, you may execute the \"main\" method in the MainApp class under the \"com.accenture.demo.start\" package folder. The first step in designing a composable application is to draw an event flow diagram. This is similar to a data flow diagram where the arrows are labeled with the event objects. Note that event flow diagram is not a flow chart and thus decision box is not required. If a user function (also known as a \"task\") contains decision logic, you can draw two or more output from the task to connect to the next set of functions. For example, label the arrows as true, false or a number starting from 1. The composable-example application is a hypothetical \"profile management system\" where you can create a profile, browse or delete it. Figure 2 - Event flow diagram As shown in Figure 2, there are three event flows. One for \"get profile\", one for \"delete profile\" and the other one for \"create profile\". The REST endpoints for the three use cases are shown in the \"rest.yaml\" configuration file under the \"main/resources\" in the example subproject. You also find the following configuration parameters in \"application.properties\": rest.server.port=8100 rest.automation=true yaml.rest.automation=classpath:/rest.yaml yaml.flow.automation=classpath:/flows.yaml The flow configuration files are shown in the \"main/resources/flows\" folder where you will find the flow configuration files for the three event flows, namely get-profile.yml, delete-profile.yml and create-profile.yml.","title":"Composable application example"},{"location":"guides/CHAPTER-1/#starting-the-application","text":"When the application is started, you will see application log like this: CompileFlows:142 - Loaded create-profile CompileFlows:142 - Loaded delete-profile CompileFlows:142 - Loaded get-profile CompileFlows:144 - Event scripts deployed: 3 ... ServiceQueue:91 - PRIVATE v1.get.profile with 100 instances started as virtual threads ... RoutingEntry:582 - GET /api/profile/{profile_id} -> [http.flow.adapter], timeout=10s, tracing=true, flow=get-profile ... AppStarter:378 - Modules loaded in 663 ms AppStarter:365 - Reactive HTTP server running on port-8100 Note that the above log is trimmed for presentation purpose. It shows that the 3 flow configuration files are compiled as objects for performance reason. The user functions are loaded into the event system and the REST endpoints are rendered from the \"rest.yaml\" file.","title":"Starting the application"},{"location":"guides/CHAPTER-1/#testing-the-application","text":"You can create a test user profile with this python code. Alternatively, you can also use PostMan or other means to do this. >>> import requests, json >>> d = { 'id': 12345, 'name': 'Hello World', 'address': '100 World Blvd', 'telephone': '123-456-7890' } >>> h = { 'content-type': 'application/json', 'accept': 'application/json' } >>> r = requests.post('http://127.0.0.1:8100/api/profile', data=json.dumps(d), headers=h) >>> print(r.status_code) 201 >>> print(r.text) { \"profile\": { \"address\": \"***\", \"name\": \"Hello World\", \"telephone\": \"***\", \"id\": 12345 }, \"type\": \"CREATE\", \"secure\": [ \"address\", \"telephone\" ] } To verify that the user profile has been created, you can point your browser to http://127.0.0.1:8100/api/profile/12345 Your browser will return the following: { \"address\": \"100 World Blvd\", \"name\": \"Hello World\", \"telephone\": \"123-456-7890\", \"id\": 12345 } You have successfully tested the two REST endpoints. Tracing information in the application log may look like this: DistributedTrace:76 - trace={path=POST /api/profile, service=http.flow.adapter, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.524Z, exec_time=0.284, from=http.request, id=f6a6ae62340e43afb0a6f30445166e08} DistributedTrace:76 - trace={path=POST /api/profile, service=event.script.manager, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.525Z, exec_time=0.57, from=http.flow.adapter, id=f6a6ae62340e43afb0a6f30445166e08} DistributedTrace:76 - trace={path=POST /api/profile, service=v1.create.profile, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.526Z, exec_time=0.342, from=task.executor, id=f6a6ae62340e43afb0a6f30445166e08} DistributedTrace:76 - trace={path=POST /api/profile, service=async.http.response, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.528Z, exec_time=0.294, from=task.executor, id=f6a6ae62340e43afb0a6f30445166e08} DistributedTrace:76 - trace={path=POST /api/profile, service=v1.encrypt.fields, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.528Z, exec_time=3.64, from=task.executor, id=f6a6ae62340e43afb0a6f30445166e08} SaveProfile:52 - Profile 12345 saved TaskExecutor:186 - Flow create-profile (f6a6ae62340e43afb0a6f30445166e08) completed in 11 ms DistributedTrace:76 - trace={path=POST /api/profile, service=v1.save.profile, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:23.533Z, exec_time=2.006, from=task.executor, id=f6a6ae62340e43afb0a6f30445166e08} DistributedTrace:76 - trace={path=GET /api/profile/12345, service=http.flow.adapter, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:52.089Z, exec_time=0.152, from=http.request, id=1a29105044e94cc3ac68aee002f6f429} DistributedTrace:76 - trace={path=GET /api/profile/12345, service=event.script.manager, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:52.090Z, exec_time=0.291, from=http.flow.adapter, id=1a29105044e94cc3ac68aee002f6f429} DistributedTrace:76 - trace={path=GET /api/profile/12345, service=v1.get.profile, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:52.091Z, exec_time=1.137, from=task.executor, id=1a29105044e94cc3ac68aee002f6f429} DistributedTrace:76 - trace={path=GET /api/profile/12345, service=v1.decrypt.fields, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:52.093Z, exec_time=1.22, from=task.executor, id=1a29105044e94cc3ac68aee002f6f429} TaskExecutor:186 - Flow get-profile (1a29105044e94cc3ac68aee002f6f429) completed in 4 ms DistributedTrace:76 - trace={path=GET /api/profile/12345, service=async.http.response, success=true, origin=202406249aea0a481d46401d8379c8896a6698a2, start=2024-06-24T22:41:52.095Z, exec_time=0.214, from=task.executor, id=1a29105044e94cc3ac68aee002f6f429}","title":"Testing the application"},{"location":"guides/CHAPTER-1/#main-module","text":"Every application has an entry point. The MainApp in the example app contains the entry point 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\"); } } Since your application is event driven, the main application does not need any additional code in the above example. However, this is a good place to put application initialization code if any. There is also a \"BeforeApplication\" annotation if you want to run some start up code before the event system is started.","title":"Main module"},{"location":"guides/CHAPTER-1/#dependency-management","text":"As a best practice, your user functions should not have any dependencies with other user functions. However, within a single user function, you may use your preferred framework or libraries. For maintainability, we do recommend to reduce library dependencies as much as you can. For example, you want to push JDBC or JPA dependency to a small number of user functions (for CRUD operation) so that the rest of the user functions do not need any DB dependencies.","title":"Dependency management"},{"location":"guides/CHAPTER-1/#component-scan","text":"Please update the following in the application.properties (or application.yml) to include packages of your own functions: web.component.scan=your.package.name You should replace \"your.package.name\" with the real package name(s) that you use in your application. Usually this is your organization software ID or \"namespace\". \"web.component.scan\" is a comma separated list of package names.","title":"Component scan"},{"location":"guides/CHAPTER-1/#deploy-your-application","text":"Composable design can be used to create microservices. You can put related functions in a bounded context with database persistence. Each composable application 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 eclipse-temurin:21.0.1_12-jdk EXPOSE 8083 WORKDIR /app COPY target/your-app-name.jar . ENTRYPOINT [\"java\",\"-jar\",\"your-app-name.jar\"] The above Dockerfile will fetch Openjdk 21 packaged in \"Ubuntu 22.04 LTS\".","title":"Deploy your application"},{"location":"guides/CHAPTER-1/#event-choreography-by-configuration","text":"The best practice for composable design is event choreography by configuration ( Event Script ) discussed above. We will examine the Event Script syntax in Chapter 4 . Generally, you do not need to use Mercury core APIs in your user functions. For composable applications that use Event Script, Mercury core APIs (Platform, PostOffice and FastRPC) are only required for writing unit tests, \"custom flow adapters\", \"legacy functional wrappers\" or \"external gateways\".","title":"Event choreography by configuration"},{"location":"guides/CHAPTER-1/#orchestration-by-code","text":"Orchestration by code is strongly discouraged because it would result in tightly coupled code . For example, just an \"Import\" statement of another function would create tight coupling of two pieces of code, even when using reactive or event-driven programming styles. However, if there is a use case that you prefer to write orchestration logic by code, you may use the Mercury core APIs to do event-driven programming. API overview will be covered in Chapter 9 . Home Chapter-2 Table of Contents Function Execution Strategy","title":"Orchestration by code"},{"location":"guides/CHAPTER-2/","text":"Function Execution Strategies Define a function In a composable application, each function is self-contained with zero dependencies with other user functions. Only flow adapter, data adapter, notification function or gateway has a single external dependency such as a network event system, a database or an external REST resource. 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 AnotherPojo handleEvent(Map headers, MyPoJo input, int instance) { // your business logic here return result; } } 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. In the above example, the input is MyPoJo and the output is AnotherPoJo. For event choreography, PoJos are treated as key-value Maps so that you can use the dot-bracket convention to map subset of a PoJo from one function to another if needed. When the input is used for a PoJo, you may also pass parameters to the user function as headers. We will discuss this in Chapter 3 \"Event Script syntax\". Non-blocking design While you can apply sequential, object oriented or reactive programming styles in your functions, you should pay attention to making your function non-blocking and fast. In a virtual thread, if you use Java Future, the \".get()\" method is synchronous but it is non-blocking behind the curtain. This is like using the \"await\" keyword in other programming language. Virtual thread execution promotes high performance and high concurrency. However, it would be suboptimal if you mix blocking code in a user function. It will block the whole event loop, resulting in substantial degradation of application performance. We therefore recommend your user function to be implemented in non-blocking or reactive styles. When you are using a reactive libaries in your function, your function can return a \"Mono\" or \"Flux\" reactive response object using the Project-Reactor Core library. This feature is supported in Java and Kotlin. For simplicity, we support only the Mono and Flux reactive response objects. If you use other types of reactive APIs, please convert them into a Mono or Flux in the return value. User function that returns a Mono object For Mono return value, a reactive user function may look like this: @PreLoad(route = \"v1.reactive.mono.function\") public class MonoUserFunction implements TypedLambdaFunction, Mono>> { private static final Logger log = LoggerFactory.getLogger(MonoUserFunction.class); private static final String EXCEPTION = \"exception\"; @Override public Mono> handleEvent(Map headers, Map input, int instance) { log.info(\"GOT {} {}\", headers, input); return Mono.create(callback -> { if (headers.containsKey(EXCEPTION)) { callback.error(new AppException(400, headers.get(EXCEPTION))); } else { callback.success(input); } }); } } When you use reactive API in your function to connect to external resources such as a database, please ensure that the reactive API is non-blocking. For example, when subscribing to a Mono publisher, you may need to add a \"Scheduler\" before your subscribe statement. It may look something like this: // obtain a virtual thread executor from the platform and apply it with the Mono's scheduler mono.subscribeOn(Schedulers.fromExecutor(Platform.getInstance().getVirtualThreadExecutor())) .subscribe(responseConsumer, errorConsumer); Without the scheduler, the subscribe statement will be blocked. Your next statement will not be reachable until the mono has completed with data or exception. User function that returns a Flux object For Flux return value, it may look like this: @PreLoad(route = \"v1.reactive.flux.function\") public class FluxUserFunction implements TypedLambdaFunction, Flux>> { private static final Logger log = LoggerFactory.getLogger(FluxUserFunction.class); private static final String EXCEPTION = \"exception\"; @Override public Flux> handleEvent(Map headers, Map input, int instance) { log.info(\"GOT {} {}\", headers, input); return Flux.create(emitter -> { if (headers.containsKey(EXCEPTION)) { emitter.error(new AppException(400, headers.get(EXCEPTION))); } else { // just generate two messages emitter.next(Map.of(\"first\", \"message\")); emitter.next(input); emitter.complete(); } }); } } Handling a Flux stream When your function returns a Flux stream object, the system will pass the stream ID of the underlying event stream to the calling function. The input arguments for the event stream ID and time-to-live parameters are provided in the event headers to your function that implements the TypedLambdaFunction or LambdaFunction. The following event headers will be provided to the calling function: x-stream-id: streamId x-ttl: ttl In the calling function, you can create a FluxConsumer to handle the incoming event stream like this: String streamId = headers.get(\"x-stream-id\"); long ttl = Utility.getInstance().str2long(headers.get(\"x-ttl\")); FluxConsumer> fc = new FluxConsumer<>(streamId, ttl); fc.consume( data -> { // handle incoming message }, e -> { // handle exception where e is a Throwable }, () -> { // handle stream completion } ); Object serialization consideration The system is designed to deliver Java primitive and HashMap through an event stream. If you pass Java primitive such as String or byte[], you do not need to do any serialization. If the objects that your function streams over a Mono or Flux channel are not supported, you must perform custom serialization. This can be achieved using the \"map\" method of the Mono or Flux class. For example, your function obtains a stream of Flux result objects from a database call. You can serialize the objects using a custom serializer like this: // \"source\" is the original Flux object Flux serializedStream = source.map(specialPoJo -> { return myCustomSerializer.toMap(specialPoJo); }); return serializedStream; Your customSerializer should implement the org.platformlambda.core.models.CustomSerializer interface. public interface CustomSerializer { public Map toMap(Object obj); public T toPoJo(Object obj, Class toValueType); } 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\"); Alternatively, you may implement authentication as a user function in the first step of an event flow. In this case, the input to the function is defined by the \"input data mapping\" rules in the event flow configuration. The advantage of this approach is that authentication is shown as part of an event flow so that the application design intention is clear. A composable application is assembled from a collection of self-contained functions that are highly reusable. 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 Virtual thread Highest throughput in terms of concurrent users. Functionally similar to a suspend function. N/A Suspend function Sequential \"non-blocking\" for RPC (request-response) that makes code easier to read and maintain Requires coding in Kotlin language Kernel threads Highest performance in terms of operations per seconds Lower number of concurrent threads due to high context switching overheads Virtual thread By default, the system will run your function as a virtual thread because this is the most efficient execution strategy. The \"Thread\" object in the standard library will operate in non-blocking mode. This means it is safe to use the Thread.sleep() method. It will release control to the event loop when your function enters into sleep, thus freeing CPU resources for other functions. We have added the \"request\" methods in the PostOffice API to support non-blocking RPC that leverages the virtual thread resource suspend/resume functionality. Future future = po.request(requestEvent, timeout); EventEnvelope result = future.get(); // alternatively, you can do: EventEnvelope result = po.request(requestEvent, timeout).get(); The PostOffice API is used when you want to do orchestration by code. If you are using Event Script, you can manage event flows using one or more configuration files. Suspend function If you prefer writing business logic in Kotlin, you may use suspend function. Similar to virtual thread, 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. Do not block your function because it may block all coroutines since they run in a single kernel thread 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. The FastRPC API is used when you want to do orchestration by code. If you are using Event Script, you can manage event flows using one or more configuration files. Kernel thread pool When you add the annotation \"KernelThreadRunner\" in a function declared as LambdaFunction or TypedLambdaFunction, the function will be executed using a \"kernel thread pool\" and Java 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 should write \"asynchronous\" code. i.e. for event-driven programming, you can use 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 virtual thread or suspend function to handle synchronous RPC calls in a non-blocking manner. Solving the puzzle of multithreading performance Before the availability of virtual thread technology, Java VM is using kernel threads for code execution. If you have a lot of users hitting your service concurrently, multiple threads are created to serve concurrent requests. When your code serving the requests make blocking call to other services, the kernel threads are busy while your user functions wait for responses. Kernel threads that are in the wait state is consuming CPU time. If the blocking calls finish very quickly, this may not be an issue. However, when the blocking calls take longer to complete, a lot of outstanding kernel threads that are waiting for responses would compete for CPU resources, resulting in higher internal friction in the JVM that makes your application running slower. This is not a productive use of computer resources. This type of performance issue caused by internal friction is very difficult to avoid. While event driven and reactive programming that uses asynchronous processing and callbacks would address this artificial bottleneck, asynchronous code is harder to implement and maintain when the application complexity increases. It would be ideal if we can write sequential code that does not block. Sequential code is much easier to write and read because it communicates the intent of the code clearly. Leveraging Java 21 virtual thread, Mercury Composable allows the developer to write code in a sequential manner. When code in your function makes an RPC call to another service using the PostOffice's \"request\" API, it returns a Java Future object but the \"Future\" object itself is running in a virtual thread. This means when your code retrieves the RPC result using the \"get\" method, your code appears \"blocked\" while waiting for the response from the target service. Although your code appears to be \"blocked\", the virtual thread is \u201csuspended\u201d. It will wake up when the response arrives. When a virtual thread is suspended, it does not consume CPU time and the memory structure for keeping the thread in suspend mode is very small. Virtual thread technology is designed to support tens of thousands, if not millions, of concurrent RPC requests in a single compute machine, container or serverless instance. Mercury Composable supports mixed thread management - virtual threads, suspend functions and kernel threads. Functions running in different types of threads are connected loosely in events. This functional isolation and encapsulation mean that you can precisely control how your application performs for each functional logic block. 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 function is self-contained with zero dependencies with other user functions. Only flow adapter, data adapter, notification function or gateway has a single external dependency such as a network event system, a database or an external REST resource. 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 AnotherPojo handleEvent(Map headers, MyPoJo input, int instance) { // your business logic here return result; } } 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. In the above example, the input is MyPoJo and the output is AnotherPoJo. For event choreography, PoJos are treated as key-value Maps so that you can use the dot-bracket convention to map subset of a PoJo from one function to another if needed. When the input is used for a PoJo, you may also pass parameters to the user function as headers. We will discuss this in Chapter 3 \"Event Script syntax\".","title":"Define a function"},{"location":"guides/CHAPTER-2/#non-blocking-design","text":"While you can apply sequential, object oriented or reactive programming styles in your functions, you should pay attention to making your function non-blocking and fast. In a virtual thread, if you use Java Future, the \".get()\" method is synchronous but it is non-blocking behind the curtain. This is like using the \"await\" keyword in other programming language. Virtual thread execution promotes high performance and high concurrency. However, it would be suboptimal if you mix blocking code in a user function. It will block the whole event loop, resulting in substantial degradation of application performance. We therefore recommend your user function to be implemented in non-blocking or reactive styles. When you are using a reactive libaries in your function, your function can return a \"Mono\" or \"Flux\" reactive response object using the Project-Reactor Core library. This feature is supported in Java and Kotlin. For simplicity, we support only the Mono and Flux reactive response objects. If you use other types of reactive APIs, please convert them into a Mono or Flux in the return value.","title":"Non-blocking design"},{"location":"guides/CHAPTER-2/#user-function-that-returns-a-mono-object","text":"For Mono return value, a reactive user function may look like this: @PreLoad(route = \"v1.reactive.mono.function\") public class MonoUserFunction implements TypedLambdaFunction, Mono>> { private static final Logger log = LoggerFactory.getLogger(MonoUserFunction.class); private static final String EXCEPTION = \"exception\"; @Override public Mono> handleEvent(Map headers, Map input, int instance) { log.info(\"GOT {} {}\", headers, input); return Mono.create(callback -> { if (headers.containsKey(EXCEPTION)) { callback.error(new AppException(400, headers.get(EXCEPTION))); } else { callback.success(input); } }); } } When you use reactive API in your function to connect to external resources such as a database, please ensure that the reactive API is non-blocking. For example, when subscribing to a Mono publisher, you may need to add a \"Scheduler\" before your subscribe statement. It may look something like this: // obtain a virtual thread executor from the platform and apply it with the Mono's scheduler mono.subscribeOn(Schedulers.fromExecutor(Platform.getInstance().getVirtualThreadExecutor())) .subscribe(responseConsumer, errorConsumer); Without the scheduler, the subscribe statement will be blocked. Your next statement will not be reachable until the mono has completed with data or exception.","title":"User function that returns a Mono object"},{"location":"guides/CHAPTER-2/#user-function-that-returns-a-flux-object","text":"For Flux return value, it may look like this: @PreLoad(route = \"v1.reactive.flux.function\") public class FluxUserFunction implements TypedLambdaFunction, Flux>> { private static final Logger log = LoggerFactory.getLogger(FluxUserFunction.class); private static final String EXCEPTION = \"exception\"; @Override public Flux> handleEvent(Map headers, Map input, int instance) { log.info(\"GOT {} {}\", headers, input); return Flux.create(emitter -> { if (headers.containsKey(EXCEPTION)) { emitter.error(new AppException(400, headers.get(EXCEPTION))); } else { // just generate two messages emitter.next(Map.of(\"first\", \"message\")); emitter.next(input); emitter.complete(); } }); } }","title":"User function that returns a Flux object"},{"location":"guides/CHAPTER-2/#handling-a-flux-stream","text":"When your function returns a Flux stream object, the system will pass the stream ID of the underlying event stream to the calling function. The input arguments for the event stream ID and time-to-live parameters are provided in the event headers to your function that implements the TypedLambdaFunction or LambdaFunction. The following event headers will be provided to the calling function: x-stream-id: streamId x-ttl: ttl In the calling function, you can create a FluxConsumer to handle the incoming event stream like this: String streamId = headers.get(\"x-stream-id\"); long ttl = Utility.getInstance().str2long(headers.get(\"x-ttl\")); FluxConsumer> fc = new FluxConsumer<>(streamId, ttl); fc.consume( data -> { // handle incoming message }, e -> { // handle exception where e is a Throwable }, () -> { // handle stream completion } );","title":"Handling a Flux stream"},{"location":"guides/CHAPTER-2/#object-serialization-consideration","text":"The system is designed to deliver Java primitive and HashMap through an event stream. If you pass Java primitive such as String or byte[], you do not need to do any serialization. If the objects that your function streams over a Mono or Flux channel are not supported, you must perform custom serialization. This can be achieved using the \"map\" method of the Mono or Flux class. For example, your function obtains a stream of Flux result objects from a database call. You can serialize the objects using a custom serializer like this: // \"source\" is the original Flux object Flux serializedStream = source.map(specialPoJo -> { return myCustomSerializer.toMap(specialPoJo); }); return serializedStream; Your customSerializer should implement the org.platformlambda.core.models.CustomSerializer interface. public interface CustomSerializer { public Map toMap(Object obj); public T toPoJo(Object obj, Class toValueType); }","title":"Object serialization consideration"},{"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\"); Alternatively, you may implement authentication as a user function in the first step of an event flow. In this case, the input to the function is defined by the \"input data mapping\" rules in the event flow configuration. The advantage of this approach is that authentication is shown as part of an event flow so that the application design intention is clear. A composable application is assembled from a collection of self-contained functions that are highly reusable.","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 Virtual thread Highest throughput in terms of concurrent users. Functionally similar to a suspend function. N/A Suspend function Sequential \"non-blocking\" for RPC (request-response) that makes code easier to read and maintain Requires coding in Kotlin language Kernel threads Highest performance in terms of operations per seconds Lower number of concurrent threads due to high context switching overheads","title":"Three strategies for function execution"},{"location":"guides/CHAPTER-2/#virtual-thread","text":"By default, the system will run your function as a virtual thread because this is the most efficient execution strategy. The \"Thread\" object in the standard library will operate in non-blocking mode. This means it is safe to use the Thread.sleep() method. It will release control to the event loop when your function enters into sleep, thus freeing CPU resources for other functions. We have added the \"request\" methods in the PostOffice API to support non-blocking RPC that leverages the virtual thread resource suspend/resume functionality. Future future = po.request(requestEvent, timeout); EventEnvelope result = future.get(); // alternatively, you can do: EventEnvelope result = po.request(requestEvent, timeout).get(); The PostOffice API is used when you want to do orchestration by code. If you are using Event Script, you can manage event flows using one or more configuration files.","title":"Virtual thread"},{"location":"guides/CHAPTER-2/#suspend-function","text":"If you prefer writing business logic in Kotlin, you may use suspend function. Similar to virtual thread, 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. Do not block your function because it may block all coroutines since they run in a single kernel thread 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. The FastRPC API is used when you want to do orchestration by code. If you are using Event Script, you can manage event flows using one or more configuration files.","title":"Suspend function"},{"location":"guides/CHAPTER-2/#kernel-thread-pool","text":"When you add the annotation \"KernelThreadRunner\" in a function declared as LambdaFunction or TypedLambdaFunction, the function will be executed using a \"kernel thread pool\" and Java 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 should write \"asynchronous\" code. i.e. for event-driven programming, you can use 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 virtual thread or suspend function to handle synchronous RPC calls in a non-blocking manner.","title":"Kernel thread pool"},{"location":"guides/CHAPTER-2/#solving-the-puzzle-of-multithreading-performance","text":"Before the availability of virtual thread technology, Java VM is using kernel threads for code execution. If you have a lot of users hitting your service concurrently, multiple threads are created to serve concurrent requests. When your code serving the requests make blocking call to other services, the kernel threads are busy while your user functions wait for responses. Kernel threads that are in the wait state is consuming CPU time. If the blocking calls finish very quickly, this may not be an issue. However, when the blocking calls take longer to complete, a lot of outstanding kernel threads that are waiting for responses would compete for CPU resources, resulting in higher internal friction in the JVM that makes your application running slower. This is not a productive use of computer resources. This type of performance issue caused by internal friction is very difficult to avoid. While event driven and reactive programming that uses asynchronous processing and callbacks would address this artificial bottleneck, asynchronous code is harder to implement and maintain when the application complexity increases. It would be ideal if we can write sequential code that does not block. Sequential code is much easier to write and read because it communicates the intent of the code clearly. Leveraging Java 21 virtual thread, Mercury Composable allows the developer to write code in a sequential manner. When code in your function makes an RPC call to another service using the PostOffice's \"request\" API, it returns a Java Future object but the \"Future\" object itself is running in a virtual thread. This means when your code retrieves the RPC result using the \"get\" method, your code appears \"blocked\" while waiting for the response from the target service. Although your code appears to be \"blocked\", the virtual thread is \u201csuspended\u201d. It will wake up when the response arrives. When a virtual thread is suspended, it does not consume CPU time and the memory structure for keeping the thread in suspend mode is very small. Virtual thread technology is designed to support tens of thousands, if not millions, of concurrent RPC requests in a single compute machine, container or serverless instance. Mercury Composable supports mixed thread management - virtual threads, suspend functions and kernel threads. Functions running in different types of threads are connected loosely in events. This functional isolation and encapsulation mean that you can precisely control how your application performs for each functional logic block. Chapter-1 Home Chapter-3 Introduction Table of Contents REST Automation","title":"Solving the puzzle of multithreading performance"},{"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. Support of multiple configuration files You can configure more than one location and the system will search and merge them sequentially. The following example tells the system to merge the rest.yaml config files in the /tmp/config folder and the project's resources folder. yaml.rest.automation=file:/tmp/config/rest.yaml, classpath:/rest.yaml Duplicated REST endpoints The system will detect duplicated REST endpoint configuation. If there is a duplicated entry, it will abort the REST endpoint rendering. Your unit tests will fail because REST endpoints are not enabled. The application log may look like this: INFO - Loading config from classpath:/rest.yaml INFO - Loading config from classpath:/event-api.yaml ERROR - REST endpoint rendering aborted due to duplicated entry 'POST /api/event' in classpath:/event-api.yaml Please correct the rest.yaml configuration files and rebuild your application again. Duplicated static content, cors and headers sections When duplicated entry is detected, the subsequent one will replace the prior one. A warning will be shown in the application log like this: WARN - Duplicated 'static-content' in classpath:/duplicated-endpoint.yaml will override a prior one WARN - Duplicated 'cors' in classpath:/duplicated-endpoint.yaml will override a prior one 'cors_1' WARN - Duplicated 'headers' in classpath:/duplicated-endpoint.yaml will override a prior one 'header_1' 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\" The 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 Script Syntax","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.","title":"Turn on the REST automation engine"},{"location":"guides/CHAPTER-3/#support-of-multiple-configuration-files","text":"You can configure more than one location and the system will search and merge them sequentially. The following example tells the system to merge the rest.yaml config files in the /tmp/config folder and the project's resources folder. yaml.rest.automation=file:/tmp/config/rest.yaml, classpath:/rest.yaml","title":"Support of multiple configuration files"},{"location":"guides/CHAPTER-3/#duplicated-rest-endpoints","text":"The system will detect duplicated REST endpoint configuation. If there is a duplicated entry, it will abort the REST endpoint rendering. Your unit tests will fail because REST endpoints are not enabled. The application log may look like this: INFO - Loading config from classpath:/rest.yaml INFO - Loading config from classpath:/event-api.yaml ERROR - REST endpoint rendering aborted due to duplicated entry 'POST /api/event' in classpath:/event-api.yaml Please correct the rest.yaml configuration files and rebuild your application again.","title":"Duplicated REST endpoints"},{"location":"guides/CHAPTER-3/#duplicated-static-content-cors-and-headers-sections","text":"When duplicated entry is detected, the subsequent one will replace the prior one. A warning will be shown in the application log like this: WARN - Duplicated 'static-content' in classpath:/duplicated-endpoint.yaml will override a prior one WARN - Duplicated 'cors' in classpath:/duplicated-endpoint.yaml will override a prior one 'cors_1' WARN - Duplicated 'headers' in classpath:/duplicated-endpoint.yaml will override a prior one 'header_1'","title":"Duplicated static content, cors and headers sections"},{"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\" The 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 Script Syntax","title":"Static content"},{"location":"guides/CHAPTER-4/","text":"Event Script Syntax Event Script uses YAML to represent an end-to-end transaction flow. A transaction is a business use case, and the flow can be an API service, a batch job or a real-time transaction. Flow list This configuration file sits in the project \"resources\" project and contains a list of filenames. The default flow list is \"flows.yaml\" under the \"resources\" folder. It may look like this. flows: - 'get-profile.yml' - 'create-profile.yml' - 'delete-profile.yml' location: 'classpath:/flows/' The \"location\" tag is optional. If present, you can tell the system to load the flow config files from another folder location. Multiple flow lists You can provide more than one flow list to your application and it can become very handy under different situations. For instance, to achieve better modularity in complex application, flows can be grouped to multiple categories based on development team's choice and these flows can be managed in multiple flow lists. Another great place to use multiple flow list is to include external libraries which contain pre-defined flow lists. The following example demonstrates that an application loads a list of flows defined in \"flows.yaml\" and additional flows defined in \"more-flows.yaml\" file of a composable library. yaml.flow.automation=classpath:/flows.yaml, classpath:/more-flows.yaml Writing new REST endpoint and function You can use the \"flow-demo\" subproject as a template to write your own composable application. For each filename in the flows.yml, you should create a corresponding configuration file under the \"resources/flows\" folder. Let's write a new flow called \"greetings\". You can copy-n-paste the following into a file called \"greetings.yml\" under the \"resources/flows\" folder. flow: id: 'greetings' description: 'Simplest flow' ttl: 10s first.task: 'greeting.demo' tasks: - input: - 'input.path_parameter.user -> user' process: 'greeting.demo' output: - 'text(application/json) -> output.header.content-type' - 'result -> output.body' description: 'Hello World' execution: end In the application.properties, you can specify the following parameter: yaml.flow.automation=classpath:/flows.yaml and update the \"flows.yaml\" file in the resources folder as follows: flows: - 'get-profile.yml' - 'create-profile.yml' - 'delete-profile.yml' - 'greetings.yml' Then, you can add a new REST endpoint in the \"rest.yaml\" configuration file like this. - service: \"http.flow.adapter\" methods: ['GET'] url: \"/api/greetings/{user}\" flow: 'greetings' timeout: 10s cors: cors_1 headers: header_1 The above REST endpoint takes the path parameter \"user\". The task executor will map the path parameter to the input arguments (headers and body) in your function. Now you can write your new function with the named route \"greeting.demo\". Please copy-n-paste the following into a Java class called \"Greetings\" and save in the package under \"my.organization.tasks\" in the source project. Note: \"my.organization\" package name is an example. Please replace it with your organization package path. @PreLoad(route=\"greeting.demo\", instances=10, isPrivate = false) public class Greetings implements TypedLambdaFunction, Map> { private static final String USER = \"user\"; @Override public Map handleEvent(Map headers, Map input, int instance) { if (input.containsKey(USER)) { String user = input.get(USER).toString(); Map result = new HashMap<>(); result.put(USER, user); result.put(\"message\", \"Welcome\"); result.put(\"time\", new Date()); return result; } else { throw new IllegalArgumentException(\"Missing path parameter 'user'\"); } } } For the flow-engine to find your new function, please update the key-value for \"web.component.scan\" in application.properties: web.component.scan=my.organization To test your new REST endpoint, flow configuration and function, please point your browser to http://127.0.0.1:8100/api/greetings/my_name You can replace \"my_name\" with your first name to see the response to the browser. Flow configuration syntax In your \"greetings.yml\" file above, you find the following key-values: flow.id - Each flow must have a unique flow ID. The flow ID is usually originated from a user facing endpoint through an event adapter. For example, you may write an adapter to listen to a cloud event in a serverless deployment. In The most common one is the HTTP adapter. The flow ID is originated from the \"rest.yaml\". The flow-engine will find the corresponding flow configuration and create a new flow instance to process the user request. flow.description - this describes the purpose of the flow flow.ttl - \"Time to live (TTL)\" timer for each flow. You can define the maximum time for a flow to finish processing. All events are delivered asynchronously and there is no timeout value for each event. The TTL defines the time budget for a complete end-to-end flow. Upon expiry, an unfinished flow will be aborted. first.task - this points to the route name of a function (aka \"task\") to which the flow engine will deliver the incoming event. The configuration file contains a list of task entries where each task is defined by \"input\", \"process\", \"output\" and \"execution\" type. In the above example, the execution type is \"end\", meaning that it is the end of a transaction and its result set will be delivered to the user. Underlying Event System The Event Script system uses platform-core as the event system where it encapsulates Java Virtual Threads, Eclipse Vertx, Kotlin coroutine and suspend function. The integration points are intentionally minimalist. For most use cases, the user application does not need to make any API calls to the underlying event system. REST automation and HTTP adapter The most common transaction entry point is a REST endpoint. The event flow may look like this: Request -> \"http.request\" -> \"task.executor\" -> user defined tasks -> \"async.http.response\" -> Response REST automation is part of the Mercury platform-core library. It contains a non-blocking HTTP server that converts HTTP requests and responses into events. It routes an HTTP request event to the HTTP adapter if the \"flow\" tag is provided. In the following example, the REST endpoint definition is declared in a \"rest.yaml\" configuration. It will route the URI \"/api/decision\" to the HTTP adapter that exposes its service route name as \"http.flow.adapter\". rest: - service: \"http.flow.adapter\" methods: ['GET'] url: \"/api/decision?decision=_\" flow: 'decision-test' timeout: 10s cors: cors_1 headers: header_1 tracing: true The \"cors\" and \"headers\" tags are optional. When specified, the REST endpoint will insert CORS headers and HTTP request headers accordingly. For rest.yaml syntax, please refer to https://accenture.github.io/mercury-composable/guides/CHAPTER-3 The HTTP adapter maps the HTTP request dataset and the flow ID into a standard event envelope for delivery to the flow engine. The HTTP request dataset, addressable with the \"input.\" namespace, contains the following: Key Values method HTTP method uri URI path header HTTP headers cookie HTTP cookies path_parameter Path parameters if any query HTTP query parameters if any body HTTP request body if any stream input stream route ID if any ip remote IP address filename filename if request is a multipart file upload session authenticated session key-values if any For easy matching, keys of headers, cookies, query and path parameters are case-insensitive. Regular API uses JSON and XML and they will be converted to a hashmap in the event's body. For special use cases like file upload/download, your application logic may invoke a streaming API to retrieve the binary payload. Please refer to the following sections for details. https://accenture.github.io/mercury-composable/guides/APPENDIX-III/#send-http-request-body-as-a-stream https://accenture.github.io/mercury-composable/guides/APPENDIX-III/#read-http-response-body-stream Task and its corresponding function Each task in a flow must have a corresponding function. You can assign a task name to the function using the Preload annotation like this. @PreLoad(route=\"greeting.demo\", instances=10) public class Greetings implements TypedLambdaFunction, Map> { @Override public Map handleEvent(Map headers, Map input, int instance) { // business logic here return someOutput; } } The \"route\" in the Preload annotation is the task name. The \"instances\" define the maximum number of \"workers\" that the function can handle concurrently. The system is designed to be reactive and the function does not consume memory and CPU resources until an event arrives. You may also define concurrency using environment variable. You can replace the \"instances\" with envInstances using standard environment variable syntax like ${SOME_ENV_VARIABLE:default_value} . Unique task naming Composable functions are designed to be reusable. By changing some input data mapping to feed different parameters and payload, your function can behave differently. Therefore, it is quite common to use the same function (\"process\") more than once in a single event flow. When a task is not named, the \"process\" tag is used to name the task. Since each task must have a unique name for event routing, we cannot use the same \"process\" name more than once in an event flow. To handle this use case, you can create unique names for the same function (i.e. \"process\") like this: flow: id: 'greetings' description: 'Simplest flow' ttl: 10s first.task: 'my.first.task' tasks: - name: 'my.first.task' input: - 'input.path_parameter.user -> user' process: 'greeting.demo' output: - 'text(application/json) -> output.header.content-type' - 'result -> output.body' description: 'Hello World' execution: sequential next: - 'another.task' The above event flow configuration uses \"my.first.task\" as a named route for \"greeting.demo\" by adding the \"name\" tag to the composable function. For configuration simplicity, the \"name\" tag is optional. If not provided, the process name is assumed to be the unique \"task\" name. Important: The Event Manager performs event choreography using the unique task name. Therefore, when the \"process\" name for the function is not unique, you must create unique task \"names\" for the same function to ensure correct routing. Assigning multiple route names to a single function The built-in distributed tracing system tracks the actual composable functions using the \"process\" name and not the task names. When there is a need to track the task names in distributed trace, you can tell the system to create additional instances of the same function with different route names. You can use a comma separated list as the route name like this: @PreLoad(route=\"greeting.case.1, greeting.case.2\", instances=10) public class Greetings implements TypedLambdaFunction, Map> { @Override public Map handleEvent(Map headers, Map input, int instance) { // business logic here return someResult; } } Note: The \"unique task naming\" method is more memory efficient than creating additional route names Preload overrides Once a composable function is published as a reusable library in the artifactory, its route name and number of instances are fixed using the \"PreLoad\" annotation in the function class. Without refactoring your libary, you can override its route name and instances using a preload override file like this: preload: - original: 'greeting.demo' routes: - 'greeting.case.1' - 'greeting.case.2' # the \"instances\" tag is optional instances: 20 - original: 'v1.another.reusable.function' keep-original: true routes: - 'v1.reusable.1' - 'v1.reusable.2' In the above example, the function associated with \"greeting.demo\" will be preloaded as \"greeting.case.1\" and \"greeting.case.2\". The number of maximum concurrent instances is also changed from 10 to 20. In the second example, \"v1.another.reusable.function\" is updated as \"v1.reusable.1\" and \"v1.reusable.2\" and the number of concurrent instances is not changed. The original route \"v1.another.reusable.function\" is preserved when the \"keep-original\" parameter is set to true. Assuming the above file is \"preload-override.yaml\" in the \"resources\" folder of the application source code project, you should add the following parameter in application.properties to activate this preload override feature. yaml.preload.override=classpath:/preload-override.yaml Multiple preload override config files When you publish a composable function as a library, you may want to ensure the route names of the functions are merged properly. In this case, you can bundle a library specific preload override config file. For example, your library contains a \"preload-kafka.yaml\" to override some route names, you can add it to the yaml.preload.override parameter like this: yaml.preload.override=classpath:/preload-override.yaml, classpath:/preload-kafka.yaml The system will then merge the two preload override config files. The concurrency value of a function is overwritten using the \"instances\" parameter in the first preload override file. Subsequent override of the \"instances\" parameter is ignored. i.e. the first preload override file will take precedence. Hierarchy of flows Inside a flow, you can run one or more sub-flows. To do this, you can use the flow protocol identifier ( flow:// ) to indicate that the task is a flow. For example, when running the following task, \"flow://my-sub-flow\" will be executed like a regular task. tasks: - input: - 'input.path_parameter.user -> header.user' - 'input.body -> body' process: 'flow://my-sub-flow' output: - 'result -> model.pojo' description: 'Execute a sub-flow' execution: sequential next: - 'my.next.function' If the sub-flow is not available, the system will throw an error stating that it is not found. Hierarchy of flows would reduce the complexity of a single flow configuration file. The \"time-to-live (TTL)\" value of the parent flow should be set to a value that covers the complete flow including the time used in the sub-flows. For simplicity, the input data mapping for a sub-flow should contain only the \"header\" and \"body\" arguments. Tasks and data mapping All tasks for a flow are defined in the \"tasks\" section. Input/Output data mapping A function is self-contained. This modularity reduces application complexity because the developer only needs interface contract details for a specific function. To handle this level of modularity, the system provides configurable input/output data mapping. Namespaces for I/O data mapping Type Keyword and/or namespace LHS / RHS Mappings Flow input dataset input. left input Flow output dataset output. right output Function input body no namespace required right input Function input or output headers header or header. right I/O Function output result set result. left output Function output status code status left output Decision value decision right output State machine dataset model. left/right I/O External state machine key-value ext: right I/O Note that external state machine namespace uses \":\" to indicate that the key-value is external. Constants for input data mapping Type Keyword for the left-hand-side argument String text(example_value) Integer int(number) Long long(number) Float float(number) Double double(number) Boolean boolean(true or false) Map map(k1=v1, k2=v2) map(base.config.parameter) File file(text:file_path) file(binary:file_path) Classpath classpath(text:file_path) classpath(binary:file_path) For input data mapping, the \"file\" constant type is used to load some file content as an argument of a user function. You can tell the system to render the file as \"text\" or \"binary\". Similarly, the \"classpath\" constant type refers to static file in the application source code's \"resources\" folder. The \"map\" constant type is used for two purposes: 1. Map of key-values The following example illustrates creation of a map of key-values. In the first entry, a map of 2 key-values is set as the input argument \"myMap\" of a user function. In the second entry, the map's values are retrieved from the key \"some.key\" in base configuration and the environment variable \"ENV_VAR_ONE\". 'map(k1=v1, k2=v2) -> myMap' 'map(k1=${some.key}, k2=${ENV_VAR_ONE}) -> myMap' Note that the comma character is used as a separator for each key-value pair. If the value contains a comma, the system cannot parse the key-values correctly. In this case, please use the 2nd method below. 2. Mapping values from application.yml The following input data mapping sets the value of \"my.key\" from the application.yml base configuration file to the input argument \"myKey\" of a user function. 'map(my.key) -> myKey' Since the system uses both application.properties and application.yml as base configuration files, you can use either configuration files depending on the data type of the value. For application.properties, \"map(my.key)\" is the same as \"text(${my.key})\". For application.yml, \"map(my.key)\" would set a primitive value (text, integer, float, boolean), a hash map of key-values or an array of values. Special content type for output data mapping Type Keyword for the right-hand-side argument File file(file_path) For output data mapping, the \"file\" content type is used to save some data from the output of a user function to a file in the local file system. Decison value The \"decision\" keyword applies to \"right hand side\" of output data mapping statement in a decision task only (See \"Decision\" in the task section). Each flow has its own input and output Each function has its input headers, input body and output result set. Optionally, a function can return an EventEnvelope object to hold its result set in the \"body\", a \"status\" code and one or more header key-values. Since each function is stateless, a state machine (with namespace model. ) is available as a temporary memory store for transaction states that can be passed from one task to another. All variables are addressable using the standard dot-bracket convention. For example, \"hello.world\" will retrieve the value 100 from this data structure: { \"hello\": { \"world\": 100 } } and \"numbers[1]\" will retrieve the value 200 below: { \"numbers\": [100, 200] } The assignment is done using the -> syntax. In the following example, the HTTP input query parameter 'amount' is passed as input body argument 'amount' to the task 'simple.decision'. The result (function \"return value\") from the task will be mapped to the special \"decision\" variable that the flow engine will evaluate. This assumes the result is a boolean or numeric value. The \"decision\" value is also saved to the state machine ( model ) for subsequent tasks to evaluate. - input: - 'input.query.amount -> amount' process: 'simple.decision' output: - 'result -> decision' - 'result -> model.decision' Metadata for each flow instance For each flow instance, the state machine in the \"model\" namespace provides the following metadata that you can use in the input/output data mapping. For example, you can set this for an exception handler to log additional information. Type Keyword Comment Flow ID model.flow The ID of the event flow config Trace ID model.trace Optional traceId when tracing is turned on Correlation ID model.cid Correlation ID of the inbound request Special handling for header When function input keyword header is specified in the \"right hand side\" of an input data mapping statement, it refers to the input event envelope's headers. Therefore, it assumes the \"left hand side\" to resolve into a Map object of key-values. Otherwise, it will reject the input data mapping statement with an error like this: Invalid input mapping 'text(ok) -> header', expect: Map, Actual: String When function input namespace header. is used, the system will map the value resolved from the \"left hand side\" statement into the specific header. For example, the input data mapping statement text(ok) -> header.demo will set \"demo=ok\" into input event envelope's headers. When function output keyword header is specified in the \"left hand side\" of an output data mapping statement, it will resolve as a Map from the function output event envelope's headers. Similarly, when function output namespace header. is used, the system will resolve the value from a specific key of the function output event envelope's headers. Function input and output To support flexible input data mapping, the input to a function must be either Map or PoJo . The output (i.e. result set) of a function can be Map, PoJo or Java primitive. Your function can implement the TypedLambdaFunction interface to configure input and output. Since a data structure is passed to your function's input argument as key-values, you may create a PoJo class to deserialize the data structure. To tell the system that your function is expecting input as a PoJo, you can use the special notation * in the right hand side. For example, the following entry tells the system to set the value in \"model.dataset\" as a PoJo input. - input: - 'model.dataset -> *' If the value from the left hand side is not a map, the system will ignore the input mapping command and print out an error message in the application log. Setting function input headers When function input body is used to hold a PoJo, we may use function input headers to pass other arguments to the function without changing the data structure of a user defined PoJo. In the following example, the HTTP query parameter \"userid\" will be mappped to the function input header key \"user\" and the HTTP request body will be mapped to the function input body. - input: - 'input.query.userid -> header.user' - 'input.body -> *' process: 'my.user.function' output: - 'text(application/json) -> output.header.content-type' - 'result -> output.body' Task types Decision task A decision task makes decision to select the next task to execute. It has the tag execution=decision . In the output data mapping section, it must map the corresponding result set or its key-value to the decision object. The \"next\" tag contains a list of tasks to be selected based on the decision value. If decision value is boolean, a true value will select the first task. Otherwise, the second task will be selected. If decision value is an integer, the number should start from 1 where the corresponding \"next\" task will be selected. tasks: - input: - 'input.query.decision -> decision' process: 'simple.decision' output: - 'result -> model.decision' - 'result -> decision' description: 'Simple decision test' execution: decision next: - 'decision.case.one' - 'decision.case.two' Response task A response task will provide result set as a flow output or \"response\". A response task allows the flow to respond to the user or caller immediately and then move on to the next task asynchronously. For example, telling the user that it has accepted a request and then moving on to process the request that may take longer time to run. A response task has the tag execution=response and a \"next\" task. tasks: - input: - 'input.path_parameter.user -> user' - 'input.query.seq -> sequence' process: 'sequential.one' output: - 'result -> model.pojo' - 'result -> output.body' description: 'Pass a pojo to another task' execution: response next: - 'sequential.two' End task An end task indicates that it is the last task of the transaction processing in a flow. If the flow has not executed a response task, the end task will generate the response. Response is defined by output data mapping. This task has the tag execution=end . For example, the greeting task in the unit tests is an end task. - input: - 'input.path_parameter.user -> user' process: 'greeting.demo' output: - 'text(application/json) -> output.header.content-type' - 'result -> output.body' description: 'Hello World' execution: end Sequential task Upon completion of a sequential task, the next task will be executed. This task has the tag execution=sequential . In the following example, sequential.two will be executed after sequential.one . tasks: - input: - 'input.path_parameter.user -> user' - 'input.query.seq -> sequence' process: 'sequential.one' output: - 'result -> model.pojo' description: 'Pass a pojo to another task' execution: sequential next: - 'sequential.two' Parallel task Upon completion of a parallel task, all tasks in the \"next\" task list will be executed in parallel. This task has the tag execution=parallel . In this example, parallel.one and parallel.two will run after begin.parallel.test tasks: - input: - 'int(2) -> count' process: 'begin.parallel.test' output: [] description: 'Setup counter for two parallel tasks' execution: parallel next: - 'parallel.one' - 'parallel.two' Fork-n-join task Fork-n-join is a parallel processing pattern. A \"fork\" task will execute multiple \"next\" tasks in parallel and then consolidate the result sets before running the \"join\" task. This task has the tag execution=fork . It must have a list of \"next\" tasks and a \"join\" task. It may look like this: tasks: - input: - 'input.path_parameter.user -> user' - 'input.query.seq -> sequence' process: 'sequential.one' output: - 'result -> model.pojo' description: 'Pass a pojo to another task' execution: fork next: - 'echo.one' - 'echo.two' join: 'join.task' Sink task A sink task is a task without any next tasks. Sink tasks are used by fork-n-join and pipeline tasks as reusable modules. This task has the tag execution=sink . - input: - 'text(hello-world-two) -> key2' process: 'echo.two' output: - 'result.key2 -> model.key2' description: 'Hello world' execution: sink Pipeline feature Pipeline is an advanced feature of Event Script. Pipeline task A pipeline is a list of tasks that will be executed orderly within the current task. When the pipeline is done, the system will execute the \"next\" task. This task has the tag execution=pipeline . tasks: - input: - 'input.path_parameter.user -> user' - 'input.query.seq -> sequence' process: 'sequential.one' output: - 'result -> model.pojo' description: 'Pass a pojo to another task' execution: pipeline pipeline: - 'echo.one' - 'echo.two' next: - 'echo.three' Some special uses of pipelines include \"for/while-loop\" and \"continue/break\" features. Simple for-loop In the following example, the loop.statement contains a for-loop that uses a variable in the state machine to evaluate the loop. In this example, the pipeline will be executed three times before passing control to the \"next\" task. tasks: - input: - 'input.path_parameter.user -> user' - 'input.query.seq -> sequence' process: 'sequential.one' output: - 'result -> model.pojo' description: 'Pass a pojo to another task' execution: pipeline loop: statement: 'for (model.n = 0; model.n < 3; model.n++)' pipeline: - 'echo.one' - 'echo.two' - 'echo.three' next: - 'echo.four' Simple while loop The loop.statement may use a \"while loop\" syntax like this: loop: statement: 'while (model.running)' To exit the above while loop, one of the functions in the pipeline should return a boolean \"false\" value with output \"data mapping\" to the model.running variable. For loop with break/continue decision In the following example, the system will evaluate if the model.quit variable is true. If yes, the break or continue condition will be executed. The state variable is obtained after output data mapping and any task in the pipeline can set a key-value for mapping into the state variable. tasks: - input: - 'input.path_parameter.user -> user' - 'input.query.seq -> sequence' process: 'sequential.one' output: - 'result -> model.pojo' description: 'Pass a pojo to another task' execution: pipeline loop: statement: 'for (model.n = 0; model.n < 3; model.n++)' condition: - 'if (model.quit) break' pipeline: - 'echo.one' - 'echo.two' - 'echo.three' next: - 'echo.four' Handling exception You can define exception handler at the top level or at the task level. Exception is said to occur when a user function throws exception or returns an EventEnvelope object with a status code equals to or larger than 400. The event status uses the same numbering scheme as HTTP exception status code. Therefore, status code less than 400 is not considered an exception. Top-level exception handler Top-level exception handler is a \"catch-all\" handler. You can define it like this: flow: id: 'greetings' description: 'Simplest flow of one task' ttl: 10s exception: 'v1.my.exception.handler' In this example, the v1.my.exception.handler should point to a corresponding exception handler that you provide. The following input arguments will be delivered to your function when exception happens. Key Description status Exception status code message Error message stack Stack trace in a text string The exception handler function can be an \"end\" task to abort the transaction or a decision task to take care of the exception. For example, the exception handler can be a \"circuit-breaker\" to retry a request. Task-level exception handler You can attach an exception handler to a task. One typical use is the \"circuit breaker\" pattern. In the following example, the user function \"breakable.function\" may throw an exception for some error condition. The exception will be caught by the \"v1.circuit.breaker\" function. - input: - 'input.path_parameter.accept -> accept' - 'model.attempt -> attempt' process: 'breakable.function' output: - 'int(0) -> model.attempt' - 'text(application/json) -> output.header.content-type' - 'result -> output.body' description: 'This demo function will break until the \"accept\" number is reached' execution: end exception: 'v1.circuit.breaker' The configuration for the circuit breaker function may look like this: - input: - 'model.attempt -> attempt' - 'int(2) -> max_attempts' - 'error.code -> status' - 'error.message -> message' - 'error.stack -> stack' process: 'v1.circuit.breaker' output: - 'result.attempt -> model.attempt' - 'result.decision -> decision' - 'result.status -> model.status' - 'result.message -> model.message' description: 'Just a demo circuit breaker' execution: decision next: - 'breakable.function' - 'abort.request' An exception handler will be provided with the \"error\" object that contains error code, error message and an exception stack trace. The exception handler can inspect the error object to make decision of the next step. For circuit breaker, we can keep the number of retry attempts in the state machine under \"model.attempt\" or any key name that you prefer. In the above example, it sets an integer constant of 2 for the maximum attempts. The circuit breaker can then evaluate if the number of attempts is less than the maximum attempts. If yes, it will return a decision of \"true\" value to tell the system to route to the \"breakable.function\" again. Otherwise, it will return a decision of \"false\" value to abort the request. A more sophisticated circuit breaker may be configured with \"alternative execution paths\" depending on the error status and stack trace. In this case, the decision value can be a number from 1 to n that corresponds to the \"next\" task list. Exception handlers may be used in both queries and transactions. For a complex transaction, the exception handler may implement some data rollback logic or recovery mechanism. Best practice When a task-level exception handler throws exception, it will be caught by the top-level exception handler, if any. A top-level exception handler should not throw exception. Otherwise it may go into an exception loop. Therefore, we recommend that an exception handler should return regular result set in a PoJo or a Map object. An example of task-level exception handler is shown in the \"HelloException.class\" in the unit test section of the event script engine where it set the status code in the result set so that the system can map the status code from the result set to the next task or to the HTTP output status code. Advanced features Simple type matching and conversion Event script's state machine supports simple type matching and conversion. This \"impedance matching\" feature allows us to accommodate minor interface contract changes without refactoring business logic of a user function. This is supported in both the left-hand-side and right-hand-side of both input and output data mappings. For the left-hand-side, the state machine's model value is matched or converted to the target data type before setting the value of the right-hand-side. The state machine values are unchanged. For the right-hand-side, the matched or converted value is applied to the state machine's model value. The syntax is model.somekey:type where \"type\" is one of the following: Type Match value as Example text text string model.someKey:text binary byte array model.someKey:binary int integer or -1 if not numeric model.someKey:int long long or -1 if not numeric model.someKey:long float float or -1 if not numeric model.someKey:float double double or -1 if not numeric model.someKey:double boolean true or false model.someKey:boolean boolean(value) true if value matches model.someKey:boolean(positive) boolean(value=true) true if value matches model.someKey:boolean(positive=true) boolean(value=false) false if value matches model.someKey:boolean(negative=false) and(model.key) boolean AND of 2 model keys model.someKey:and(model.another) or(model.key) boolean OR of 2 model keys model.someKey:or(model.another) substring(start, end) extract a substring model.someKey:substring(0, 5) substring(start) extract a substring model.someKey:substring(5) b64 byte-array to Base64 text model.someKey:b64 b64 Base64 text to byte-array model.someKey:b64 For boolean with value matching, the value can be null. This allows your function to test if the key-value in the left-hand-side is a null value. For Base64 type matching, if the key-value is a text string, the system will assume it is a Base64 text string and convert it to a byte-array. If the key-value is a byte-array, the system will encode it into a Base64 text string. An interesting use case of type matching is a simple decision task using the built-in no-op function. For example, when a control file for the application is not available, your application will switch to run in dev mode. A sample task may look like this: first.task: 'no.op' tasks: - input: - 'file(binary:/tmp/interesting-config-file) -> model.is-local:boolean(null=true)' process: 'no.op' output: - 'model.is-local -> decision' execution: decision next: - 'start.in.dev.mode' - 'start.in.cloud' External state machine The in-memory state machine is created for each query or transaction flow and it is temporal. For complex transactions or long running work flows, you would typically want to externalize some transaction states to a persistent store such as a distributed cache system or a high performance key-value data store. In these use cases, you can implement an external state machine function and configure it in a flow. Below is an example from a unit test. When you externalize a key-value to an external state machine, you must configure the route name (aka level-3 functional topic) of the external state machine. Note that when passing a null value to a key of an external state machine means \"removal\". external.state.machine: 'v1.ext.state.machine' tasks: - input: # A function can call an external state machine using input or output mapping. # In this example, it calls external state machine from input data mapping. - 'input.path_parameter.user -> ext:/${app.id}/user' - 'input.body -> model.body' # demonstrate saving constant to state machine and remove it using model.none - 'text(world) -> ext:hello' - 'model.none -> ext:hello' process: 'no.op' output: - 'text(application/json) -> output.header.content-type' # It calls external state machine again from output data mapping - 'input.body -> ext:/${app.id}/body' - 'input.body -> output.body' - 'text(message) -> ext:test' - 'model.none -> ext:test' description: 'Hello World' execution: end The \"external.state.machine\" parameter is optional. When present, the system will send a key-value from the current flow instance's state machine to the function implementing the external state machine. The system uses the \"ext:\" namespace to externalize a state machine's key-value. Note that the delivery of key-values to the external state machine is asynchronous. Therefore, please assume eventual consistency. You should implement a user function as the external state machine. The input interface contract to the external state machine for saving a key-value is: header.type = 'put' header.key = key body = value Your function should save the input key-value to a persistent store. In another flow that requires the key-value, you can add an initial task to retrieve from the persistent store and do \"output data mapping\" to save to the in-memory state machine so that your transaction flow can use the persisted key-values to continue processing. In the unit tests of the event-script-engine subproject, these two flows work together: externalize-put-key-value externalize-get-key-value IMPORTANT: Events to an external state machine are delivered asynchronously. If you want to guarantee message sequencing, please do not set the \"instances\" parameter in the PreLoad annotation. To illustrate a minimalist implementation, below is an example of an external state machine in the event-script-engine's unit test section. @PreLoad(route = \"v1.ext.state.machine\") public class ExternalStateMachine implements LambdaFunction { private static final Logger log = LoggerFactory.getLogger(ExternalStateMachine.class); private static final ManagedCache store = ManagedCache.createCache(\"state.machine\", 5000); private static final String TYPE = \"type\"; private static final String PUT = \"put\"; private static final String GET = \"get\"; private static final String REMOVE = \"remove\"; private static final String KEY = \"key\"; @Override public Object handleEvent(Map headers, Object input, int instance) { if (!headers.containsKey(KEY)) { throw new IllegalArgumentException(\"Missing key in headers\"); } String type = headers.get(TYPE); String key = headers.get(KEY); if (PUT.equals(type) && input != null) { log.info(\"Saving {} to store\", key); store.put(key, input); return true; } if (GET.equals(type)) { Object v = store.get(key); if (v != null) { log.info(\"Retrieve {} from store\", key); return v; } else { return null; } } if (REMOVE.equals(type)) { if (store.exists(key)) { store.remove(key); log.info(\"Removed {} from store\", key); return true; } else { return false; } } return false; } } Future task scheduling You may add a \u201cdelay\u201d tag in a task so that it will be executed later. This feature is usually used for unit tests or \"future task scheduling\". Since the system is event-driven and non-blocking, the delay is simulated by event scheduling. It does not block the processing flow. Type Value Example Fixed delay Milliseconds delay=1000 Variable delay State machine variable delay=model.delay When delay is set to a state variable that its value is not configured by a prior data mapping, the delay command will be ignored. An example task that has an artificial delay of 2 seconds: tasks: - input: - 'input.path_parameter.user -> user' - 'input.query.ex -> exception' - 'text(hello world) -> greeting' process: 'greeting.test' output: - 'text(application/json) -> output.header.content-type' - 'result -> output.body' description: 'Hello World' execution: end delay: 2000 Chapter-3 Home Chapter-5 REST Automation Table of Contents Build, Test and Deploy","title":"Chapter-4"},{"location":"guides/CHAPTER-4/#event-script-syntax","text":"Event Script uses YAML to represent an end-to-end transaction flow. A transaction is a business use case, and the flow can be an API service, a batch job or a real-time transaction.","title":"Event Script Syntax"},{"location":"guides/CHAPTER-4/#flow-list","text":"This configuration file sits in the project \"resources\" project and contains a list of filenames. The default flow list is \"flows.yaml\" under the \"resources\" folder. It may look like this. flows: - 'get-profile.yml' - 'create-profile.yml' - 'delete-profile.yml' location: 'classpath:/flows/' The \"location\" tag is optional. If present, you can tell the system to load the flow config files from another folder location.","title":"Flow list"},{"location":"guides/CHAPTER-4/#multiple-flow-lists","text":"You can provide more than one flow list to your application and it can become very handy under different situations. For instance, to achieve better modularity in complex application, flows can be grouped to multiple categories based on development team's choice and these flows can be managed in multiple flow lists. Another great place to use multiple flow list is to include external libraries which contain pre-defined flow lists. The following example demonstrates that an application loads a list of flows defined in \"flows.yaml\" and additional flows defined in \"more-flows.yaml\" file of a composable library. yaml.flow.automation=classpath:/flows.yaml, classpath:/more-flows.yaml","title":"Multiple flow lists"},{"location":"guides/CHAPTER-4/#writing-new-rest-endpoint-and-function","text":"You can use the \"flow-demo\" subproject as a template to write your own composable application. For each filename in the flows.yml, you should create a corresponding configuration file under the \"resources/flows\" folder. Let's write a new flow called \"greetings\". You can copy-n-paste the following into a file called \"greetings.yml\" under the \"resources/flows\" folder. flow: id: 'greetings' description: 'Simplest flow' ttl: 10s first.task: 'greeting.demo' tasks: - input: - 'input.path_parameter.user -> user' process: 'greeting.demo' output: - 'text(application/json) -> output.header.content-type' - 'result -> output.body' description: 'Hello World' execution: end In the application.properties, you can specify the following parameter: yaml.flow.automation=classpath:/flows.yaml and update the \"flows.yaml\" file in the resources folder as follows: flows: - 'get-profile.yml' - 'create-profile.yml' - 'delete-profile.yml' - 'greetings.yml' Then, you can add a new REST endpoint in the \"rest.yaml\" configuration file like this. - service: \"http.flow.adapter\" methods: ['GET'] url: \"/api/greetings/{user}\" flow: 'greetings' timeout: 10s cors: cors_1 headers: header_1 The above REST endpoint takes the path parameter \"user\". The task executor will map the path parameter to the input arguments (headers and body) in your function. Now you can write your new function with the named route \"greeting.demo\". Please copy-n-paste the following into a Java class called \"Greetings\" and save in the package under \"my.organization.tasks\" in the source project. Note: \"my.organization\" package name is an example. Please replace it with your organization package path. @PreLoad(route=\"greeting.demo\", instances=10, isPrivate = false) public class Greetings implements TypedLambdaFunction, Map> { private static final String USER = \"user\"; @Override public Map handleEvent(Map headers, Map input, int instance) { if (input.containsKey(USER)) { String user = input.get(USER).toString(); Map result = new HashMap<>(); result.put(USER, user); result.put(\"message\", \"Welcome\"); result.put(\"time\", new Date()); return result; } else { throw new IllegalArgumentException(\"Missing path parameter 'user'\"); } } } For the flow-engine to find your new function, please update the key-value for \"web.component.scan\" in application.properties: web.component.scan=my.organization To test your new REST endpoint, flow configuration and function, please point your browser to http://127.0.0.1:8100/api/greetings/my_name You can replace \"my_name\" with your first name to see the response to the browser.","title":"Writing new REST endpoint and function"},{"location":"guides/CHAPTER-4/#flow-configuration-syntax","text":"In your \"greetings.yml\" file above, you find the following key-values: flow.id - Each flow must have a unique flow ID. The flow ID is usually originated from a user facing endpoint through an event adapter. For example, you may write an adapter to listen to a cloud event in a serverless deployment. In The most common one is the HTTP adapter. The flow ID is originated from the \"rest.yaml\". The flow-engine will find the corresponding flow configuration and create a new flow instance to process the user request. flow.description - this describes the purpose of the flow flow.ttl - \"Time to live (TTL)\" timer for each flow. You can define the maximum time for a flow to finish processing. All events are delivered asynchronously and there is no timeout value for each event. The TTL defines the time budget for a complete end-to-end flow. Upon expiry, an unfinished flow will be aborted. first.task - this points to the route name of a function (aka \"task\") to which the flow engine will deliver the incoming event. The configuration file contains a list of task entries where each task is defined by \"input\", \"process\", \"output\" and \"execution\" type. In the above example, the execution type is \"end\", meaning that it is the end of a transaction and its result set will be delivered to the user.","title":"Flow configuration syntax"},{"location":"guides/CHAPTER-4/#underlying-event-system","text":"The Event Script system uses platform-core as the event system where it encapsulates Java Virtual Threads, Eclipse Vertx, Kotlin coroutine and suspend function. The integration points are intentionally minimalist. For most use cases, the user application does not need to make any API calls to the underlying event system.","title":"Underlying Event System"},{"location":"guides/CHAPTER-4/#rest-automation-and-http-adapter","text":"The most common transaction entry point is a REST endpoint. The event flow may look like this: Request -> \"http.request\" -> \"task.executor\" -> user defined tasks -> \"async.http.response\" -> Response REST automation is part of the Mercury platform-core library. It contains a non-blocking HTTP server that converts HTTP requests and responses into events. It routes an HTTP request event to the HTTP adapter if the \"flow\" tag is provided. In the following example, the REST endpoint definition is declared in a \"rest.yaml\" configuration. It will route the URI \"/api/decision\" to the HTTP adapter that exposes its service route name as \"http.flow.adapter\". rest: - service: \"http.flow.adapter\" methods: ['GET'] url: \"/api/decision?decision=_\" flow: 'decision-test' timeout: 10s cors: cors_1 headers: header_1 tracing: true The \"cors\" and \"headers\" tags are optional. When specified, the REST endpoint will insert CORS headers and HTTP request headers accordingly. For rest.yaml syntax, please refer to https://accenture.github.io/mercury-composable/guides/CHAPTER-3 The HTTP adapter maps the HTTP request dataset and the flow ID into a standard event envelope for delivery to the flow engine. The HTTP request dataset, addressable with the \"input.\" namespace, contains the following: Key Values method HTTP method uri URI path header HTTP headers cookie HTTP cookies path_parameter Path parameters if any query HTTP query parameters if any body HTTP request body if any stream input stream route ID if any ip remote IP address filename filename if request is a multipart file upload session authenticated session key-values if any For easy matching, keys of headers, cookies, query and path parameters are case-insensitive. Regular API uses JSON and XML and they will be converted to a hashmap in the event's body. For special use cases like file upload/download, your application logic may invoke a streaming API to retrieve the binary payload. Please refer to the following sections for details. https://accenture.github.io/mercury-composable/guides/APPENDIX-III/#send-http-request-body-as-a-stream https://accenture.github.io/mercury-composable/guides/APPENDIX-III/#read-http-response-body-stream","title":"REST automation and HTTP adapter"},{"location":"guides/CHAPTER-4/#task-and-its-corresponding-function","text":"Each task in a flow must have a corresponding function. You can assign a task name to the function using the Preload annotation like this. @PreLoad(route=\"greeting.demo\", instances=10) public class Greetings implements TypedLambdaFunction, Map> { @Override public Map handleEvent(Map headers, Map input, int instance) { // business logic here return someOutput; } } The \"route\" in the Preload annotation is the task name. The \"instances\" define the maximum number of \"workers\" that the function can handle concurrently. The system is designed to be reactive and the function does not consume memory and CPU resources until an event arrives. You may also define concurrency using environment variable. You can replace the \"instances\" with envInstances using standard environment variable syntax like ${SOME_ENV_VARIABLE:default_value} .","title":"Task and its corresponding function"},{"location":"guides/CHAPTER-4/#unique-task-naming","text":"Composable functions are designed to be reusable. By changing some input data mapping to feed different parameters and payload, your function can behave differently. Therefore, it is quite common to use the same function (\"process\") more than once in a single event flow. When a task is not named, the \"process\" tag is used to name the task. Since each task must have a unique name for event routing, we cannot use the same \"process\" name more than once in an event flow. To handle this use case, you can create unique names for the same function (i.e. \"process\") like this: flow: id: 'greetings' description: 'Simplest flow' ttl: 10s first.task: 'my.first.task' tasks: - name: 'my.first.task' input: - 'input.path_parameter.user -> user' process: 'greeting.demo' output: - 'text(application/json) -> output.header.content-type' - 'result -> output.body' description: 'Hello World' execution: sequential next: - 'another.task' The above event flow configuration uses \"my.first.task\" as a named route for \"greeting.demo\" by adding the \"name\" tag to the composable function. For configuration simplicity, the \"name\" tag is optional. If not provided, the process name is assumed to be the unique \"task\" name. Important: The Event Manager performs event choreography using the unique task name. Therefore, when the \"process\" name for the function is not unique, you must create unique task \"names\" for the same function to ensure correct routing.","title":"Unique task naming"},{"location":"guides/CHAPTER-4/#assigning-multiple-route-names-to-a-single-function","text":"The built-in distributed tracing system tracks the actual composable functions using the \"process\" name and not the task names. When there is a need to track the task names in distributed trace, you can tell the system to create additional instances of the same function with different route names. You can use a comma separated list as the route name like this: @PreLoad(route=\"greeting.case.1, greeting.case.2\", instances=10) public class Greetings implements TypedLambdaFunction, Map> { @Override public Map handleEvent(Map headers, Map input, int instance) { // business logic here return someResult; } } Note: The \"unique task naming\" method is more memory efficient than creating additional route names","title":"Assigning multiple route names to a single function"},{"location":"guides/CHAPTER-4/#preload-overrides","text":"Once a composable function is published as a reusable library in the artifactory, its route name and number of instances are fixed using the \"PreLoad\" annotation in the function class. Without refactoring your libary, you can override its route name and instances using a preload override file like this: preload: - original: 'greeting.demo' routes: - 'greeting.case.1' - 'greeting.case.2' # the \"instances\" tag is optional instances: 20 - original: 'v1.another.reusable.function' keep-original: true routes: - 'v1.reusable.1' - 'v1.reusable.2' In the above example, the function associated with \"greeting.demo\" will be preloaded as \"greeting.case.1\" and \"greeting.case.2\". The number of maximum concurrent instances is also changed from 10 to 20. In the second example, \"v1.another.reusable.function\" is updated as \"v1.reusable.1\" and \"v1.reusable.2\" and the number of concurrent instances is not changed. The original route \"v1.another.reusable.function\" is preserved when the \"keep-original\" parameter is set to true. Assuming the above file is \"preload-override.yaml\" in the \"resources\" folder of the application source code project, you should add the following parameter in application.properties to activate this preload override feature. yaml.preload.override=classpath:/preload-override.yaml","title":"Preload overrides"},{"location":"guides/CHAPTER-4/#multiple-preload-override-config-files","text":"When you publish a composable function as a library, you may want to ensure the route names of the functions are merged properly. In this case, you can bundle a library specific preload override config file. For example, your library contains a \"preload-kafka.yaml\" to override some route names, you can add it to the yaml.preload.override parameter like this: yaml.preload.override=classpath:/preload-override.yaml, classpath:/preload-kafka.yaml The system will then merge the two preload override config files. The concurrency value of a function is overwritten using the \"instances\" parameter in the first preload override file. Subsequent override of the \"instances\" parameter is ignored. i.e. the first preload override file will take precedence.","title":"Multiple preload override config files"},{"location":"guides/CHAPTER-4/#hierarchy-of-flows","text":"Inside a flow, you can run one or more sub-flows. To do this, you can use the flow protocol identifier ( flow:// ) to indicate that the task is a flow. For example, when running the following task, \"flow://my-sub-flow\" will be executed like a regular task. tasks: - input: - 'input.path_parameter.user -> header.user' - 'input.body -> body' process: 'flow://my-sub-flow' output: - 'result -> model.pojo' description: 'Execute a sub-flow' execution: sequential next: - 'my.next.function' If the sub-flow is not available, the system will throw an error stating that it is not found. Hierarchy of flows would reduce the complexity of a single flow configuration file. The \"time-to-live (TTL)\" value of the parent flow should be set to a value that covers the complete flow including the time used in the sub-flows. For simplicity, the input data mapping for a sub-flow should contain only the \"header\" and \"body\" arguments.","title":"Hierarchy of flows"},{"location":"guides/CHAPTER-4/#tasks-and-data-mapping","text":"All tasks for a flow are defined in the \"tasks\" section.","title":"Tasks and data mapping"},{"location":"guides/CHAPTER-4/#inputoutput-data-mapping","text":"A function is self-contained. This modularity reduces application complexity because the developer only needs interface contract details for a specific function. To handle this level of modularity, the system provides configurable input/output data mapping. Namespaces for I/O data mapping Type Keyword and/or namespace LHS / RHS Mappings Flow input dataset input. left input Flow output dataset output. right output Function input body no namespace required right input Function input or output headers header or header. right I/O Function output result set result. left output Function output status code status left output Decision value decision right output State machine dataset model. left/right I/O External state machine key-value ext: right I/O Note that external state machine namespace uses \":\" to indicate that the key-value is external. Constants for input data mapping Type Keyword for the left-hand-side argument String text(example_value) Integer int(number) Long long(number) Float float(number) Double double(number) Boolean boolean(true or false) Map map(k1=v1, k2=v2) map(base.config.parameter) File file(text:file_path) file(binary:file_path) Classpath classpath(text:file_path) classpath(binary:file_path) For input data mapping, the \"file\" constant type is used to load some file content as an argument of a user function. You can tell the system to render the file as \"text\" or \"binary\". Similarly, the \"classpath\" constant type refers to static file in the application source code's \"resources\" folder. The \"map\" constant type is used for two purposes: 1. Map of key-values The following example illustrates creation of a map of key-values. In the first entry, a map of 2 key-values is set as the input argument \"myMap\" of a user function. In the second entry, the map's values are retrieved from the key \"some.key\" in base configuration and the environment variable \"ENV_VAR_ONE\". 'map(k1=v1, k2=v2) -> myMap' 'map(k1=${some.key}, k2=${ENV_VAR_ONE}) -> myMap' Note that the comma character is used as a separator for each key-value pair. If the value contains a comma, the system cannot parse the key-values correctly. In this case, please use the 2nd method below. 2. Mapping values from application.yml The following input data mapping sets the value of \"my.key\" from the application.yml base configuration file to the input argument \"myKey\" of a user function. 'map(my.key) -> myKey' Since the system uses both application.properties and application.yml as base configuration files, you can use either configuration files depending on the data type of the value. For application.properties, \"map(my.key)\" is the same as \"text(${my.key})\". For application.yml, \"map(my.key)\" would set a primitive value (text, integer, float, boolean), a hash map of key-values or an array of values. Special content type for output data mapping Type Keyword for the right-hand-side argument File file(file_path) For output data mapping, the \"file\" content type is used to save some data from the output of a user function to a file in the local file system. Decison value The \"decision\" keyword applies to \"right hand side\" of output data mapping statement in a decision task only (See \"Decision\" in the task section). Each flow has its own input and output Each function has its input headers, input body and output result set. Optionally, a function can return an EventEnvelope object to hold its result set in the \"body\", a \"status\" code and one or more header key-values. Since each function is stateless, a state machine (with namespace model. ) is available as a temporary memory store for transaction states that can be passed from one task to another. All variables are addressable using the standard dot-bracket convention. For example, \"hello.world\" will retrieve the value 100 from this data structure: { \"hello\": { \"world\": 100 } } and \"numbers[1]\" will retrieve the value 200 below: { \"numbers\": [100, 200] } The assignment is done using the -> syntax. In the following example, the HTTP input query parameter 'amount' is passed as input body argument 'amount' to the task 'simple.decision'. The result (function \"return value\") from the task will be mapped to the special \"decision\" variable that the flow engine will evaluate. This assumes the result is a boolean or numeric value. The \"decision\" value is also saved to the state machine ( model ) for subsequent tasks to evaluate. - input: - 'input.query.amount -> amount' process: 'simple.decision' output: - 'result -> decision' - 'result -> model.decision'","title":"Input/Output data mapping"},{"location":"guides/CHAPTER-4/#metadata-for-each-flow-instance","text":"For each flow instance, the state machine in the \"model\" namespace provides the following metadata that you can use in the input/output data mapping. For example, you can set this for an exception handler to log additional information. Type Keyword Comment Flow ID model.flow The ID of the event flow config Trace ID model.trace Optional traceId when tracing is turned on Correlation ID model.cid Correlation ID of the inbound request","title":"Metadata for each flow instance"},{"location":"guides/CHAPTER-4/#special-handling-for-header","text":"When function input keyword header is specified in the \"right hand side\" of an input data mapping statement, it refers to the input event envelope's headers. Therefore, it assumes the \"left hand side\" to resolve into a Map object of key-values. Otherwise, it will reject the input data mapping statement with an error like this: Invalid input mapping 'text(ok) -> header', expect: Map, Actual: String When function input namespace header. is used, the system will map the value resolved from the \"left hand side\" statement into the specific header. For example, the input data mapping statement text(ok) -> header.demo will set \"demo=ok\" into input event envelope's headers. When function output keyword header is specified in the \"left hand side\" of an output data mapping statement, it will resolve as a Map from the function output event envelope's headers. Similarly, when function output namespace header. is used, the system will resolve the value from a specific key of the function output event envelope's headers.","title":"Special handling for header"},{"location":"guides/CHAPTER-4/#function-input-and-output","text":"To support flexible input data mapping, the input to a function must be either Map or PoJo . The output (i.e. result set) of a function can be Map, PoJo or Java primitive. Your function can implement the TypedLambdaFunction interface to configure input and output. Since a data structure is passed to your function's input argument as key-values, you may create a PoJo class to deserialize the data structure. To tell the system that your function is expecting input as a PoJo, you can use the special notation * in the right hand side. For example, the following entry tells the system to set the value in \"model.dataset\" as a PoJo input. - input: - 'model.dataset -> *' If the value from the left hand side is not a map, the system will ignore the input mapping command and print out an error message in the application log.","title":"Function input and output"},{"location":"guides/CHAPTER-4/#setting-function-input-headers","text":"When function input body is used to hold a PoJo, we may use function input headers to pass other arguments to the function without changing the data structure of a user defined PoJo. In the following example, the HTTP query parameter \"userid\" will be mappped to the function input header key \"user\" and the HTTP request body will be mapped to the function input body. - input: - 'input.query.userid -> header.user' - 'input.body -> *' process: 'my.user.function' output: - 'text(application/json) -> output.header.content-type' - 'result -> output.body'","title":"Setting function input headers"},{"location":"guides/CHAPTER-4/#task-types","text":"","title":"Task types"},{"location":"guides/CHAPTER-4/#decision-task","text":"A decision task makes decision to select the next task to execute. It has the tag execution=decision . In the output data mapping section, it must map the corresponding result set or its key-value to the decision object. The \"next\" tag contains a list of tasks to be selected based on the decision value. If decision value is boolean, a true value will select the first task. Otherwise, the second task will be selected. If decision value is an integer, the number should start from 1 where the corresponding \"next\" task will be selected. tasks: - input: - 'input.query.decision -> decision' process: 'simple.decision' output: - 'result -> model.decision' - 'result -> decision' description: 'Simple decision test' execution: decision next: - 'decision.case.one' - 'decision.case.two'","title":"Decision task"},{"location":"guides/CHAPTER-4/#response-task","text":"A response task will provide result set as a flow output or \"response\". A response task allows the flow to respond to the user or caller immediately and then move on to the next task asynchronously. For example, telling the user that it has accepted a request and then moving on to process the request that may take longer time to run. A response task has the tag execution=response and a \"next\" task. tasks: - input: - 'input.path_parameter.user -> user' - 'input.query.seq -> sequence' process: 'sequential.one' output: - 'result -> model.pojo' - 'result -> output.body' description: 'Pass a pojo to another task' execution: response next: - 'sequential.two'","title":"Response task"},{"location":"guides/CHAPTER-4/#end-task","text":"An end task indicates that it is the last task of the transaction processing in a flow. If the flow has not executed a response task, the end task will generate the response. Response is defined by output data mapping. This task has the tag execution=end . For example, the greeting task in the unit tests is an end task. - input: - 'input.path_parameter.user -> user' process: 'greeting.demo' output: - 'text(application/json) -> output.header.content-type' - 'result -> output.body' description: 'Hello World' execution: end","title":"End task"},{"location":"guides/CHAPTER-4/#sequential-task","text":"Upon completion of a sequential task, the next task will be executed. This task has the tag execution=sequential . In the following example, sequential.two will be executed after sequential.one . tasks: - input: - 'input.path_parameter.user -> user' - 'input.query.seq -> sequence' process: 'sequential.one' output: - 'result -> model.pojo' description: 'Pass a pojo to another task' execution: sequential next: - 'sequential.two'","title":"Sequential task"},{"location":"guides/CHAPTER-4/#parallel-task","text":"Upon completion of a parallel task, all tasks in the \"next\" task list will be executed in parallel. This task has the tag execution=parallel . In this example, parallel.one and parallel.two will run after begin.parallel.test tasks: - input: - 'int(2) -> count' process: 'begin.parallel.test' output: [] description: 'Setup counter for two parallel tasks' execution: parallel next: - 'parallel.one' - 'parallel.two'","title":"Parallel task"},{"location":"guides/CHAPTER-4/#fork-n-join-task","text":"Fork-n-join is a parallel processing pattern. A \"fork\" task will execute multiple \"next\" tasks in parallel and then consolidate the result sets before running the \"join\" task. This task has the tag execution=fork . It must have a list of \"next\" tasks and a \"join\" task. It may look like this: tasks: - input: - 'input.path_parameter.user -> user' - 'input.query.seq -> sequence' process: 'sequential.one' output: - 'result -> model.pojo' description: 'Pass a pojo to another task' execution: fork next: - 'echo.one' - 'echo.two' join: 'join.task'","title":"Fork-n-join task"},{"location":"guides/CHAPTER-4/#sink-task","text":"A sink task is a task without any next tasks. Sink tasks are used by fork-n-join and pipeline tasks as reusable modules. This task has the tag execution=sink . - input: - 'text(hello-world-two) -> key2' process: 'echo.two' output: - 'result.key2 -> model.key2' description: 'Hello world' execution: sink","title":"Sink task"},{"location":"guides/CHAPTER-4/#pipeline-feature","text":"Pipeline is an advanced feature of Event Script.","title":"Pipeline feature"},{"location":"guides/CHAPTER-4/#pipeline-task","text":"A pipeline is a list of tasks that will be executed orderly within the current task. When the pipeline is done, the system will execute the \"next\" task. This task has the tag execution=pipeline . tasks: - input: - 'input.path_parameter.user -> user' - 'input.query.seq -> sequence' process: 'sequential.one' output: - 'result -> model.pojo' description: 'Pass a pojo to another task' execution: pipeline pipeline: - 'echo.one' - 'echo.two' next: - 'echo.three' Some special uses of pipelines include \"for/while-loop\" and \"continue/break\" features.","title":"Pipeline task"},{"location":"guides/CHAPTER-4/#simple-for-loop","text":"In the following example, the loop.statement contains a for-loop that uses a variable in the state machine to evaluate the loop. In this example, the pipeline will be executed three times before passing control to the \"next\" task. tasks: - input: - 'input.path_parameter.user -> user' - 'input.query.seq -> sequence' process: 'sequential.one' output: - 'result -> model.pojo' description: 'Pass a pojo to another task' execution: pipeline loop: statement: 'for (model.n = 0; model.n < 3; model.n++)' pipeline: - 'echo.one' - 'echo.two' - 'echo.three' next: - 'echo.four'","title":"Simple for-loop"},{"location":"guides/CHAPTER-4/#simple-while-loop","text":"The loop.statement may use a \"while loop\" syntax like this: loop: statement: 'while (model.running)' To exit the above while loop, one of the functions in the pipeline should return a boolean \"false\" value with output \"data mapping\" to the model.running variable.","title":"Simple while loop"},{"location":"guides/CHAPTER-4/#for-loop-with-breakcontinue-decision","text":"In the following example, the system will evaluate if the model.quit variable is true. If yes, the break or continue condition will be executed. The state variable is obtained after output data mapping and any task in the pipeline can set a key-value for mapping into the state variable. tasks: - input: - 'input.path_parameter.user -> user' - 'input.query.seq -> sequence' process: 'sequential.one' output: - 'result -> model.pojo' description: 'Pass a pojo to another task' execution: pipeline loop: statement: 'for (model.n = 0; model.n < 3; model.n++)' condition: - 'if (model.quit) break' pipeline: - 'echo.one' - 'echo.two' - 'echo.three' next: - 'echo.four'","title":"For loop with break/continue decision"},{"location":"guides/CHAPTER-4/#handling-exception","text":"You can define exception handler at the top level or at the task level. Exception is said to occur when a user function throws exception or returns an EventEnvelope object with a status code equals to or larger than 400. The event status uses the same numbering scheme as HTTP exception status code. Therefore, status code less than 400 is not considered an exception.","title":"Handling exception"},{"location":"guides/CHAPTER-4/#top-level-exception-handler","text":"Top-level exception handler is a \"catch-all\" handler. You can define it like this: flow: id: 'greetings' description: 'Simplest flow of one task' ttl: 10s exception: 'v1.my.exception.handler' In this example, the v1.my.exception.handler should point to a corresponding exception handler that you provide. The following input arguments will be delivered to your function when exception happens. Key Description status Exception status code message Error message stack Stack trace in a text string The exception handler function can be an \"end\" task to abort the transaction or a decision task to take care of the exception. For example, the exception handler can be a \"circuit-breaker\" to retry a request.","title":"Top-level exception handler"},{"location":"guides/CHAPTER-4/#task-level-exception-handler","text":"You can attach an exception handler to a task. One typical use is the \"circuit breaker\" pattern. In the following example, the user function \"breakable.function\" may throw an exception for some error condition. The exception will be caught by the \"v1.circuit.breaker\" function. - input: - 'input.path_parameter.accept -> accept' - 'model.attempt -> attempt' process: 'breakable.function' output: - 'int(0) -> model.attempt' - 'text(application/json) -> output.header.content-type' - 'result -> output.body' description: 'This demo function will break until the \"accept\" number is reached' execution: end exception: 'v1.circuit.breaker' The configuration for the circuit breaker function may look like this: - input: - 'model.attempt -> attempt' - 'int(2) -> max_attempts' - 'error.code -> status' - 'error.message -> message' - 'error.stack -> stack' process: 'v1.circuit.breaker' output: - 'result.attempt -> model.attempt' - 'result.decision -> decision' - 'result.status -> model.status' - 'result.message -> model.message' description: 'Just a demo circuit breaker' execution: decision next: - 'breakable.function' - 'abort.request' An exception handler will be provided with the \"error\" object that contains error code, error message and an exception stack trace. The exception handler can inspect the error object to make decision of the next step. For circuit breaker, we can keep the number of retry attempts in the state machine under \"model.attempt\" or any key name that you prefer. In the above example, it sets an integer constant of 2 for the maximum attempts. The circuit breaker can then evaluate if the number of attempts is less than the maximum attempts. If yes, it will return a decision of \"true\" value to tell the system to route to the \"breakable.function\" again. Otherwise, it will return a decision of \"false\" value to abort the request. A more sophisticated circuit breaker may be configured with \"alternative execution paths\" depending on the error status and stack trace. In this case, the decision value can be a number from 1 to n that corresponds to the \"next\" task list. Exception handlers may be used in both queries and transactions. For a complex transaction, the exception handler may implement some data rollback logic or recovery mechanism.","title":"Task-level exception handler"},{"location":"guides/CHAPTER-4/#best-practice","text":"When a task-level exception handler throws exception, it will be caught by the top-level exception handler, if any. A top-level exception handler should not throw exception. Otherwise it may go into an exception loop. Therefore, we recommend that an exception handler should return regular result set in a PoJo or a Map object. An example of task-level exception handler is shown in the \"HelloException.class\" in the unit test section of the event script engine where it set the status code in the result set so that the system can map the status code from the result set to the next task or to the HTTP output status code.","title":"Best practice"},{"location":"guides/CHAPTER-4/#advanced-features","text":"","title":"Advanced features"},{"location":"guides/CHAPTER-4/#simple-type-matching-and-conversion","text":"Event script's state machine supports simple type matching and conversion. This \"impedance matching\" feature allows us to accommodate minor interface contract changes without refactoring business logic of a user function. This is supported in both the left-hand-side and right-hand-side of both input and output data mappings. For the left-hand-side, the state machine's model value is matched or converted to the target data type before setting the value of the right-hand-side. The state machine values are unchanged. For the right-hand-side, the matched or converted value is applied to the state machine's model value. The syntax is model.somekey:type where \"type\" is one of the following: Type Match value as Example text text string model.someKey:text binary byte array model.someKey:binary int integer or -1 if not numeric model.someKey:int long long or -1 if not numeric model.someKey:long float float or -1 if not numeric model.someKey:float double double or -1 if not numeric model.someKey:double boolean true or false model.someKey:boolean boolean(value) true if value matches model.someKey:boolean(positive) boolean(value=true) true if value matches model.someKey:boolean(positive=true) boolean(value=false) false if value matches model.someKey:boolean(negative=false) and(model.key) boolean AND of 2 model keys model.someKey:and(model.another) or(model.key) boolean OR of 2 model keys model.someKey:or(model.another) substring(start, end) extract a substring model.someKey:substring(0, 5) substring(start) extract a substring model.someKey:substring(5) b64 byte-array to Base64 text model.someKey:b64 b64 Base64 text to byte-array model.someKey:b64 For boolean with value matching, the value can be null. This allows your function to test if the key-value in the left-hand-side is a null value. For Base64 type matching, if the key-value is a text string, the system will assume it is a Base64 text string and convert it to a byte-array. If the key-value is a byte-array, the system will encode it into a Base64 text string. An interesting use case of type matching is a simple decision task using the built-in no-op function. For example, when a control file for the application is not available, your application will switch to run in dev mode. A sample task may look like this: first.task: 'no.op' tasks: - input: - 'file(binary:/tmp/interesting-config-file) -> model.is-local:boolean(null=true)' process: 'no.op' output: - 'model.is-local -> decision' execution: decision next: - 'start.in.dev.mode' - 'start.in.cloud'","title":"Simple type matching and conversion"},{"location":"guides/CHAPTER-4/#external-state-machine","text":"The in-memory state machine is created for each query or transaction flow and it is temporal. For complex transactions or long running work flows, you would typically want to externalize some transaction states to a persistent store such as a distributed cache system or a high performance key-value data store. In these use cases, you can implement an external state machine function and configure it in a flow. Below is an example from a unit test. When you externalize a key-value to an external state machine, you must configure the route name (aka level-3 functional topic) of the external state machine. Note that when passing a null value to a key of an external state machine means \"removal\". external.state.machine: 'v1.ext.state.machine' tasks: - input: # A function can call an external state machine using input or output mapping. # In this example, it calls external state machine from input data mapping. - 'input.path_parameter.user -> ext:/${app.id}/user' - 'input.body -> model.body' # demonstrate saving constant to state machine and remove it using model.none - 'text(world) -> ext:hello' - 'model.none -> ext:hello' process: 'no.op' output: - 'text(application/json) -> output.header.content-type' # It calls external state machine again from output data mapping - 'input.body -> ext:/${app.id}/body' - 'input.body -> output.body' - 'text(message) -> ext:test' - 'model.none -> ext:test' description: 'Hello World' execution: end The \"external.state.machine\" parameter is optional. When present, the system will send a key-value from the current flow instance's state machine to the function implementing the external state machine. The system uses the \"ext:\" namespace to externalize a state machine's key-value. Note that the delivery of key-values to the external state machine is asynchronous. Therefore, please assume eventual consistency. You should implement a user function as the external state machine. The input interface contract to the external state machine for saving a key-value is: header.type = 'put' header.key = key body = value Your function should save the input key-value to a persistent store. In another flow that requires the key-value, you can add an initial task to retrieve from the persistent store and do \"output data mapping\" to save to the in-memory state machine so that your transaction flow can use the persisted key-values to continue processing. In the unit tests of the event-script-engine subproject, these two flows work together: externalize-put-key-value externalize-get-key-value IMPORTANT: Events to an external state machine are delivered asynchronously. If you want to guarantee message sequencing, please do not set the \"instances\" parameter in the PreLoad annotation. To illustrate a minimalist implementation, below is an example of an external state machine in the event-script-engine's unit test section. @PreLoad(route = \"v1.ext.state.machine\") public class ExternalStateMachine implements LambdaFunction { private static final Logger log = LoggerFactory.getLogger(ExternalStateMachine.class); private static final ManagedCache store = ManagedCache.createCache(\"state.machine\", 5000); private static final String TYPE = \"type\"; private static final String PUT = \"put\"; private static final String GET = \"get\"; private static final String REMOVE = \"remove\"; private static final String KEY = \"key\"; @Override public Object handleEvent(Map headers, Object input, int instance) { if (!headers.containsKey(KEY)) { throw new IllegalArgumentException(\"Missing key in headers\"); } String type = headers.get(TYPE); String key = headers.get(KEY); if (PUT.equals(type) && input != null) { log.info(\"Saving {} to store\", key); store.put(key, input); return true; } if (GET.equals(type)) { Object v = store.get(key); if (v != null) { log.info(\"Retrieve {} from store\", key); return v; } else { return null; } } if (REMOVE.equals(type)) { if (store.exists(key)) { store.remove(key); log.info(\"Removed {} from store\", key); return true; } else { return false; } } return false; } }","title":"External state machine"},{"location":"guides/CHAPTER-4/#future-task-scheduling","text":"You may add a \u201cdelay\u201d tag in a task so that it will be executed later. This feature is usually used for unit tests or \"future task scheduling\". Since the system is event-driven and non-blocking, the delay is simulated by event scheduling. It does not block the processing flow. Type Value Example Fixed delay Milliseconds delay=1000 Variable delay State machine variable delay=model.delay When delay is set to a state variable that its value is not configured by a prior data mapping, the delay command will be ignored. An example task that has an artificial delay of 2 seconds: tasks: - input: - 'input.path_parameter.user -> user' - 'input.query.ex -> exception' - 'text(hello world) -> greeting' process: 'greeting.test' output: - 'text(application/json) -> output.header.content-type' - 'result -> output.body' description: 'Hello World' execution: end delay: 2000 Chapter-3 Home Chapter-5 REST Automation Table of Contents Build, Test and Deploy","title":"Future task scheduling"},{"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-composable/examples/lambda-example mvn clean package java -jar target/lambda-example-4.0.16.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-2 , 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::add); EventEnvelope response = bench.poll(10, TimeUnit.SECONDS); assert response != null; assertEquals(HashMap.class, response.getBody().getClass()); MultiLevelMap map = new MultiLevelMap((Map) response.getBody()); assertEquals(\"b\", map.getElement(\"headers.a\")); assertEquals(name, map.getElement(\"body.name\")); assertEquals(address, map.getElement(\"body.address\")); assertEquals(telephone, map.getElement(\"body.telephone\")); 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::add); EventEnvelope response = bench.poll(10, TimeUnit.SECONDS); assert response != null; assertEquals(SamplePoJo.class, response.getBody().getClass()); SamplePoJo pojo = response.getBody(SamplePoJo.class); assertEquals(ID, pojo.getId()); assertEquals(NAME, pojo.getName()); 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(); String traceId = Utility.getInstance().getUuid(); PostOffice po = new PostOffice(\"unit.test\", traceId, \"/stream/upload/test\"); int len = 0; ByteArrayOutputStream bytes = new ByteArrayOutputStream(); EventPublisher publisher = new EventPublisher(10000); for (int i=0; i < 10; i++) { String line = \"hello world \"+i+\"\\n\"; byte[] d = util.getUTF(line); publisher.publish(d); bytes.write(d); len += d.length; } publisher.publishCompletion(); // emulate a multipart 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(publisher.getStreamId()); // send the HTTP request event to the \"hello.upload\" function EventEnvelope request = new EventEnvelope().setTo(\"hello.upload\") .setBody(req).setTrace(\"12345\", \"/api/upload/demo\").setFrom(\"unit.test\"); po.asyncRequest(request, 8000).onSuccess(bench::add); EventEnvelope response = bench.poll(10, TimeUnit.SECONDS); assert response != null; assertEquals(HashMap.class, response.getBody().getClass()); Map map = (Map) response.getBody(); System.out.println(response.getBody()); assertEquals(len, map.get(\"expected_size\")); assertEquals(len, map.get(\"actual_size\")); assertEquals(FILENAME, map.get(\"filename\")); 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); assertTrue(file.exists()); assertEquals(len, file.length()); // compare file content byte[] b = Utility.getInstance().file2bytes(file); 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:21-ubuntu EXPOSE 8083 WORKDIR /app COPY target/rest-spring-3-example-3.1.2.jar . ENTRYPOINT [\"java\",\"-jar\",\"rest-spring-3-example-3.1.2.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 Script Syntax 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-composable/examples/lambda-example mvn clean package java -jar target/lambda-example-4.0.16.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-2 , 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::add); EventEnvelope response = bench.poll(10, TimeUnit.SECONDS); assert response != null; assertEquals(HashMap.class, response.getBody().getClass()); MultiLevelMap map = new MultiLevelMap((Map) response.getBody()); assertEquals(\"b\", map.getElement(\"headers.a\")); assertEquals(name, map.getElement(\"body.name\")); assertEquals(address, map.getElement(\"body.address\")); assertEquals(telephone, map.getElement(\"body.telephone\")); 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::add); EventEnvelope response = bench.poll(10, TimeUnit.SECONDS); assert response != null; assertEquals(SamplePoJo.class, response.getBody().getClass()); SamplePoJo pojo = response.getBody(SamplePoJo.class); assertEquals(ID, pojo.getId()); assertEquals(NAME, pojo.getName()); 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(); String traceId = Utility.getInstance().getUuid(); PostOffice po = new PostOffice(\"unit.test\", traceId, \"/stream/upload/test\"); int len = 0; ByteArrayOutputStream bytes = new ByteArrayOutputStream(); EventPublisher publisher = new EventPublisher(10000); for (int i=0; i < 10; i++) { String line = \"hello world \"+i+\"\\n\"; byte[] d = util.getUTF(line); publisher.publish(d); bytes.write(d); len += d.length; } publisher.publishCompletion(); // emulate a multipart 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(publisher.getStreamId()); // send the HTTP request event to the \"hello.upload\" function EventEnvelope request = new EventEnvelope().setTo(\"hello.upload\") .setBody(req).setTrace(\"12345\", \"/api/upload/demo\").setFrom(\"unit.test\"); po.asyncRequest(request, 8000).onSuccess(bench::add); EventEnvelope response = bench.poll(10, TimeUnit.SECONDS); assert response != null; assertEquals(HashMap.class, response.getBody().getClass()); Map map = (Map) response.getBody(); System.out.println(response.getBody()); assertEquals(len, map.get(\"expected_size\")); assertEquals(len, map.get(\"actual_size\")); assertEquals(FILENAME, map.get(\"filename\")); 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); assertTrue(file.exists()); assertEquals(len, file.length()); // compare file content byte[] b = Utility.getInstance().file2bytes(file); 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:21-ubuntu EXPOSE 8083 WORKDIR /app COPY target/rest-spring-3-example-3.1.2.jar . ENTRYPOINT [\"java\",\"-jar\",\"rest-spring-3-example-3.1.2.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 Script Syntax 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 3.2.1 and implement your Spring Boot main application Add the 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-3 library in your application and turn it into a pre-configured Spring Boot 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-3-example demo application Let's review the rest-spring-3-example demo application in the \"examples/rest-spring-3-example\" project. You can use the rest-spring-3-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: Spring RestControllers with Mono/Flux Servlet 3.1 WebServlets We will examine asynchronous REST endpoint with the AsyncHelloWorld class. @RestController public class AsyncHelloWorld { private static final AtomicInteger seq = new AtomicInteger(0); @GetMapping(value = \"/api/hello/world\", produces={\"application/json\", \"application/xml\"}) public Mono> hello(HttpServletRequest request) { 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 a parameter showing the sequence counter. // The echo service will return both. int n = seq.incrementAndGet(); EventEnvelope req = new EventEnvelope(); req.setTo(\"hello.world\").setBody(forward).setHeader(\"seq\", n); return Mono.create(callback -> { try { po.asyncRequest(req, 3000) .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()); callback.success(result); }) .onFailure(ex -> callback.error(new AppException(408, ex.getMessage()))); } catch (IOException e) { callback.error(e); } }); } } In this hello world REST endpoint, Spring Reactor 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 3.2.1 and implement your Spring Boot main application Add the 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-3 library in your application and turn it into a pre-configured Spring Boot 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-3-example-demo-application","text":"Let's review the rest-spring-3-example demo application in the \"examples/rest-spring-3-example\" project. You can use the rest-spring-3-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: Spring RestControllers with Mono/Flux Servlet 3.1 WebServlets We will examine asynchronous REST endpoint with the AsyncHelloWorld class. @RestController public class AsyncHelloWorld { private static final AtomicInteger seq = new AtomicInteger(0); @GetMapping(value = \"/api/hello/world\", produces={\"application/json\", \"application/xml\"}) public Mono> hello(HttpServletRequest request) { 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 a parameter showing the sequence counter. // The echo service will return both. int n = seq.incrementAndGet(); EventEnvelope req = new EventEnvelope(); req.setTo(\"hello.world\").setBody(forward).setHeader(\"seq\", n); return Mono.create(callback -> { try { po.asyncRequest(req, 3000) .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()); callback.success(result); }) .onFailure(ex -> callback.error(new AppException(408, ex.getMessage()))); } catch (IOException e) { callback.error(e); } }); } } In this hello world REST endpoint, Spring Reactor 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-3-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.1.2.jar Second, build and run the rest-spring-example application. cd examples/rest-spring-example-3 java -jar target/rest-spring-3-example-3.1.2.jar The rest-spring-3-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: @RestController public class HelloPoJoEventOverHttp { @GetMapping(\"/api/pojo/http/{id}\") public Mono getPoJo(@PathVariable(\"id\") Integer id) { 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); return Mono.create(callback -> { try { EventEnvelope response = po.request(req, 3000, Collections.emptyMap(), remoteEndpoint, true).get(); if (response.getBody() instanceof SamplePoJo result) { callback.success(result); } else { callback.error(new AppException(response.getStatus(), response.getError())); } } catch (IOException | ExecutionException | InterruptedException e) { callback.error(e); } }); } } The method signatures of the Event API is shown as follows: Asynchronous API (Java) // io.vertx.core.Future public Future asyncRequest(final EventEnvelope event, long timeout, Map headers, String eventEndpoint, boolean rpc) throws IOException; Sequential non-blocking API (virtual thread function) // java.util.concurrent.Future public Future request(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: 'hello.pojo2' target: 'http://127.0.0.1:${lambda.example.port}/api/event' - 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 three routes (hello.pojo2, 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. An example in the rest-spring-3-example subproject is shown below to illustrate this service abstraction. In this example, the remote Event-over-HTTP endpoint address is resolved from the event-over-http.yaml configuration. @RestController public class HelloPoJoEventOverHttpByConfig { @GetMapping(\"/api/pojo2/http/{id}\") public Mono getPoJo(@PathVariable(\"id\") Integer id) { String traceId = Utility.getInstance().getUuid(); PostOffice po = new PostOffice(\"hello.pojo.endpoint\", traceId, \"GET /api/pojo2/http\"); /* * \"hello.pojo2\" resides in the lambda-example and is reachable by \"Event-over-HTTP\". * In HelloPojoEventOverHttp.java, it demonstrates the use of Event-over-HTTP API. * In this example, it illustrates the use of the \"Event-over-HTTP by configuration\" feature. * Please see application.properties and event-over-http.yaml files for more details. */ EventEnvelope req = new EventEnvelope().setTo(\"hello.pojo2\").setHeader(\"id\", id); return Mono.create(callback -> { try { EventEnvelope response = po.request(req, 3000, false).get(); if (response.getBody() instanceof SamplePoJo result) { callback.success(result); } else { callback.error(new AppException(response.getStatus(), response.getError())); } } catch (IOException | ExecutionException | InterruptedException e) { callback.error(e); } }); } } Note: The configuration based \"event-over-HTTP\" feature does not support fork-n-join request API. You can achieve similar parallel processing using multiple calls to \"po.request API\" where each call returns a Java \"Future\". 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 Minimalist 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.1.2.jar Second, build and run the rest-spring-example application. cd examples/rest-spring-example-3 java -jar target/rest-spring-3-example-3.1.2.jar The rest-spring-3-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: @RestController public class HelloPoJoEventOverHttp { @GetMapping(\"/api/pojo/http/{id}\") public Mono getPoJo(@PathVariable(\"id\") Integer id) { 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); return Mono.create(callback -> { try { EventEnvelope response = po.request(req, 3000, Collections.emptyMap(), remoteEndpoint, true).get(); if (response.getBody() instanceof SamplePoJo result) { callback.success(result); } else { callback.error(new AppException(response.getStatus(), response.getError())); } } catch (IOException | ExecutionException | InterruptedException e) { callback.error(e); } }); } } The method signatures of the Event API is shown as follows:","title":"Test drive Event API"},{"location":"guides/CHAPTER-7/#asynchronous-api-java","text":"// io.vertx.core.Future 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-virtual-thread-function","text":"// java.util.concurrent.Future public Future request(final EventEnvelope event, long timeout, Map headers, String eventEndpoint, boolean rpc) throws IOException;","title":"Sequential non-blocking API (virtual thread function)"},{"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: 'hello.pojo2' target: 'http://127.0.0.1:${lambda.example.port}/api/event' - 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 three routes (hello.pojo2, 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. An example in the rest-spring-3-example subproject is shown below to illustrate this service abstraction. In this example, the remote Event-over-HTTP endpoint address is resolved from the event-over-http.yaml configuration. @RestController public class HelloPoJoEventOverHttpByConfig { @GetMapping(\"/api/pojo2/http/{id}\") public Mono getPoJo(@PathVariable(\"id\") Integer id) { String traceId = Utility.getInstance().getUuid(); PostOffice po = new PostOffice(\"hello.pojo.endpoint\", traceId, \"GET /api/pojo2/http\"); /* * \"hello.pojo2\" resides in the lambda-example and is reachable by \"Event-over-HTTP\". * In HelloPojoEventOverHttp.java, it demonstrates the use of Event-over-HTTP API. * In this example, it illustrates the use of the \"Event-over-HTTP by configuration\" feature. * Please see application.properties and event-over-http.yaml files for more details. */ EventEnvelope req = new EventEnvelope().setTo(\"hello.pojo2\").setHeader(\"id\", id); return Mono.create(callback -> { try { EventEnvelope response = po.request(req, 3000, false).get(); if (response.getBody() instanceof SamplePoJo result) { callback.success(result); } else { callback.error(new AppException(response.getStatus(), response.getError())); } } catch (IOException | ExecutionException | InterruptedException e) { callback.error(e); } }); } } Note: The configuration based \"event-over-HTTP\" feature does not support fork-n-join request API. You can achieve similar parallel processing using multiple calls to \"po.request API\" where each call returns a Java \"Future\".","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 Minimalist Service Mesh","title":"Advantages"},{"location":"guides/CHAPTER-8/","text":"Minimalist 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 minimalist 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. 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 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\" or \"kafka\". 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.1.2.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 a temporary directory \"/tmp/kafka-logs\". The kafka server is designed for development purpose only. The kafka message log store 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.1.2.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.1.2.jar cd examples/lambda-example java -Dcloud.connector=kafka -Dmandatory.health.dependencies=cloud.connector.health -jar target/lambda-example-3.1.2.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: { \"dependency\": [ { \"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 { \"dependency\": [ { \"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/#minimalist-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":"Minimalist Service Mesh"},{"location":"guides/CHAPTER-8/#using-kafka-as-a-minimalist-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. 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 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\" or \"kafka\". 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 minimalist 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.1.2.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 a temporary directory \"/tmp/kafka-logs\". The kafka server is designed for development purpose only. The kafka message log store 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.1.2.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.1.2.jar cd examples/lambda-example java -Dcloud.connector=kafka -Dmandatory.health.dependencies=cloud.connector.health -jar target/lambda-example-3.1.2.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: { \"dependency\": [ { \"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 { \"dependency\": [ { \"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 must 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 AutoStart class 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. Duplicated sequence numbers are allowed. Normal startup sequence must be between 1 and 999. Note : It is the \"start\" method of each EntryPoint implementation that follows the execution sequence of the MainApplication annotation. The optional \"main\" method is used only to kick off the application bootstrap and it must include only the following statement: public static void main(String[] args) { AutoStart.main(args); } Therefore, even when the default sequence of the MainApplication annotation is 10 and you invoke the \"main\" method from an IDE, the \"start\" method of each MainApplication modules will execute orderly. Setup before the Main Application 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(s). 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. Normal startup sequence must be between 6 and 999. Sequence 5 is reserved by the AsyncHttpClientLoader. If your startup code does not need the async HTTP client service and you want it to run first, you may use sequence from 1 to 4. Event envelope Mercury 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. PoJo transport Your function can implement the TypedLambdaFunction interface if you want to use PoJo as input and output. If you use the EventEnvelope as input, PoJo payload is provided as a HashMap in the event's body. The original class name of the PoJo payload is saved in the event's type attribute. You can compare and restore the PoJo like this: if (SamplePoJo.class.getName().equals(input.getType())) { SamplePoJo pojo = input.getBody(SamplePoJo.class); // do something with your input PoJo } If you use the \"untyped\" LambdaFunction, the input \"Object\" is a HashMap and you would need to convert it back to a PoJo using the SimpleMapper or a serializer of your choice. For example, SamplePoJo pojo = SimpleMapper.getInstance().getMapper().readValue((Map) input, SamplePoJo.class); 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; } } 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 most convenient method to make a sequential non-blocking RPC call is to use the PostOffice's request API. // for a single RPC call PostOffice po = new PostOffice(headers, instance); EventEnvelope result = po.request(requestEvent, timeoutInMills).get(); // for a fork-n-join call PostOffice po = new PostOffice(headers, instance); List result = po.request(requestEvents, timeoutInMills).get(); If you prefer the Kotlin programming language, you may use the FastRPC API. 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} another.key=${my.key:12345} complex.key=first ${FIRST_ENV_VAR}, second ${SECOND_ENV_VAR} In the above example, a parameter may contain references to more than one environment variable. Default value, if not given, will be assumed to be an empty string. 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 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. Event Scripting To further reduce coding effort, you can perform \"event choreography\" by configuration using \"Event Script\". Please refer to Event Script syntax in Chapter 4 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. The core-engine 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 We recommend using the composable-example project as a template to start writing your Composable applications. You can follow the Composable methodology where you draw event flow diagrams to represent various use cases, convert them into event scripts that carry out event chorerography for your self-contained functions. For more information, please refer to Event Script syntax in Chapter 4 . If you prefer to do low-level event-driven programming, you can use the lambda-example project as a template. 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. Technical support For enterprise clients, optional technical support is available. Please contact your Accenture representative for details. Chapter-8 Home Minimalist Service Mesh Table of Contents","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 must 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 AutoStart class 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. Duplicated sequence numbers are allowed. Normal startup sequence must be between 1 and 999. Note : It is the \"start\" method of each EntryPoint implementation that follows the execution sequence of the MainApplication annotation. The optional \"main\" method is used only to kick off the application bootstrap and it must include only the following statement: public static void main(String[] args) { AutoStart.main(args); } Therefore, even when the default sequence of the MainApplication annotation is 10 and you invoke the \"main\" method from an IDE, the \"start\" method of each MainApplication modules will execute orderly.","title":"Main Application"},{"location":"guides/CHAPTER-9/#setup-before-the-main-application","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(s). 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. Normal startup sequence must be between 6 and 999. Sequence 5 is reserved by the AsyncHttpClientLoader. If your startup code does not need the async HTTP client service and you want it to run first, you may use sequence from 1 to 4.","title":"Setup before the Main Application"},{"location":"guides/CHAPTER-9/#event-envelope","text":"Mercury 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/#pojo-transport","text":"Your function can implement the TypedLambdaFunction interface if you want to use PoJo as input and output. If you use the EventEnvelope as input, PoJo payload is provided as a HashMap in the event's body. The original class name of the PoJo payload is saved in the event's type attribute. You can compare and restore the PoJo like this: if (SamplePoJo.class.getName().equals(input.getType())) { SamplePoJo pojo = input.getBody(SamplePoJo.class); // do something with your input PoJo } If you use the \"untyped\" LambdaFunction, the input \"Object\" is a HashMap and you would need to convert it back to a PoJo using the SimpleMapper or a serializer of your choice. For example, SamplePoJo pojo = SimpleMapper.getInstance().getMapper().readValue((Map) input, SamplePoJo.class);","title":"PoJo transport"},{"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; } } 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 most convenient method to make a sequential non-blocking RPC call is to use the PostOffice's request API. // for a single RPC call PostOffice po = new PostOffice(headers, instance); EventEnvelope result = po.request(requestEvent, timeoutInMills).get(); // for a fork-n-join call PostOffice po = new PostOffice(headers, instance); List result = po.request(requestEvents, timeoutInMills).get(); If you prefer the Kotlin programming language, you may use the FastRPC API. 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} another.key=${my.key:12345} complex.key=first ${FIRST_ENV_VAR}, second ${SECOND_ENV_VAR} In the above example, a parameter may contain references to more than one environment variable. Default value, if not given, will be assumed to be an empty string.","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","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"},{"location":"guides/CHAPTER-9/#event-scripting","text":"To further reduce coding effort, you can perform \"event choreography\" by configuration using \"Event Script\". Please refer to Event Script syntax in Chapter 4","title":"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. The core-engine 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":"We recommend using the composable-example project as a template to start writing your Composable applications. You can follow the Composable methodology where you draw event flow diagrams to represent various use cases, convert them into event scripts that carry out event chorerography for your self-contained functions. For more information, please refer to Event Script syntax in Chapter 4 . If you prefer to do low-level event-driven programming, you can use the lambda-example project as a template. 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.","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 Minimalist Service Mesh Table of Contents","title":"Technical support"},{"location":"guides/TABLE-OF-CONTENTS/","text":"Developer's Guide Mercury Composable is a software development toolkit for writing composable applications. Chapter 1 - Getting Started Chapter 2 - Function Execution Strategies Chapter 3 - REST Automation Chapter 4 - Event Script Syntax Chapter 5 - Build, Test and Deploy Chapter 6 - Spring Boot Chapter 7 - Event over HTTP Chapter 8 - Minimalist Service Mesh Chapter 9 - API Overview Appendix I - application.properties Appendix II - Reserved names and headers Appendix III - Actuators, HTTP client and More","title":"Contents"},{"location":"guides/TABLE-OF-CONTENTS/#developers-guide","text":"Mercury Composable is a software development toolkit for writing composable applications. Chapter 1 - Getting Started Chapter 2 - Function Execution Strategies Chapter 3 - REST Automation Chapter 4 - Event Script Syntax Chapter 5 - Build, Test and Deploy Chapter 6 - Spring Boot Chapter 7 - Event over HTTP Chapter 8 - Minimalist Service Mesh Chapter 9 - API Overview Appendix I - application.properties Appendix II - Reserved names and headers Appendix III - Actuators, HTTP client and More","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 00000000..8628dbce --- /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 00000000..3e356220 --- /dev/null +++ b/docs/sitemap.xml @@ -0,0 +1,98 @@ + + + + https://github.com/accenture/mercury/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/CHANGELOG/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/CODE_OF_CONDUCT/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/CONTRIBUTING/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/INCLUSIVITY/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/arch-decisions/DESIGN-NOTES/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/guides/APPENDIX-I/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/guides/APPENDIX-II/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/guides/APPENDIX-III/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-1/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-2/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-3/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-4/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-5/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-6/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-7/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-8/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/guides/CHAPTER-9/ + 2024-12-19 + daily + + + https://github.com/accenture/mercury/guides/TABLE-OF-CONTENTS/ + 2024-12-19 + daily + + \ No newline at end of file diff --git a/docs/sitemap.xml.gz b/docs/sitemap.xml.gz new file mode 100644 index 00000000..077d3a83 Binary files /dev/null and b/docs/sitemap.xml.gz differ