Skip to content

Commit

Permalink
Retry implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
Acanguven committed May 8, 2019
1 parent 6e6e896 commit 227acfe
Show file tree
Hide file tree
Showing 13 changed files with 540 additions and 76 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [1.4.0] - 2019-05-08
### Added
- Retry plugin for retrying failed responses
- Added generic identifiers
### Changed
- Identifiers are not required anymore

## [1.3.0] - 2019-05-02
### Added
- Security for set-cookie header to prevent dangerous response caching with credentials.
Expand Down
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Warden is an outgoing request optimizer for creating fast and scalable applicati
- 🚧 **Request Holder** Stopping same request to be sent multiple times. ✅
- 🔌 **Support** Warden can be used with anything but it supports [request](https://github.com/request/request) out of the box. ✅
- 😎 **Easy Implementation** Warden can be easily implemented with a few lines of codes. ✅
- 🔁 **Request Retry** Requests will automatically be re-attempted on recoverable errors. 📝
- 🔁 **Request Retry** Requests will automatically be re-attempted on recoverable errors.
- 📇 **Schema Parser** Warden uses a schema which can be provided by you for parsing JSON faster. 📝
- 🚥 **API Queue** Throttles API calls to protect target service. 📝
- 👻 **Request Shadowing** Copies a fraction of traffic to a new deployment for observation. 📝
Expand All @@ -29,6 +29,7 @@ Warden is an outgoing request optimizer for creating fast and scalable applicati
- [Identifier](#identifier)
- [Registering Route](#registering-route)
- [Cache](#cache)
- [Retry](#retry)
- [Holder](#holder)
- [Api](#api)

Expand Down Expand Up @@ -122,6 +123,8 @@ warden.register('test', {
});
```

`identifier` is an optional field. If an identifier is not provided warden will be use generic identifier which is `${name}_${url}_${JSON.stringify({cookie, headers, query})}_${method}`.

### Cache

You can simply enable cache with default values using.
Expand Down Expand Up @@ -176,7 +179,36 @@ Simple old school caching. Asks cache plugin if it has a valid cached response.
### Holder

Holder prevents same HTTP requests to be sent at the same time.
Let's assume we have an identifier for a request: `{query.foo}`. We send a HTTP request `/product?foo=bar`. While waiting for the response, warden received another HTTP request to the same address which means both HTTP requests are converted to the same key. Then Warden stops the second request. After receiving the response from the first request, Warden returns both requests with the same response by sending only one HTTP request.
Let's assume we have an identifier for a request: `{query.foo}`. We send a HTTP request `/product?foo=bar`. While waiting for the response, warden received another HTTP request to the same address which means both HTTP requests are converted to the same key. Then Warden stops the second request. After receiving the response from the first request, Warden returns both requests with the same response by sending only one HTTP request.

### Retry

When the connection fails with one of ECONNRESET, ENOTFOUND, ESOCKETTIMEDOUT, ETIMEDOUT, ECONNREFUSED, EHOSTUNREACH, EPIPE, EAI_AGAIN or when an HTTP 5xx or 429 error occurrs, the request will automatically be re-attempted as these are often recoverable errors and will go away on retry.

```js
warden.register('routeName', {
retry: {
delay: 100,
count: 1,
logger: (retryCount) => {
console.log(retryCount);
}
}
});

warden.register('routeName', {
retry: true // default settings
});
```

Default values and properties

| Property | Required | Default Value | Definition |
| :--- | :---: | ---: | :--- |
| delay || 100 | Warden will wait for 100ms before retry |
| count || 1 | It will try for 1 time by default |
| logger || 1m | Logger will be called on each retry with retry count|


### Api

Expand Down
9 changes: 0 additions & 9 deletions demo.ts

This file was deleted.

9 changes: 4 additions & 5 deletions src/cache-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,10 @@ class CacheFactory {
}

getPlugin(plugin?: CACHE_PLUGIN): CachePlugin {
switch (plugin) {
case CACHE_PLUGIN.Memory:
return new MemoryCache();
default:
return new MemoryCache();
if (plugin === CACHE_PLUGIN.Memory) {
return new MemoryCache();
} else {
return new MemoryCache();
}
}

Expand Down
17 changes: 11 additions & 6 deletions src/request-manager.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {StreamHead} from "./stream-head";
import {KeyMaker, Tokenizer} from "./tokenizer";
import * as request from "request";
import {RequestCallback} from "request";
import Url from "fast-url-parser";
import {ConfigurableStream, StreamFactory, StreamType} from "./stream-factory";
import {ConfigurableStream, StreamFactory} from "./stream-factory";
import {CacheConfiguration} from "./cache-factory";
import {WardenStream} from "./warden-stream";
import Cookie from "cookie";
import {Network} from "./network";
import * as request from "request";
import {RetryInputConfiguration} from "./retry";
import http from "http";

type KeyStreamPair = {
keyMaker: KeyMaker;
Expand All @@ -18,6 +19,8 @@ interface StreamMap {
[routeName: string]: KeyStreamPair[];
}

const DEFAULT_IDENTIFIER = `u_{Date.now()}`;

interface RequestOptions extends request.CoreOptions {
url: string;
method: 'get' | 'post';
Expand All @@ -30,9 +33,10 @@ interface RequestOptions extends request.CoreOptions {
interface RouteConfiguration {
[key: string]: any;

identifier: string;
identifier?: string;
cache?: CacheConfiguration | boolean;
holder?: boolean;
retry?: RetryInputConfiguration | boolean | number;
}

class RequestManager {
Expand All @@ -49,9 +53,9 @@ class RequestManager {
}

register(name: string, routeConfiguration: RouteConfiguration) {
const stream = this.streamFactory.create<StreamHead>(StreamType.HEAD);
const stream = this.streamFactory.createHead();
let streamLink: WardenStream = stream;
const network = this.streamFactory.create<Network>(StreamType.NETWORK);
const network = this.streamFactory.createNetwork();
const keyMaker = this.tokenizer.tokenize(name, routeConfiguration.identifier);

Object.values(ConfigurableStream).forEach((streamType: string) => {
Expand Down Expand Up @@ -100,6 +104,7 @@ class RequestManager {
}

export {
DEFAULT_IDENTIFIER,
RequestOptions,
RouteConfiguration,
RequestManager
Expand Down
114 changes: 92 additions & 22 deletions src/retry.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,113 @@
import {RequestChunk, ResponseChunk, WardenStream} from "./warden-stream";
import {TransformCallback} from "stream";
import {StreamType} from "./stream-factory";

interface RetryDecoratedResponse extends ResponseChunk {
retryCount: number;
retryCount: number;
}

interface RetryDecoratedRequest extends RequestChunk {
retryCount: number;
retryCount: number;
}

interface RetryConfiguration {
count: number;
delay: number;
logger?: (retry: number) => void;
}

interface RetryInputConfiguration {
count?: number;
delay?: number;
logger?: (retry: number) => void;
}

const DEFAULT_RETRY_CONFIGURATION = {
count: 1,
delay: 100,
};

const RETRYABLE_ERRORS = ['ECONNRESET', 'ENOTFOUND', 'ESOCKETTIMEDOUT', 'ETIMEDOUT', 'ECONNREFUSED', 'EHOSTUNREACH', 'EPIPE', 'EAI_AGAIN'];

class Retry extends WardenStream {
retryLimit: number;
private configuration: RetryConfiguration;

constructor(configuration: RetryConfiguration) {
super(StreamType.RETRY);

constructor(retryLimit: number) {
super('Retry');
this.configuration = configuration;
}

this.retryLimit = retryLimit;
static create(configuration: RetryInputConfiguration | true | number) {
if (typeof configuration === "boolean") {
return new Retry(DEFAULT_RETRY_CONFIGURATION);
} else if (typeof configuration === "number") {
return new Retry({
count: configuration,
delay: 0
});
} else if (typeof configuration === "object") {
const count = configuration.count || 1;
const delay = configuration.delay || 0;
return new Retry({
count,
delay,
logger: configuration.logger
});
} else {
return new Retry(DEFAULT_RETRY_CONFIGURATION);
}
}

async onResponse(chunk: RetryDecoratedResponse, callback: TransformCallback): Promise<void> {
if (chunk.error && chunk.retryCount <= this.retryLimit) {
this.request<RetryDecoratedRequest>({
...chunk,
retryCount: chunk.retryCount + 1
});
callback(undefined, null);
} else {
callback(undefined, chunk);
}
async onResponse(chunk: RetryDecoratedResponse, callback: TransformCallback): Promise<void> {
if (chunk.retryCount <= this.configuration.count && this.shouldRetry(chunk)) {
if (this.configuration.delay > 0) {
setTimeout(() => {
this.retry(chunk);
}, this.configuration.delay);
} else {
this.retry(chunk);
}
callback(undefined, null);
} else {
callback(undefined, chunk);
}
}

async onRequest(chunk: RetryDecoratedRequest, callback: TransformCallback): Promise<void> {
callback(null, {
...chunk,
retryCount: 0
} as RequestChunk);
async onRequest(chunk: RetryDecoratedRequest, callback: TransformCallback): Promise<void> {
callback(null, {
...chunk,
retryCount: 0
} as RequestChunk);
}

private retry(chunk: RetryDecoratedResponse) {
if (this.configuration.logger) this.configuration.logger(chunk.retryCount + 1);
this.request<RetryDecoratedRequest>({
retryCount: chunk.retryCount + 1,
cb: chunk.cb,
key: chunk.key,
requestOptions: chunk.requestOptions
});
}

private shouldRetry(chunk: RetryDecoratedResponse) {
const statusCode = chunk.response ? chunk.response.statusCode : null;

if (statusCode && (statusCode === 429 || (500 <= statusCode && statusCode < 600))) {
return true;
}

if (chunk.error) {
return RETRYABLE_ERRORS.includes(chunk.error.code);
}

return false;
}
}

export {
Retry
RetryInputConfiguration,
RetryConfiguration,
Retry
};
16 changes: 10 additions & 6 deletions src/stream-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,28 @@ class StreamFactory {
this.requestWrapper = requestWrapper;
}

create<U, T = {}>(streamType: string, configuration?: T) {
create<U, T = {}>(streamType: string, configuration: T) {
switch (streamType) {
case StreamType.CACHE:
return this.cacheFactory.create(configuration as T) as unknown as U;
case StreamType.HOLDER:
return new Holder() as unknown as U;
// case StreamType.CIRCUIT:
// throw new Error('Not implemented');
case StreamType.NETWORK:
return new Network(this.requestWrapper) as unknown as U;
case StreamType.RETRY:
return new Retry(3) as unknown as U;
case StreamType.HEAD:
return new StreamHead() as unknown as U;
return Retry.create(configuration) as unknown as U;
default:
throw new Error('Unknown stream type');
}
}

createNetwork() {
return new Network(this.requestWrapper);
}

createHead() {
return new StreamHead();
}
}

export {
Expand Down
12 changes: 11 additions & 1 deletion src/tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ type KeyMaker = (


class Tokenizer {
tokenize(name: string, identifier: string): KeyMaker {
tokenize(name: string, identifier?: string): KeyMaker {
return identifier ? this.generateCustomIdentifier(name, identifier) : this.createGenericIdentifier(name);
}

private generateCustomIdentifier(name: string, identifier: string) {
const reversedIdentifier = reverseString(identifier);
const interpolationsAdded = reversedIdentifier
.replace(/{(?!\\)/g, `{$`)
Expand All @@ -23,6 +27,12 @@ class Tokenizer {

return fn();
}

private createGenericIdentifier(name: string): KeyMaker {
return (url, cookie, headers, query, method) => {
return `${name}_${url}_${JSON.stringify({cookie, headers, query})}_${method}`;
};
}
}

export {
Expand Down
2 changes: 1 addition & 1 deletion src/warden-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface RequestChunk {
interface ResponseChunk extends RequestChunk{
response?: request.Response;
error?: {
name: string
code: string
};
}

Expand Down
Loading

0 comments on commit 227acfe

Please sign in to comment.