Giter Site home page Giter Site logo

zenflow / composite-service Goto Github PK

View Code? Open in Web Editor NEW
7.0 1.0 1.0 1.6 MB

Compose multiple services into one

License: MIT License

JavaScript 3.77% TypeScript 96.23%
devops monolith monolith-architecture monoliths monolithic microservice microservices-architecture microservices-libraries

composite-service's Introduction

composite-service

Compose multiple services into one

npm version CI status Code Climate maintainability Known Vulnerabilities License: MIT

In essence, composite-service is a Node.js library to use in a script that composes multiple services into one "composite service", by managing a child process for each composed service.

A composite service script is useful for packaging an application of multiple services or process types (e.g. a frontend web app & a backend web api) within a single Docker image, for simplified delivery and deployment. That is, if you have multiple services that make up your overall application, you do not need to deploy them to your hosting individually (multiplying cost & weakening manageability), nor do you need to use more advanced/complex tools like Kubernetes to manage your deployments; You can just bundle the services together with a composite service script.

Features

  • Configuration lives in a script, not .json or .yaml file & therefore supports startup tasks and dynamic configurations
  • Includes TypeScript types, meaning autocompletion & code intelligence in your IDE even if you're writing JavaScript
  • Configurable Crash Handling with smart default
  • Graceful Startup, to ensure a service is only started after the services it depends on are ready
  • Options for Shutting Down
  • Supports executing Node.js CLI programs by name
  • Companion composite-service-http-gateway package

Table of Contents

Install

npm install composite-service

Basic Usage

Create a Node.js script that calls the exported startCompositeService function with a CompositeServiceConfig object. That object includes a services property, which is a collection of ServiceConfig objects keyed by service ID.

The complete documentation for these config object interfaces is in the source code ( CompositeServiceConfig.ts & ServiceConfig.ts ) and should be accessible with code intelligence in most IDEs. Please use this as your main reference when using composite-service.

The most basic properties of ServiceConfig are:

  • cwd Current working directory of the service. Defaults to '.'.
  • command Command used to run the service. Required.
  • env Environment variables to pass to the service. Defaults to process.env.

Example

const { startCompositeService } = require('composite-service')

const { PORT, DATABASE_URL } = process.env

startCompositeService({
  services: {
    worker: {
      cwd: `${__dirname}/worker`,
      command: 'node main.js',
      env: { DATABASE_URL },
    },
    web: {
      cwd: `${__dirname}/web`,
      command: ['node', 'server.js'],
      env: { PORT, DATABASE_URL },
    },
  },
})

The above script will:

  1. Start the described services (worker & web) with their respective configuration
  2. Print interleaved stdout & stderr of each service, each line prepended with the service ID
  3. Restart services after they crash
  4. Shut down services and exit when it is itself told to shut down

Crash Handling

A "crash" is considered to be when a service exits unexpectedly (i.e. without being told to do so).

Default behavior

The default behavior for handling crashes is to restart the service if it already achieved "ready" status. If the service crashes before becoming "ready" (i.e. during startup), the composite service itself will bail out and crash (shut down & exit). This saves us from burning system resources (continuously crashing & restarting) when a service is completely broken.

To benefit from this behavior, ServiceConfig.ready must be defined. Otherwise, the service is considered ready as soon as the process is spawned, and therefore the service will always be restarted after a crash, even if it happened during startup.

Example

These changes to the initial example will prevent either service from spinning and burning resources when unable to start up:

const { startCompositeService } = require('composite-service')

const { PORT, DATABASE_URL } = process.env

startCompositeService({
  services: {
    worker: {
      cwd: `${__dirname}/worker`,
      command: 'node main.js',
      env: { DATABASE_URL },
+     // ready once a certain line appears in the console output
+     ready: ctx => ctx.onceOutputLine(line => line === 'Started worker'),
    },
    web: {
      cwd: `${__dirname}/web`,
      command: ['node', 'server.js'],
      env: { PORT, DATABASE_URL },
+     // ready once port is accepting connections
+     ready: ctx => ctx.onceTcpPortUsed(PORT),
    },
  },
})

Configuring behavior

Crash handling behavior can be configured with ServiceConfig.onCrash. This is a function executed each time the service crashes, to determine whether to restart the service or to crash the composite service, and possibly perform arbitrary tasks such as sending an email or calling a web hook. It receives an OnCrashContext object with some contextual information.

The default crash handling behavior (described in the section above) is implemented as the default value for onCrash. You may want to include this logic in your own custom onCrash functions:

ctx => {
  if (!ctx.isServiceReady) throw new Error('Crashed before becoming ready')
}

Example

const { startCompositeService } = require('composite-service')

startCompositeService({
  services: { ... },
  // Override configuration defaults for all services
  serviceDefaults: {
    onCrash: async ctx => {
      // Crash composite process if service crashed before becoming ready
      if (!ctx.isServiceReady) throw new Error('Crashed before becoming ready')
      // Try sending an alert via email (but don't wait for it or require it to succeed)
      email('[email protected]', `Service ${ctx.serviceId} crashed`, ctx.crash.logTail.join('\n'))
        .catch(console.error)
      // Do "something async" before restarting the service
      await doSomethingAsync()
    },
    // Set max length of `ctx.crash.logTail` used above (default is 0)
    logTailLength: 5,
  },
})

Graceful Startup

If we have a service that depends on another service or services, and don't want it to be started until the other "dependency" service or services are "ready", we can use dependencies and ready ServiceConfig properties. A service will not be started until all its dependencies are ready according to their respective ready config.

Example

