Skip to content

Commit

Permalink
feature: totally new api
Browse files Browse the repository at this point in the history
* simpler API
* no more multiple-events-per action
* simpler event schema
* simpler and more consistent terminology (no more cute ping/pong etc.)
  • Loading branch information
jasonkuhrt authored Oct 17, 2017
1 parent effb885 commit 2fd5181
Show file tree
Hide file tree
Showing 7 changed files with 4,706 additions and 241 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
language: node_js
node_js:
- "6"
- "8"
142 changes: 107 additions & 35 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,74 +9,146 @@ image:https://travis-ci.org/jasonkuhrt/uri-monitor.svg?branch=master["Build Stat
toc::[]



## Installation

```
npm install --save uri-monitor
yarn add uri-monitor
```

## About

This is a tiny library that makes it trivial to monitor the results of repeatedly executing an asynchronous action. A canonical use-case would be monitoring a URL for healthy (e.g. status code 200) responses but really it could be anything, including actions that don't do any IO but still have something to monitor such random computation. This is a fairly general tool and it interestingly generalizes some things that previously would have been realized with purpose-built code. For example the concept of request-retry can be generally implemented with this library, see the example below.

## Examples

#### Declarative Request-Retry

We can combine streams together to declaratively create request-retry logic. Inspiration for this example was taken from the imperative request-retry logic in https://github.com/elastic/elasticsearch-js/blob/master/test/integration/yaml_suite/client_manager.js#L30-L42[this ElasticSearch test suite] which establishes the connectivity with the database before running its test suite.

```js
const runTests = () =>
Promise.resolve("Your integration test suite here...")

const pingElasticSearch = () =>
Boolean(Math.round(Math.random()))
? Promise.reject(new Error("Foobar network error"))
: Promise.resolve("OK!")

const pingInterval = 1000
const pingAttempts = 3
const monitor = Monitor.create(pingElasticSearch, pingInterval)
const maxRetries = monitor.downs.takeUntil(monitor.ups).take(pingAttempts)

monitor
.ups
.take(1)
.takeUntil(maxRetries)
.drain()
.then((result) =>
result.isResponsive
? runTests()
: Promise.reject(result.data)
))
```

## API

### Monitor.create
### Types

#### MonitorEvent

```js
type MonitorEvent<A> = {
isResponsive: boolean,
isResponsiveChanged: boolean,
data: A | null
error: Error | null
}
```

A monitor event represents the result of an action invocation. The type variable `A` represents the action's resolution type.

`isResponsive` indicates if the action is resolving while `isResponsiveChanged` indicates if that state is different than the previous event. For the first check wherein there is no such previous event `isResponsiveChanged` is considered to be `true`.

You can think of `isResponsive` as being your health indicator while `isResponsiveChanged` as being your alert to when crashing or recovery occurs. `isResponsiveChanged` is technically a convenience that you could calculate yourself but seeing as its often needed and that its calculation requires state it seems worthwhile.

### .create

```js
Monitor.create(action : () => Promise<A>, intervalMs : number?): Observable<MonitorEvent<A>>
Monitor.create(action: () => Promise<A>, intervalMs: number?) => Observable<MonitorEvent<A>>
```

* `action` The async action to recurse. The expected usage is that you are somehow pinging the resource you are interested in monitoring.
* `action` The async action to recurse. This could be anything: HTTP requests, pure calculation, file system checks...
* `intervalMs` The milliseconds between each `action` invocation. Defaults to `1000`.
* `returns` An observable stream of monitor events. The observable implementation we use is https://github.com/cujojs/most[`most`].
* `returns` An observable stream of `MonitorEvent`. The observable implementation we use is https://github.com/cujojs/most[`most`].
### Event State Helpers

There are several statically exported predicate functions that can tell you what kind of state a `MonitorEvent` is in.

### MonitorEvent<A>
##### .isUp

Every application of `action` returns a regular promise which ultimately resolves or rejects. In the context of a stream of such values, it is useful to have various terms to refer to different states and patterns of state change:
```js
Monitor.isUp(event: MonitorEvent) => boolean
```

|===
| Event Name | Case
| `pong` | promise resolved
| `drop` | promise rejected
| `check` | case of `pong` or `drop`
| `up` | promise resolved and: previous attempt rejected or was nothing (initial check)
| `down` | promise rejected and: previous attempt resolved or was nothing (initial case)
| `change` | case of `up` or `down`
|===
Returns `true` if event `isResponsive` is `true`.

##### .isDown

With this in mind, the structure of a `MonitorEvent` is:
```js
Monitor.isDown(event: MonitorEvent) => boolean
```

Returns `true` if event `isResponsive` is `false`.

##### .isRise

```js
type MonitorEvent<A> = {
type : string,
data : {
isResponsive : boolean,
result : A,
}
}
Monitor.isRise(event: MonitorEvent) => boolean
```

And the value of `type` is any of the aforementioned event names.
Returns `true` if event `isResponsive` is `true` and `isResponsiveChanged` is `true`.

##### .isFall

```js
Monitor.isFall(event: MonitorEvent) => boolean
```

Returns `true` if event `isResponsive` is `false` and `isResponsiveChanged` is `true`.

##### .isChanges

```js
Monitor.isChanges(event: MonitorEvent) => boolean
```

Returns `true` if event `isResponsiveChanged` is `true`.


### Sub-streams

There are several additional streams on monitor instances that are filtered by event state predicates. These sub-streams provide a convenient way to consume just a subset of events without any additional code/work from you.

`result` is the settled value of the promise returned from `action`.
##### #ups

### monitor.<substream>
Stream filtered by `Monitor.isUp`

The observable stream returned by <<create, `create`>> contains **all** events. For example on the first monitor check four events will occur: `check`, `drop | pong`, `change`, `up | down`. But often this is more noise than you want and while it is easy enough to filter the stream (`monitor.filter(...)`) there is an easier way. Each event type has a corresponding property on the main stream whose value is a filtered stream of _just that event_:
##### #downs

Stream filtered by `Monitor.isDown`

##### monitor.checks
##### #rises

##### monitor.pongs
Stream filtered by `Monitor.isRise`

##### monitor.drops
##### #falls

##### monitor.ups
Stream filtered by `Monitor.isFall`

##### monitor.downs
##### #changes

##### monitor.changes
Stream filtered by `Monitor.isChange`
14 changes: 14 additions & 0 deletions examples/request-retry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Monitor from "../source/main"

const action = () =>
Math.round(Math.random())
? Promise.reject(new Error("This bad thing happened"))
: Promise.resolve("OK!")

const monitor = Monitor.create(action, 1000)

monitor.ups
.takeUntil(monitor.downs.takeUntil(monitor.ups).take(3))
.take(1)
.drain()
.then(console.log.bind(null, "Finished awaiting availability with result: "))
24 changes: 15 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
"license": "MIT",
"scripts": {
"test": "jest",
"test-dev": "jest --watch",
"test:dev": "jest --watch",
"build": "rm -rf build && babel --out-dir build source",
"postversion": "git push && git push --tags && npm publish",
"prepublish": "npm run build"
"postversion": "git push && git push --tags && npm publish && release",
"prepublish": "yarn run build",
"examples:request-retry": "yarn babel-node examples/request-retry.js"
},
"dependencies": {
"most": "^1.1.1"
"most": "^1.7.2"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -38,17 +39,18 @@
"babel-preset-es2015": "^6.3.13",
"babel-register": "^6.3.13",
"bluebird": "^3.4.6",
"chai": "^3.4.1",
"chai": "^4.1.2",
"eslint": "^3.12.0",
"eslint-config-littlebits": "^0.5.1",
"jest-cli": "^18.1.0",
"ramda": "^0.22.1"
"jest-cli": "^21.2.1",
"ramda": "^0.25.0",
"release": "^2.5.2"
},
"eslintConfig": {
"extends": [
"littlebits"
],
"env": {
"env": {
"jest": true
}
},
Expand All @@ -61,6 +63,10 @@
"url": "https://github.com/jasonkuhrt/uri-monitor/issues"
},
"jest": {
"testEnvironment": "node"
"testEnvironment": "node",
"testPathIgnorePatterns": [
"/node_modules/",
"/build/"
]
}
}
Loading

0 comments on commit 2fd5181

Please sign in to comment.