Skip to content

Commit

Permalink
feat: allow multiple attempts to fail before caching a negative result
Browse files Browse the repository at this point in the history
  • Loading branch information
roncohen committed Jun 19, 2024
1 parent 79ee9bd commit 751211a
Show file tree
Hide file tree
Showing 2 changed files with 30 additions and 6 deletions.
15 changes: 14 additions & 1 deletion packages/tracking-sdk/src/flags-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface cacheEntry {
staleAt: number;
success: boolean; // we also want to cache failures to avoid the UI waiting and spamming the API
flags: Flags | undefined;
attemptCount: number;
}

export function validateFlags(flagsInput: any): Flags | undefined {
Expand All @@ -35,6 +36,7 @@ export interface CacheResult {
flags: Flags | undefined;
stale: boolean;
success: boolean;
attemptCount: number;
}

export class FlagCache {
Expand All @@ -48,7 +50,14 @@ export class FlagCache {
this.expireTimeMs = expireTimeMs;
}

set(key: string, success: boolean, flags?: Flags) {
set(
key: string,
{
success,
flags,
attemptCount,
}: { success: boolean; flags?: Flags; attemptCount: number },
) {
let cacheData: CacheData = {};

try {
Expand All @@ -65,6 +74,7 @@ export class FlagCache {
staleAt: Date.now() + this.staleTimeMs,
flags,
success,
attemptCount,
} satisfies cacheEntry;

cacheData = Object.fromEntries(
Expand All @@ -90,6 +100,7 @@ export class FlagCache {
flags: cachedResponse[key].flags,
success: cachedResponse[key].success,
stale: cachedResponse[key].staleAt < Date.now(),
attemptCount: cachedResponse[key].attemptCount,
};
}
}
Expand Down Expand Up @@ -119,6 +130,7 @@ function validateCacheData(cacheDataInput: any) {
typeof cacheEntry.expireAt !== "number" ||
typeof cacheEntry.staleAt !== "number" ||
typeof cacheEntry.success !== "boolean" ||
typeof cacheEntry.attemptCount !== "number" ||
(cacheEntry.flags && !validateFlags(cacheEntry.flags))
) {
return;
Expand All @@ -129,6 +141,7 @@ function validateCacheData(cacheDataInput: any) {
staleAt: cacheEntry.staleAt,
success: cacheEntry.success,
flags: cacheEntry.flags,
attemptCount: cacheEntry.attemptCount,
};
}
return cacheData;
Expand Down
21 changes: 16 additions & 5 deletions packages/tracking-sdk/src/flags-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,20 @@ export async function fetchFlags(url: string, timeoutMs: number) {
console.error("[Bucket] error fetching flags: ", e);
} finally {
if (success) {
cache.set(url, success, flags);
cache.set(url, { success, flags, attemptCount: 0 });
} else {
const current = cache.get(url);
if (current) {
// if there is a previous version, extend it's expireAt time
cache.set(url, current.success, current.flags);
// if there is a previous failure cached, increase the attempt count
cache.set(url, {
success: current.success,
flags: current.flags,
attemptCount: current.attemptCount + 1,
});
} else {
// otherwise cache if the request failed and there is no previous version to extend
// to avoid having the UI wait and spam the API
cache.set(url, false, flags);
cache.set(url, { success: false, flags, attemptCount: 1 });
}
}

Expand All @@ -123,6 +127,7 @@ export async function fetchFlags(url: string, timeoutMs: number) {
const FLAG_FETCH_DEFAULT_TIMEOUT_MS = 5000;
export const FLAGS_STALE_MS = 60000; // turn stale after 60 seconds, optionally reevaluate in the background
export const FLAGS_EXPIRE_MS = 7 * 24 * 60 * 60 * 1000; // expire entirely after 7 days
const FAILURE_RETRY_ATTEMPTS = 3;
const localStorageCacheKey = `__bucket_flags`;

const cache = new FlagCache(
Expand Down Expand Up @@ -160,10 +165,16 @@ export async function getFlags({
const url = `${apiBaseUrl}/flags/evaluate?` + params.toString();
const cachedItem = cache.get(url);

if (!cachedItem) {
// if there's no cached item OR the cached item is a failure and we haven't retried
// too many times yet - fetch now
if (
!cachedItem ||
(!cachedItem.success && cachedItem.attemptCount <= FAILURE_RETRY_ATTEMPTS)
) {
return fetchFlags(url, timeoutMs);
}

// cachedItem is a success or a failed attempt that we've retried too many times
if (cachedItem.stale) {
// serve successful stale cache if `staleWhileRevalidate` is enabled
if (staleWhileRevalidate && cachedItem.success) {
Expand Down

0 comments on commit 751211a

Please sign in to comment.