The following script will start web only after api is accepting connections. This prevents web from appearing ready to handle requests before it's actually ready, and allows it to safely make calls to api during startup.

const { startCompositeService } = require('composite-service')

const webPort = process.env.PORT || 3000
const apiPort = process.env.API_PORT || 8000

startCompositeService({
  services: {
    web: {
      dependencies: ['api'],
      command: 'node web/server.js',
      env: { PORT: webPort, API_ENDPOINT: `http://localhost:${apiPort}` },
      ready: ctx => ctx.onceTcpPortUsed(webPort),
    },
    api: {
      command: 'node api/server.js',
      env: { PORT: apiPort },
      ready: ctx => ctx.onceTcpPortUsed(apiPort),
    },
  },
})

Shutting Down

The composite service will shut down when it encounters a fatal error (error spawning process, or error from ready or onCrash config functions) or when it receives a signal to shut down (ctrl+c, SIGINT, or SIGTERM).

The default procedure for shutting down is to immediately signal all composed services to shut down, and wait for them to exit before exiting itself. Where supported (i.e. on non-Windows systems), a SIGINT signal is issued first, and if the process does not exit within a period of time (ServiceConfig.forceKillTimeout), a SIGKILL signal is issued to forcibly kill the process. On Windows, where such signal types don't exist, a single signal is issued, which forcibly kills the process.

Some optional behaviors can be enabled. See gracefulShutdown & windowsCtrlCShutdown properties in CompositeServiceConfig.

Hint: If a composed service needs to do any cleanup before exiting, you should enable windowsCtrlCShutdown to allow for that when on Windows. This option however comes with some caveats. See windowsCtrlCShutdown in CompositeServiceConfig.

composite-service's People

Contributors

dependabot[bot] avatar semantic-release-bot avatar zenflow avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

lgtm-migrator

composite-service's Issues

Show website some love

  1. nicer theme & styles/colors
  2. github stars somwhere on website to encourage starring
  3. include README badges & links, & possibly whole README (organizational change)
  4. frontpage demo animation (show contents of a composite service script & results of running it)
  5. site search https://docsearch.algolia.com/
  6. sitemap - docusaurus-plugin-sitemap package?
  7. social media meta tags - https://metatags.io
  8. make landing page pleasant and or informative. For inspiration:

Upgrade dependencies

This also includes replacing tsdx which is blocking upgrading TypeScript, ESLint, etc.:

  • bundling: microbundle or tsup
  • linting: eslint & eslint-config-zenflow
  • testing: unit testing needs to test TS source which also relies on Babel + babel-plugin-macros + ts-interface-builder/macro

README enhancements

  • general editing
  • less verbose Features section
  • simplify/reduce Basic Usage section
  • explain ServiceConfig.ready function before Crash Handling (which should be changed to Custom Crash Handling)
  • Shutting down: detail "optional behaviors"
  • Shutting Down: "hint" -> Caveats (need for windowsCtrlCShutdown /w e.g. nodemon)

and enhancements to README for https://github.com/zenflow/composite-service-http-gateway

Companion package: composite-service-node-cluster

// Configures a composite service that creates a cluster (using Node.js's `cluster` module) of the given `config.script`
declare function configureNodeCluster (config: NodeClusterConfig): ServiceConfig

// Example of using configureNodeCluster
startCompositeService({
  services: {
    cluster: configureNodeCluster({ script: 'path/to/script.js', scale: 4 }),
    // more services....
  },
})

Fix color contrast in terminal output

Each line of output for each composed service has a prefix with some styles applied: bright background colors & default text color.
This looks fine when the terminal background is black/dark background, but sometimes terminal background is white/light and the contrast is not nice.

Demo project on codesandbox

I'm thinking a composite service composing:

  1. some node web app like a file manager or TODO tracker from awesome-selfhosted
  2. an express.js backend that makes some calls to it
  3. a static frontend with Parcel

Project should be able to run in either production or development mode.

Feature this in the docs introduction.

Add "Port Utilities"

The following interface would be useful for safely determining port numbers for your services.

type PortNumber = number | string

// Throw an error if the given `port` number is already in use
declare function assertPortFree(port: PortNumber): void

// Returns an array of port numbers that are not in use. 
declare function findPorts(numberOfPorts: number, options: { exclude: PortNumber | PortNumber[] }): number[]

// Example of using findPorts
const [apiPort, webPort] = findPorts(2, { exclude: PORT });

The function signatures above assume that we're only ever working with ports on localhost.
Should we support working with ports on other hosts?
Docker Machine runs on a different host than localhost on Windows, for example.

Interactive mode when `process.stdin.isTTY`

Interactive mode could be used to receive commands from keyboard input, such as:

  1. Select service to direct keyboard input into
  2. Restart a certain service on-the-fly
  3. Stop a certain service's dependents, restart the certain service, start the certain service's dependents again

If there will be some kind of interactive UI in the terminal, it would also be nice to have a "dashboard line" displayed below the log tail and above the command prompt, which indicates the status of each service (e.g. starting, stopping, crashed)

This ink package looks pretty neato! Uses React, and this build system already compiles jsx.

Config validation error improvements

Config validation errors:

  1. should show the invalid value
  2. should show link to documentation for the relevant interface field
  3. should include full description of type instead of "1 more" (issue for https://github.com/gristlabs/ts-interface-checker)
  4. should not show stack trace (?)

Also, maybe validation errors for config.services.foo should appear like nested validation errors in config. Or maybe validation errors for config.serviceDefaults should not appear like nested validation errors in config.

Also, not an enhancement to functionality, but for maintainability gristlabs/ts-interface-builder#24 needs to be documented & an official release made.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.