1.2.2 • Published 8 months ago

eptanaut v1.2.2

Weekly downloads
-
License
MIT
Repository
github
Last release
8 months ago

eptánaut

A scalable multithreaded Node.js HTTP server.

Eptanaut

Each eptanaut Service consists of a TCP proxy and a configurable pool of HTTP servers that run in Worker threads. When the thread pool is exhausted, eptanaut will uniformly distribute TCP sockets across the pool of allocated HTTP servers. This strategy allows for both distribution and parallel processing of incoming requests. Eptanaut exposes the same API for HTTP requests provided by Node's http.Server and https.Server; hence, if you know the Node API, you already know how to build applications on eptanaut!

Features

  • Eptanaut requires 0 out-of-org dependencies. Eptanaut's dependencies are published and maintained by the FAR Analytics and Research org.
    Dependencies: - The farar/memoir logger. - The farar/port_agent RPC facility.
  • The eptanaut Service constructor consumes native Node net.Server and http.Server instances; you can configure them however you choose.
  • The http.IncomingMessage and http.ServerResponse objects passed to route handlers are unadulterated native Node objects - nothing added - nothing removed.
  • Import eptanaut as a Node.js module (see the Hello World! example) or take advantage of the packaged type definitions and import it into your TypeScript project.

Table of Contents

  1. Installation
  2. Concepts
  3. API
  4. Usage
  5. Examples
  6. Tuning Strategies
  7. Logging
  8. Extending Eptanaut
  9. FAQ

Installation

npm install eptanaut

Concepts

The eptanaut framework consists of the following 5 concepts.

Services

A Service consists of a TCP server bound to a specified port (usu. a public port) and a pool of spawned Worker threads that each bind an HTTP server to a random internal port. The TCP Server dispatches TCP connections to HTTP servers in the thread pool. The HTTP Server that receives a connection handles the HTTP request by passing the request to its routes. Spawned threads may run in parallel and will be reused.

Routes

Routes are connected to their respective service. A route evaluates if it will handle an HTTP request and returns true if it will handle the request and false if it will not. If it returns true no further routes will be tried. If a route returns false, the request will be passed to the next route handler. A route is a handler of type (req: http.IncomingMessage, res: http.ServerResponse) => Promise<boolean> | boolean, with an optional Metadata parameter. Routes come in 3 flavours: preroutes, routes, and postroutes.

Filters

A filter is a TCP Socket handler of type (socket: net.Socket) => Promise<boolean> | boolean that filters TCP Sockets. It returns true if the Socket is to be allowed else it returns false if it is to be refused.

Transformers

A transformer is a TCP Socket handler of type (socket: net.Socket) => Promise<net.Socket> | net.Socket that transforms TCP Sockets. It takes a net.Socket as an argument and returns a net.Socket. The net.Socket passed to the transformer is the socket shared by the client and the TCP proxy. A transformer could be used in order to transform the data stream or add a timeout to the net.Socket.

Metadata

A Metadata object can be mutated by routes in order to pass information between them e.g., a parsed URL. An example of mutating the Metadata object, in order to add a parsed URL, is provided in the Examples section. A new empty Metadata object is created for each request.

API

The Service Class

eptanaut.Service(options)

  • options <ServiceOptions & ServiceProxyOptions & ServiceServerOptions>

    • logLevel <'BASE'|'DEBUG'|'INFO'|'WARN'|'ERROR'> Optionally set eptanaut's log level. Default: INFO

    • maxThreads <number> Optional argument that specifies the maximum number of Worker threads permitted.

    • minThreads <number> Optional argument that specifies the minimum number of Worker threads permitted. Default: 0

    • path <string> Optional path to the .js file that contains your Service instance. Use fileURLToPath(import.meta.url) from node:url. Default: process.argv[1]

    • proxy <net.Server> A net.Server configured however you choose. Bind the server to a usu. public facing port using the Service.listen method. NB You must bind the epanaut.Service to a port using Service.listen in order for eptanaut to be aware of the binding.

    • proxyListenOptions <net.ListenOptions> Optional object that specifies a host and port that the TCP proxy will bind to e.g., { host:'0.0.0.0', port:3443 }.

    • proxyServerConnectOptions Omit<net.NetConnectOpts, 'port' | 'host' | 'path'> Optional argument that will be passed to options parameter of the net.createConnection function. Port, host, and path are omitted because the HTTP servers notify the proxy of these parameters once they bind.

    • server <http.Server> or <https.Server> A http.Server configured however you choose.

    • threadsCheckingInterval <number> Optional argument that specifies the approximate interval (milliseconds) at which inactive threads will be cleaned up. Default: 30000

A Service may alternatively be created using the createService helper function, which accepts the same arguments as the Service constructor.

service.listen(options)

  • options <net.ListenOptions>

    • port <number>

    • host <string>

