Skip to content

Commit 0062f5a

Browse files
committed
refactor: replace FastIndex caching with Map and add round-robin target selection
- Replace cafe-utility FastIndex with native Map for response caching - Add round-robin load balancing for healthy targets - Improve error handling in fetchWithTimeout with proper JSON validation - Add markTargetAsUnhealthy function for better target management
1 parent 6b25650 commit 0062f5a

3 files changed

Lines changed: 58 additions & 24 deletions

File tree

src/index.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Arrays, Objects } from 'cafe-utility'
44
import { IncomingMessage, ServerResponse, createServer } from 'http'
55
import fetch from 'node-fetch'
66
import { metrics } from './metrics'
7-
import { Target, getHealthyTarget } from './target'
7+
import { Target, getHealthyTarget, markTargetAsUnhealthy } from './target'
88
import { RequestContext, ResponseContext } from './types'
99
import { fetchWithTimeout, respondWithFetchPromise } from './utility'
1010

@@ -21,10 +21,11 @@ function main() {
2121
const port = Arrays.getNumberArgument(process.argv, 'port', process.env, PORT_ENV) || DEFAULT_PORT
2222
const target = Arrays.getArgument(process.argv, 'target', process.env, TARGET_ENV) || DEFAULT_TARGET
2323
const expiry = Arrays.getNumberArgument(process.argv, 'expiry', process.env, EXPIRY_ENV) || DEFAULT_EXPIRY
24-
const fastIndex = Objects.createFastIndex()
24+
const cache = new Map<string, { promise: Promise<ResponseContext | null>, expiry: number }>()
2525
const targets: Target[] = target.split(',').map(x => ({
2626
url: x,
27-
lastErrorAt: 0
27+
lastErrorAt: 0,
28+
lastUsedAt: 0
2829
}))
2930
const server = createServer(async (request: IncomingMessage, response: ServerResponse) => {
3031
request.on('error', error => {
@@ -37,12 +38,12 @@ function main() {
3738
for (let i = 0; i < targets.length; i++) {
3839
const target = getHealthyTarget(targets)
3940
try {
40-
await fetch(target.url, { timeout: 10_000 })
41+
await fetch(target.url)
4142
response.statusCode = 200
4243
response.end(`200 OK - ${metrics.requests} requests served`)
4344
return
4445
} catch (error) {
45-
target.lastErrorAt = Date.now()
46+
markTargetAsUnhealthy(targets, target.url)
4647
console.error(error)
4748
}
4849
}
@@ -69,17 +70,15 @@ function main() {
6970
delete parsedBody.id
7071
metrics.requests++
7172
const key = `${target.url}_${JSON.stringify(parsedBody)}`
72-
const cachedPromise = Objects.getFromFastIndexWithExpiracy(
73-
fastIndex,
74-
key
75-
) as Promise<ResponseContext>
73+
const cached = cache.get(key)
74+
const cachedPromise = cached && cached.expiry > Date.now() ? cached.promise : null
7675
if (cachedPromise) {
7776
process.stdout.write(`Cache hit: ${key}\n`)
7877
const successful = await respondWithFetchPromise(id, response, cachedPromise)
7978
if (successful) {
8079
return
8180
} else {
82-
target.lastErrorAt = Date.now()
81+
markTargetAsUnhealthy(targets, target.url)
8382
continue
8483
}
8584
}
@@ -92,16 +91,16 @@ function main() {
9291
headers: context.headers,
9392
body: context.body
9493
})
95-
Objects.pushToFastIndexWithExpiracy(fastIndex as any, key, responsePromise, expiry)
94+
cache.set(key, { promise: responsePromise, expiry: Date.now() + expiry })
9695
const successful = await respondWithFetchPromise(id, response, responsePromise)
9796
if (successful) {
9897
return
9998
} else {
100-
target.lastErrorAt = Date.now()
99+
markTargetAsUnhealthy(targets, target.url)
101100
continue
102101
}
103102
} catch (error) {
104-
target.lastErrorAt = Date.now()
103+
markTargetAsUnhealthy(targets, target.url)
105104
console.error(error)
106105
}
107106
}

src/target.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,29 @@ import { Dates } from 'cafe-utility'
33
export type Target = {
44
url: string
55
lastErrorAt: number
6+
lastUsedAt: number
67
}
78

9+
let lastUsedIndex = 0
10+
811
export function getHealthyTarget(targets: Target[]): Target {
912
const healthyIfLastErrorIsBefore = Date.now() - Dates.hours(2)
1013
const healthyTargets = targets.filter(x => x.lastErrorAt < healthyIfLastErrorIsBefore)
11-
return healthyTargets[0] || targets[0]
14+
15+
if (healthyTargets.length === 0) {
16+
return targets[0] // Fallback to first target if none are healthy
17+
}
18+
19+
// Round-robin selection among healthy targets
20+
const selectedTarget = healthyTargets[lastUsedIndex % healthyTargets.length]
21+
lastUsedIndex = (lastUsedIndex + 1) % healthyTargets.length
22+
23+
return selectedTarget
24+
}
25+
26+
export function markTargetAsUnhealthy(targets: Target[], targetUrl: string): void {
27+
const target = targets.find(t => t.url === targetUrl)
28+
if (target) {
29+
target.lastErrorAt = Date.now()
30+
}
1231
}

src/utility.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,31 @@ export async function respondWithFetchPromise(
2525
}
2626

2727
export async function fetchWithTimeout(url: string, options: RequestInit): Promise<ResponseContext | null> {
28-
const response = fetch(url, { ...options, timeout: 10_000 })
29-
.then(async x => ({
30-
status: x.status,
31-
headers: x.headers.raw(),
32-
json: await x.json()
33-
}))
34-
.catch(error => {
35-
console.error(error)
28+
try {
29+
const response = await fetch(url, { ...options, timeout: 10_000 })
30+
31+
// Check if response is ok (status 200-299)
32+
if (!response.ok) {
33+
console.error(`HTTP ${response.status} error from ${url}: ${response.statusText}`)
34+
return null
35+
}
36+
37+
// Try to parse JSON, but handle non-JSON responses gracefully
38+
let jsonData
39+
try {
40+
jsonData = await response.json()
41+
} catch (jsonError) {
42+
console.error(`Invalid JSON response from ${url}: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`)
3643
return null
37-
})
38-
return response
44+
}
45+
46+
return {
47+
status: response.status,
48+
headers: response.headers.raw(),
49+
json: jsonData
50+
}
51+
} catch (error) {
52+
console.error(`Fetch error from ${url}:`, error)
53+
return null
54+
}
3955
}

0 commit comments

Comments
 (0)