mirror of
https://github.com/urbit/shrub.git
synced 2024-12-21 01:41:37 +03:00
interface: cleaner API, robust GcpManager retry
GcpApi now acts like other APIs. Since GcpManager can no longer get at the token exipry by inspecting the raw update, it must depend on the global store instead. This also means it can check whether the user has configured S3, and not try to refresh the token in that case. In the case where no storage is configured, this will spam the console with request failures since the thread returns 500 if there is no token. Perhaps this is a good argument for making the thread return a unit.
This commit is contained in:
parent
723a5a050e
commit
347d51fde9
@ -4,20 +4,12 @@ import {GcpToken} from '../types/gcp-state';
|
|||||||
|
|
||||||
|
|
||||||
export default class GcpApi extends BaseApi<StoreState> {
|
export default class GcpApi extends BaseApi<StoreState> {
|
||||||
// Return value resolves to the token's expiry time if successful.
|
|
||||||
refreshToken() {
|
refreshToken() {
|
||||||
return this.spider('noun', 'gcp-token', 'get-gcp-token', {})
|
return this.spider('noun', 'gcp-token', 'get-gcp-token', {})
|
||||||
.then((token) => {
|
.then((token) => {
|
||||||
this.store.handleEvent({
|
this.store.handleEvent({
|
||||||
data: token
|
data: token
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof(token) === 'object' &&
|
|
||||||
typeof(token['gcp-token']) === 'object' &&
|
|
||||||
typeof(token['gcp-token']['expiresIn']) === 'number') {
|
|
||||||
return Promise.resolve(token['gcp-token']['expiresIn']);
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error("invalid token"));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -2,18 +2,27 @@
|
|||||||
//
|
//
|
||||||
// To use:
|
// To use:
|
||||||
//
|
//
|
||||||
// 1. call setApi with a GlobalApi.
|
// 1. call configure with a GlobalApi and GlobalStore.
|
||||||
// 2. call start() to start the token refresh loop.
|
// 2. call start() to start the token refresh loop.
|
||||||
//
|
//
|
||||||
|
// If the ship has S3 credentials set, we don't try to get a token, but we keep
|
||||||
|
// checking at regular intervals to see if they get unset. Otherwise, we try to
|
||||||
|
// invoke the GCP token thread on the ship until it gives us an access token.
|
||||||
|
// Once we have a token, we refresh it every hour or so, since it has an
|
||||||
|
// intrinsic expiry.
|
||||||
|
//
|
||||||
//
|
//
|
||||||
import GlobalApi from '../api/global';
|
import GlobalApi from '../api/global';
|
||||||
|
import GlobalStore from '../store/store';
|
||||||
|
|
||||||
|
|
||||||
class GcpManager {
|
class GcpManager {
|
||||||
#api: GlobalApi | null = null;
|
#api: GlobalApi | null = null;
|
||||||
|
#store: GlobalStore | null = null;
|
||||||
|
|
||||||
setApi(api: GlobalApi) {
|
configure(api: GlobalApi, store: GlobalStore) {
|
||||||
this.#api = api;
|
this.#api = api;
|
||||||
|
this.#store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
#running = false;
|
#running = false;
|
||||||
@ -24,8 +33,8 @@ class GcpManager {
|
|||||||
console.warn('GcpManager already running');
|
console.warn('GcpManager already running');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.#api) {
|
if (!this.#api || !this.#store) {
|
||||||
console.error('GcpManager must have api set');
|
console.error('GcpManager must have api and store set');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.#running = true;
|
this.#running = true;
|
||||||
@ -51,17 +60,37 @@ class GcpManager {
|
|||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#consecutiveFailures: number = 0;
|
||||||
|
|
||||||
private refreshLoop() {
|
private refreshLoop() {
|
||||||
|
const s3 = this.#store.state.s3;
|
||||||
|
// XX ships currently always have S3 credentials, but the fields are all
|
||||||
|
// set to '' if they are not configured.
|
||||||
|
if (s3 &&
|
||||||
|
s3.credentials &&
|
||||||
|
s3.credentials.accessKeyId &&
|
||||||
|
s3.credentials.secretAccessKey) {
|
||||||
|
// do nothing, and check again in 5s.
|
||||||
|
this.refreshAfter(5_000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.#api.gcp.refreshToken()
|
this.#api.gcp.refreshToken()
|
||||||
.then(
|
.then(() => {
|
||||||
(expiresIn: number) => {
|
const token = this.#store.state.gcp?.token;
|
||||||
this.refreshAfter(this.refreshInterval(expiresIn));
|
if (token) {
|
||||||
})
|
this.#consecutiveFailures = 0;
|
||||||
.catch(
|
const interval = this.refreshInterval(token.expiresIn);
|
||||||
(reason) => {
|
console.log('GcpManager got token; refreshing after', interval);
|
||||||
console.error('GcpManager token refresh failed', reason);
|
this.refreshAfter(interval);
|
||||||
this.refreshAfter(30_000); // XX backoff?
|
} else {
|
||||||
});
|
throw new Error('thread succeeded, but returned no token?');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((reason) => {
|
||||||
|
this.#consecutiveFailures++;
|
||||||
|
console.warn('GcpManager refresh failed; retrying with backoff');
|
||||||
|
this.refreshAfter(this.backoffInterval());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private refreshAfter(durationMs) {
|
private refreshAfter(durationMs) {
|
||||||
@ -71,7 +100,6 @@ class GcpManager {
|
|||||||
console.warn('GcpManager already has a timeout set');
|
console.warn('GcpManager already has a timeout set');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('GcpManager refreshing after', durationMs, 'ms');
|
|
||||||
this.#timeoutId = setTimeout(() => {
|
this.#timeoutId = setTimeout(() => {
|
||||||
this.#timeoutId = null;
|
this.#timeoutId = null;
|
||||||
this.refreshLoop();
|
this.refreshLoop();
|
||||||
@ -79,10 +107,18 @@ class GcpManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private refreshInterval(expiresIn: number) {
|
private refreshInterval(expiresIn: number) {
|
||||||
// Give ourselves 30 seconds for processing delays, but never refresh
|
// Give ourselves a minute for processing delays, but never refresh sooner
|
||||||
// sooner than 30 minutes from now. (The expiry window should be about an
|
// than 30 minutes from now. (The expiry window should be about an hour.)
|
||||||
// hour.)
|
return Math.max(30 * 60_000, expiresIn - 60_000);
|
||||||
return Math.max(30 * 60_000, expiresIn - 30_000);
|
}
|
||||||
|
|
||||||
|
private backoffInterval() {
|
||||||
|
// exponential backoff.
|
||||||
|
const slotMs = 5_000;
|
||||||
|
const maxSlot = 60; // 5 minutes
|
||||||
|
const backoffSlots =
|
||||||
|
Math.floor(Math.random() * Math.min(maxSlot, this.#consecutiveFailures));
|
||||||
|
return slotMs * backoffSlots;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,14 +8,18 @@ export default class GcpReducer<S extends GcpState>{
|
|||||||
reduce(json: Cage, state: S) {
|
reduce(json: Cage, state: S) {
|
||||||
let data = json['gcp-token'];
|
let data = json['gcp-token'];
|
||||||
if (data) {
|
if (data) {
|
||||||
this.setAccessKey(data, state);
|
this.setToken(data, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setAccessKey(json: GcpToken, state: S) {
|
setToken(data: any, state: S) {
|
||||||
const data = _.get(json, 'accessKey');
|
if (this.isToken(data)) {
|
||||||
if (data) {
|
state.gcp.token = data;
|
||||||
state.gcp.accessKey = data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isToken(token: any): token is GcpToken {
|
||||||
|
return (typeof(token.accessKey) === 'string' &&
|
||||||
|
typeof(token.expiresIn) === 'number');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,5 +4,5 @@ export interface GcpToken {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface GcpState {
|
export interface GcpState {
|
||||||
accessKey?: string;
|
token?: GcpToken
|
||||||
};
|
};
|
||||||
|
@ -79,7 +79,7 @@ class App extends React.Component {
|
|||||||
|
|
||||||
this.appChannel = new window.channel();
|
this.appChannel = new window.channel();
|
||||||
this.api = new GlobalApi(this.ship, this.appChannel, this.store);
|
this.api = new GlobalApi(this.ship, this.appChannel, this.store);
|
||||||
gcpManager.setApi(this.api);
|
gcpManager.configure(this.api, this.store);
|
||||||
this.subscription =
|
this.subscription =
|
||||||
new GlobalSubscription(this.store, this.api, this.appChannel);
|
new GlobalSubscription(this.store, this.api, this.appChannel);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user