eptanaut v1.2.2
eptánaut
A scalable multithreaded Node.js HTTP server.
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: - Thefarar/memoir
logger. - Thefarar/port_agent
RPC facility. - The eptanaut
Service
constructor consumes native Nodenet.Server
andhttp.Server
instances; you can configure them however you choose. - The
http.IncomingMessage
andhttp.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
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. UsefileURLToPath(import.meta.url)
fromnode:url
. Default:process.argv[1]
proxy
<net.Server>
Anet.Server
configured however you choose. Bind the server to a usu. public facing port using theService.listen
method. NB You must bind theepanaut.Service
to a port usingService.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 tooptions
parameter of thenet.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>
Ahttp.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, atrue
value in the context of a filter, means that thenet.Socket
passed the rules of the filter and will be allowed if it passes all subsequent filters; if any one of the filters returnsfalse
then thenet.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.
8 months ago
8 months ago
8 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
9 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
9 months ago
9 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
9 months ago
9 months ago
9 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
9 months ago
10 months ago
9 months ago
10 months ago
9 months ago
9 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago