| 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 |