| OLD | NEW |
| (Empty) |
| 1 /* | |
| 2 Copyright 2016 The LUCI Authors. All rights reserved. | |
| 3 Use of this source code is governed under the Apache License, Version 2.0 | |
| 4 that can be found in the LICENSE file. | |
| 5 */ | |
| 6 | |
| 7 import * as luci_sleep_promise from "luci-sleep-promise/promise"; | |
| 8 | |
| 9 export namespace luci_rpc { | |
| 10 | |
| 11 interface PolymerClient { | |
| 12 service: string; | |
| 13 method: string; | |
| 14 request: any; | |
| 15 | |
| 16 call(): { | |
| 17 completes: Promise<any>, | |
| 18 }; | |
| 19 } | |
| 20 | |
| 21 export class Client { | |
| 22 transientRetry: Retry = new Retry(new RetryIterator(10, 500, null)); | |
| 23 private pc: PolymerClient; | |
| 24 | |
| 25 constructor(pc: any) { | |
| 26 this.pc = pc as PolymerClient; | |
| 27 } | |
| 28 | |
| 29 /** Call invokes the specified service's method, returning a Promise. */ | |
| 30 call<T, R>(service: string, method: string, request?: T): Promise<R> { | |
| 31 this.pc.service = service; | |
| 32 this.pc.method = method; | |
| 33 this.pc.request = request; | |
| 34 | |
| 35 let transientRetry = this.transientRetry.iterator(); | |
| 36 let doCall = (): Promise<R> => { | |
| 37 let callPromise: Promise<R> = this.pc.call().completes; | |
| 38 return callPromise.then( (resp: any) => { | |
| 39 return resp.response; | |
| 40 }).catch( (err: Error) => { | |
| 41 // Is this a transient error? | |
| 42 if ( isTransientError(err) ) { | |
| 43 let delay = transientRetry.next(); | |
| 44 if ( delay ) { | |
| 45 console.warn( | |
| 46 `Transient error calling ${service}.${method} with params:`, | |
| 47 request, `:`, err, `; retrying after ${delay}ms.`); | |
| 48 return luci_sleep_promise.sleep(delay).then( () => { | |
| 49 return doCall(); | |
| 50 }); | |
| 51 } | |
| 52 } | |
| 53 | |
| 54 // Non-transient, throw the error. | |
| 55 throw err; | |
| 56 }); | |
| 57 }; | |
| 58 return doCall(); | |
| 59 } | |
| 60 } | |
| 61 | |
| 62 /** gRPC Codes */ | |
| 63 export enum Code { | |
| 64 OK = 0, | |
| 65 CANCELED = 1, | |
| 66 UNKNOWN = 2, | |
| 67 INVALID_ARGUMENT = 3, | |
| 68 DEADLINE_EXCEEDED = 4, | |
| 69 NOT_FOUND = 5, | |
| 70 ALREADY_EXISTS = 6, | |
| 71 PERMISSION_DENIED = 7, | |
| 72 UNAUTHENTICATED = 16, | |
| 73 RESOURCE_EXHAUSTED = 8, | |
| 74 FAILED_PRECONDITION = 9, | |
| 75 ABORTED = 10, | |
| 76 OUT_OF_RANGE = 11, | |
| 77 UNIMPLEMENTED = 12, | |
| 78 INTERNAL = 13, | |
| 79 UNAVAILABLE = 14, | |
| 80 DATA_LOSS = 15 | |
| 81 } | |
| 82 | |
| 83 export class GrpcError extends Error { | |
| 84 constructor(readonly code: Code, readonly description?: string) { | |
| 85 super("code = " + code + ", desc = " + description); | |
| 86 } | |
| 87 | |
| 88 /** | |
| 89 * Converts the supplied Error into a GrpcError if its name is | |
| 90 * "GrpcError". This merges between the non-Typescript RPC code and this | |
| 91 * error type. | |
| 92 */ | |
| 93 static convert(err: Error): GrpcError | null { | |
| 94 if ( err.name === 'GrpcError' ) { | |
| 95 let aerr = err as GrpcError; | |
| 96 return new GrpcError(aerr.code, aerr.description); | |
| 97 } | |
| 98 return null; | |
| 99 } | |
| 100 | |
| 101 /** Returns true if the error is considered transient. */ | |
| 102 get transient(): boolean { | |
| 103 switch ( this.code ) { | |
| 104 case Code.INTERNAL: | |
| 105 case Code.UNAVAILABLE: | |
| 106 case Code.RESOURCE_EXHAUSTED: | |
| 107 return true; | |
| 108 | |
| 109 default: | |
| 110 return false; | |
| 111 } | |
| 112 } | |
| 113 } | |
| 114 | |
| 115 export class HttpError extends Error { | |
| 116 constructor(readonly code: Code, readonly description?: string) { | |
| 117 super("code = " + code + ", desc = " + description); | |
| 118 } | |
| 119 | |
| 120 /** | |
| 121 * Converts the supplied Error into a HttpError if its name is | |
| 122 * "HttpError". This merges between the non-Typescript RPC code and this | |
| 123 * error type. | |
| 124 */ | |
| 125 static convert(err: Error): HttpError | null { | |
| 126 if ( err.name === 'HttpError' ) { | |
| 127 let aerr = err as HttpError; | |
| 128 return new HttpError(aerr.code, aerr.description); | |
| 129 } | |
| 130 return null; | |
| 131 } | |
| 132 | |
| 133 /** Returns true if the error is considered transient. */ | |
| 134 get transient(): boolean { return ( this.code >= 500 ); } | |
| 135 } | |
| 136 | |
| 137 export function isTransientError(err: Error): boolean { | |
| 138 let grpc = GrpcError.convert(err); | |
| 139 if ( grpc ) { | |
| 140 return grpc.transient; | |
| 141 } | |
| 142 | |
| 143 // Is this an HTTP Error? | |
| 144 let http = HttpError.convert(err); | |
| 145 if ( http ) { | |
| 146 return http.transient; | |
| 147 } | |
| 148 | |
| 149 // Unknown error. | |
| 150 return false; | |
| 151 } | |
| 152 | |
| 153 export class RetryIterator { | |
| 154 private retries: number | null; | |
| 155 private delay: number; | |
| 156 private maxDelay: number | null; | |
| 157 | |
| 158 constructor(retries: number | null, delay: number, | |
| 159 maxDelay: number | null) { | |
| 160 | |
| 161 this.retries = retries; | |
| 162 this.delay = delay; | |
| 163 this.maxDelay = maxDelay; | |
| 164 } | |
| 165 | |
| 166 clone(): RetryIterator { | |
| 167 return new RetryIterator(this.retries, this.delay, this.maxDelay); | |
| 168 } | |
| 169 | |
| 170 next(): number | null { | |
| 171 if ( this.retries !== null ) { | |
| 172 if ( this.retries <= 0 ) { | |
| 173 // No more retries remaining. | |
| 174 return null; | |
| 175 } | |
| 176 this.retries--; | |
| 177 } | |
| 178 | |
| 179 let delay = this.delay; | |
| 180 this.delay *= 2; | |
| 181 if ( this.maxDelay !== null && this.delay > this.maxDelay ) { | |
| 182 this.delay = this.maxDelay; | |
| 183 } | |
| 184 return delay; | |
| 185 } | |
| 186 } | |
| 187 | |
| 188 export class Retry { | |
| 189 private base: RetryIterator; | |
| 190 | |
| 191 constructor(base: RetryIterator) { | |
| 192 this.base = base; | |
| 193 } | |
| 194 | |
| 195 iterator(): RetryIterator { | |
| 196 return this.base.clone(); | |
| 197 } | |
| 198 } | |
| 199 | |
| 200 } | |
| OLD | NEW |