service.preroute(route)

  • route <(req: http.IncomingMessage, res: http.ServerResponse, meta: T) => Promise<boolean> | boolean>

Preroutes are called before routes. Preroutes are called in the order they are added to the Service. If a preroute returns true no further preroutes will be tried. A preroute could be used for logging a request (i.e., http.IncomingMessage) or mutating the Metadata object. Multiple preroutes may be added to the Service. Preroutes may be added by either consecutively calling the Service.preroute method with a route function as an argument or by passing route functions as consecutive arguments to the Service.preroute method.

service.route(route)

  • route <(req: http.IncomingMessage, res: http.ServerResponse, meta: T) => Promise<boolean> | boolean>

Routes are called after preroutes and before postroutes. A route evaluates if it will handle an HTTP request and returns true if it will and false if it will not. If it returns true no further routes will be tried. Multiple routes may be added to the Service. Routes may be added by either consecutively calling the Service.route method with a route function as an argument or by passing route functions as consecutive arguments to the Service.route method.

service.postroute(route)

  • route <(req: http.IncomingMessage, res: http.ServerResponse, meta: T) => Promise<boolean> | boolean>

Postroutes are called after routes. Postroutes are called in the order they are added to the Service. If a postroute returns true no further postroutes will be tried. A postroute could be used for logging a response (i.e., http.ServerResponse). Multiple postroutes may be added to the Service. Postroutes may be added by either consecutively calling the Service.postroute method with a route function as an argument or by passing route functions as consecutive arguments to the Service.postroute method.

service.transform(transformer)

  • transform <(socket: net.Socket) => Promise<net.Socket> | net.Socket>

Transformers are called in the order they were added. Transformers are called after filtering. Multiple transformers may be added to the Service. Transformers may be added by either consecutively calling the Service.transform method with a transformer function as an argument or by passing transformer functions as consecutive arguments to the Service.transform method.

service.filter(filter)

  • filter <(socket: net.Socket) => Promise<boolean> | boolean>

Filters are called in the order they were added. Filters are called shortly after the net.Server 'connection' event. Stateful filters could, for example, be used for filtering TCP sockets. A filter returns true if the Socket is to be allowed else it returns false if it is to be denied. Multiple filters may be added to the Service. Filters may be added by either consecutively calling the Service.filter method with a filter function as an argument or by passing filter functions as consecutive arguments to the Service.filter method.

NB The Boolean semantics of routes and filters differ. A true value in the context of a route means that the request is fully handled by the route and routing stops. Conversely, a true value in the context of a filter, means that the net.Socket passed the rules of the filter and will be allowed if it passes all subsequent filters; if any one of the filters returns false then the net.Socket is denied and filtering ceases.

Usage

Install eptanaut and import the Service class or the createService helper function from the module into your index.js (or index.ts):

import { Service } from 'eptanaut';

or

import { createService } from 'eptanaut';

Examples

An instance of Hello World!. (example)

In this simple example you will implement a scalable Service using Node.js (no types) that responds with the message body "Hello World!" for requests to "http://example.com:3000/". A working example of this implementation can be found at https://github.com/faranalytics/eptanaut_hello_world. Please make sure that your firewall is configured to allow connections on port 3000 for this example to work.

index.js

import { createService } from 'eptanaut';
import * as http from 'node:http';
import * as net from 'node:net';

// Create a scalable Service that responds with the message body "Hello World!" for requests to "http://example.com:3000/" and bind the TCP proxy to port 3000.
const service = createService({
    proxy: net.createServer(), // Configure this TCP server however you choose.
    server: http.createServer(), // Configure this HTTP server however you choose.
    minThreads: 4,
    maxThreads: 42,
    threadsCheckingInterval: 4200
}).listen({ port: 3000, host: '0.0.0.0' });

service.route((req, res) => {
    if (req.url) {
        const url = new URL(req.url, `http://${req.headers.host}`);
        if (url.pathname == '/') {
            res.end('Hello World!');
            return true;
        }
    }
    return false;
});

An HTTP server that redirects to an HTTPS server. (example)

A working implementation of this example can be found on GitHub.

Don't block the event loop! (example)

You've heard it before: "Don't block the event loop!". In this example you're going to break the rules and do just that. In this example you will:

  • Run a scalable eptanaut Service that hosts an end-point named /dont-block-the-event-loop that blocks for about 100 milliseconds on each request.
  • Send the Service 100 concurrent requests, each with a payload of 1000 random bytes - as fast as you can.
  • Receive the same in return; this is an echo route.
  • Log the duration.
  • Do it again - now with a pool of 100 Workers spun up.
  • Log the duration.

A working implementation of this example can be found on GitHub.

Getting started. (example)

A simple template for building your TypeScript project on eptanaut.

Tuning Strategies

An eptanaut Service can be tuned by specifying a minimum and maximum number of allocated Worker threads. The minimum and maximum number of Worker threads can be specified in the constructor of each Service by assigning values to the minThreads and maxThreads parameters. Further, the threadsCheckingInterval can be used in order to set the frequency at which Workers are culled until the minThreads threshold is reached.

Service constructor parameters relevant to tuning:

eptanaut.Service(options)

  • options <ServiceProxyOptions>

    • minThreads <number> An argument that specifies the minimum number of Worker threads permitted.

    • maxThreads <number> An argument that specifies the maximum number of Worker threads permitted.

    • threadsCheckingInterval <number> An argument that specifies the approximate interval at which inactive threads will be cleaned up. Default: 30000

The minThreads argument specifies the minimum number of Worker threads permitted. minThreads threads will be instantiated when the eptanaut Service starts. Eptanaut will not allow the thread pool to drop below the specified threshold.

The maxThreads argument is a hard limit.

The threadsCheckingInterval specifies the approximate interval at which eptanaut will attempt to clean up inactive threads. If eptanaut's Proxy finds that a thread has 0 connections, eptanaut will remove it from the pool and send it a notification requesting that it close its server and exit. The default interval is 30000 milliseconds.

By variously specifying minThreads, maxThreads, threadsCheckingInterval you can tune eptanaut according to the requirements of your environment.

Logging

Eptanaut uses the Node.js memoir logging facility. You can set the log level in your index.js by passing a valid log level argument to the logLevel parameter of the Service constructor.

Eptanaut exports its instance of a memoir logger, named eptalog, which can be consumed and reconfigured by another memoir logger; see the memoir documentation for how to do this - or use the logger of your choice.

Extending Eptanaut

Extending eptanaut is straight-forward.

Packaging a preroute or postroute handler.

Eptanaut can be extended by publishing a package that provides a preroute (or postroute) handler that mutates the Metadata object in a way that may be useful to a downstream route. The mutated Metadata object will be passed to the route handlers.

This is an example of a simple extension that parses a URL and mutates the Metadata object.
service.preroute((req: http.IncomingMessage, res: http.ServerResponse, meta: Metadata) => {
    if (req.url) {
        meta.url = new URL(req.url, `https://${req.headers.host}`);
    }
    return false;
});

A handler extension may want to return false in order to indicate that it did not fully handle the preroute and pass it on to the next preroute handler. The subsequent handler may further mutate the Metadata object.

Subclassing Service.

A microservices design pattern could be implemented where each Service has a specific task.

FAQ

What is an eptanaut?

An eptanaut is an explorer, traveler, or navigator of Layer 7.

What kind of scaling implementation is this?

Eptanaut is a multithreaded vertical scaling implementation. However, eptanaut could be containerized and scaled horizontally.

1.2.0

8 months ago

1.2.2

8 months ago

1.2.1

8 months ago

1.0.2

9 months ago

1.0.1

9 months ago

1.0.0

9 months ago

0.1.10

9 months ago

0.1.11

9 months ago

0.1.12

9 months ago

0.1.13

9 months ago

0.1.14

9 months ago

0.1.15

9 months ago

1.0.3

9 months ago

0.0.30

10 months ago

0.0.31

10 months ago

0.0.32

10 months ago

0.0.33

10 months ago

0.0.34

10 months ago

0.1.0

10 months ago

0.1.2

10 months ago

0.1.1

10 months ago

0.1.8

10 months ago

0.0.26

10 months ago

0.1.7

10 months ago

0.0.27

10 months ago

0.0.28

10 months ago

0.1.9

9 months ago

0.0.29

10 months ago

0.1.4

10 months ago

0.1.3

10 months ago

0.1.6

10 months ago

0.1.5

10 months ago

1.1.1

9 months ago

1.1.0

9 months ago

0.0.20

10 months ago

0.0.21

10 months ago

0.0.22

10 months ago

0.0.23

10 months ago

0.0.24

10 months ago

0.0.25

10 months ago

0.0.15

10 months ago

0.0.16

10 months ago

0.0.17

10 months ago

0.0.18

10 months ago

0.0.19

10 months ago

0.1.20

9 months ago

0.1.21

9 months ago

0.1.22

9 months ago

0.0.10

10 months ago

0.0.11

10 months ago

0.0.12

10 months ago

0.0.13

10 months ago

0.0.14

10 months ago

0.1.16

9 months ago

0.0.9

10 months ago

0.1.17

9 months ago

0.0.8

10 months ago

0.1.18

9 months ago

0.1.19

9 months ago

0.0.7

10 months ago

0.0.6

10 months ago

0.0.5

10 months ago

0.0.4

10 months ago

0.0.3

10 months ago

0.0.2

10 months ago

0.0.1

10 months ago