Skip to content

Commit

Permalink
feat(ur): allow configuring ownerToHost #808
Browse files Browse the repository at this point in the history
  • Loading branch information
TillaTheHun0 committed Jun 21, 2024
1 parent 1a5667f commit 7cc5543
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 85 deletions.
33 changes: 30 additions & 3 deletions servers/ur/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,23 @@ const serverConfigSchema = z.object({
return typeof val === 'string' ? parseInt(val) : -1
}, z.number().positive()),
processToHost: stringifiedJsonSchema.nullish(),
ownerToHost: stringifiedJsonSchema.nullish(),
hosts: z.preprocess(
(arg) => (typeof arg === 'string' ? arg.split(',').map(str => str.trim()) : arg),
z.array(z.string().url())
),
aoUnit: z.enum(['cu', 'mu']),
strategy: z.enum(['proxy', 'redirect']),
subrouterUrl: z.string().nullable().optional(),
surUrl: z.string().nullable().optional(),
/**
* @deprecated - use ownerToHost or processToHost to
* achieve subrouting
*/
subrouterUrl: z.string().nullable().optional(),
/**
* @deprecated - use ownerToHost or processToHost to
* achieve subrouting
*/
owners: z.preprocess(
(arg) => (typeof arg === 'string' ? arg.split(',').map(str => str.trim()) : arg),
z.array(z.string())
Expand All @@ -58,6 +67,7 @@ const CONFIG_ENVS = {
port: process.env.PORT || 3005,
hosts: process.env.HOSTS || ['http://127.0.0.1:3005'],
processToHost: process.env.PROCESS_TO_HOST || JSON.stringify({}),
ownerToHost: process.env.OWNER_TO_HOST || JSON.stringify({}),

/**
* default to the CU for no hassle startup in development mode,
Expand All @@ -67,21 +77,38 @@ const CONFIG_ENVS = {
aoUnit: process.env.AO_UNIT || 'cu',
strategy: process.env.STRATEGY || 'proxy',

subrouterUrl: process.env.SUBROUTER_URL,
surUrl: process.env.SUR_URL,
/**
* @deprecated - use ownerToHost or processToHost to
* achieve subrouting
*/
subrouterUrl: process.env.SUBROUTER_URL,
/**
* @deprecated - use ownerToHost or processToHost to
* achieve subrouting
*/
owners: process.env.OWNERS
},
production: {
MODE,
port: process.env.PORT || 3005,
hosts: process.env.HOSTS,
processToHost: process.env.PROCESS_TO_HOST || JSON.stringify({}),
ownerToHost: process.env.OWNER_TO_HOST || JSON.stringify({}),

aoUnit: process.env.AO_UNIT,
strategy: process.env.STRATEGY || 'proxy',

subrouterUrl: process.env.SUBROUTER_URL,
surUrl: process.env.SUR_URL,
/**
* @deprecated - use ownerToHost or processToHost to
* achieve subrouting
*/
subrouterUrl: process.env.SUBROUTER_URL,
/**
* @deprecated - use ownerToHost or processToHost to
* achieve subrouting
*/
owners: process.env.OWNERS
}
}
Expand Down
61 changes: 41 additions & 20 deletions servers/ur/src/domain.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { defaultTo, isEmpty, complement, path } from 'ramda'
import { LRUCache } from 'lru-cache'

export function bailoutWith ({ fetch, subrouterUrl, surUrl, owners, processToHost }) {
const cache = new LRUCache({
const isNotEmpty = complement(isEmpty)

export function bailoutWith ({ fetch, subrouterUrl, surUrl, owners, processToHost, ownerToHost }) {
const processToOwnerCache = new LRUCache({
/**
* 10MB
*/
Expand All @@ -12,34 +15,49 @@ export function bailoutWith ({ fetch, subrouterUrl, surUrl, owners, processToHos
sizeCalculation: () => 8
})

async function findProcessOwner (processId) {
const owner = processToOwnerCache.get(processId)
if (owner) return owner

return fetch(`${surUrl}/processes/${processId}`)
.then((res) => res.json())
.then(defaultTo({}))
.then(path(['owner', 'address']))
.then((owner) => {
if (!owner) return null

processToOwnerCache.set(processId, owner)
return owner
})
.catch((_e) => null)
}

return async (processId) => {
/**
* If a process has a specific mapping configured,
* then immediately return it's mapping
*/
if (processToHost && processToHost[processId]) return processToHost[processId]
/**
* If there are owner -> host configured, then we lookup the process
* owner and return the specific host if found
*/
if (ownerToHost && isNotEmpty(ownerToHost)) {
const owner = await findProcessOwner(processId)
if (ownerToHost[owner]) return ownerToHost[owner]
}

/**
* @deprecated - this functionality is subsumed by ownerToHost
* and will eventually be removed
*
* All three of these must be set for the
* subrouter logic to work so if any are
* not set just return.
*/
if (!subrouterUrl || !surUrl || !owners) return
let owner = cache.get(processId)
if (!owner) {
const suResponse = await fetch(`${surUrl}/processes/${processId}`)
.then((res) => res.json())
.catch((_e) => null)
if (!suResponse) return
if (!suResponse.owner) return
if (!suResponse.owner.address) return
cache.set(processId, suResponse.owner.address)
owner = suResponse.owner.address
}

if (owners.includes(owner)) {
return subrouterUrl
}
const owner = await findProcessOwner(processId)
if (owners.includes(owner)) return subrouterUrl
}
}

Expand All @@ -53,7 +71,10 @@ export function bailoutWith ({ fetch, subrouterUrl, surUrl, owners, processToHos
* been attempted, and so return undefined, to be handled upstream
*/
export function determineHostWith ({ hosts = [], bailout }) {
const cache = new LRUCache({
/**
* TODO: should we inject this cache?
*/
const processToHostCache = new LRUCache({
/**
* 10MB
*/
Expand All @@ -75,13 +96,13 @@ export function determineHostWith ({ hosts = [], bailout }) {
/**
* Check cache, and hydrate if necessary
*/
let hashSum = cache.get(processId)
let hashSum = processToHostCache.get(processId)
if (!hashSum) {
/**
* Only perform the expensive computation of hash -> idx once and cache
*/
hashSum = computeHashSumFromProcessId({ processId, length: hosts.length })
cache.set(processId, hashSum)
processToHostCache.set(processId, hashSum)
}

return hosts[(hashSum + failoverAttempt) % hosts.length]
Expand Down
181 changes: 123 additions & 58 deletions servers/ur/src/domain.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,87 +6,152 @@ import { groupBy, identity } from 'ramda'
import { determineHostWith, bailoutWith, computeHashSumFromProcessId } from './domain.js'

const HOSTS = ['http://foo.bar', 'http://fizz.buzz']
const cache = {
get: () => undefined,
set: () => undefined
}

describe('domain', () => {
describe('determineHostWith', () => {
test('should deterministically return a valid host', async () => {
const determineHost = determineHostWith({ hosts: HOSTS, cache })
describe('compute or cached', () => {
test('should deterministically return a valid host', async () => {
const determineHost = determineHostWith({ hosts: HOSTS })

assert(await determineHost({ processId: 'process-123', failoverAttempt: 0 }))
assert.equal(await determineHost({ processId: 'process-123', failoverAttempt: 0 }), await determineHost({ processId: 'process-123', failoverAttempt: 0 }))
})
assert(await determineHost({ processId: 'process-123', failoverAttempt: 0 }))
assert.equal(await determineHost({ processId: 'process-123', failoverAttempt: 0 }), await determineHost({ processId: 'process-123', failoverAttempt: 0 }))
})

test('should shift the determined host according to failoverAttempt', async () => {
const determineHost = determineHostWith({ hosts: HOSTS, cache })
test('should shift the determined host according to failoverAttempt', async () => {
const determineHost = determineHostWith({ hosts: HOSTS })

assert.notEqual(await determineHost({ processId: 'process-123', failoverAttempt: 0 }), await determineHost({ processId: 'process-123', failoverAttempt: 1 }))
assert.notEqual(await determineHost({ processId: 'process-123', failoverAttempt: 0 }), await determineHost({ processId: 'process-123', failoverAttempt: 1 }))
})

test('should return undefined if all hosts have been attempted', async () => {
const determineHost = determineHostWith({ hosts: HOSTS })
assert.equal(await determineHost({ processId: 'process-123', failoverAttempt: HOSTS.length }), undefined)
})
})

test('should return undefined if all hosts have been attempted', async () => {
const determineHost = determineHostWith({ hosts: HOSTS, cache })
assert.equal(await determineHost({ processId: 'process-123', failoverAttempt: HOSTS.length }), undefined)
describe('processToHost', () => {
test('should redirect to the specific host for the process', async () => {
const bailout = bailoutWith({ processToHost: { 'process-123': 'https://specific.host' } })

const determineHost = determineHostWith({ hosts: HOSTS, bailout })
assert.equal(await determineHost({ processId: 'process-123', failoverAttempt: 0 }), 'https://specific.host')
})
})

test('should serve from the cache, if found', async () => {
const determineHost = determineHostWith({
hosts: HOSTS,
cache: { ...cache, get: () => 10 }
describe('ownerToHost', () => {
test('should bailout if the process owner is mapped to a specific host', async () => {
const fetchMock = async (url) => {
assert.equal(url, 'surUrl1/processes/process-123')
return new Response(JSON.stringify({ owner: { address: 'owner2' } }))
}

const bailout = bailoutWith({
fetch: fetchMock,
surUrl: 'surUrl1',
ownerToHost: { owner2: 'https://specific_owner.host' }
})

const determineHost = determineHostWith({ hosts: HOSTS, bailout })

const host = await determineHost({ processId: 'process-123', failoverAttempt: 0 })
assert.equal(host, 'https://specific_owner.host')
})

assert.equal(await determineHost({ processId: 'process-123', failoverAttempt: HOSTS.length }), HOSTS[HOSTS.length & 10])
})
test('should NOT bailout if the process owner is not mapped to a specific host', async () => {
const fetchMock = async (url) => {
assert.equal(url, 'surUrl1/processes/process-123')
return new Response(JSON.stringify({ owner: { address: 'owner2' } }))
}

const bailout = bailoutWith({
fetch: fetchMock,
surUrl: 'surUrl1',
ownerToHost: { notOwner2: 'https://specific_owner.host' }
})

const determineHost = determineHostWith({ hosts: HOSTS, bailout })

test('should redirect to the subrouterUrl', async () => {
const fetchMock = async (url) => {
assert.equal(url, 'surUrl1/processes/process-123')
return new Response(JSON.stringify({ owner: { address: 'owner2' } }))
}

const bailout = bailoutWith({
fetch: fetchMock,
surUrl: 'surUrl1',
subrouterUrl: 'subrouterUrl1',
owners: ['owner1', 'owner2']
const host = await determineHost({ processId: 'process-123', failoverAttempt: 0 })
assert.ok(host !== 'https://specific_owner.host')
assert.ok(HOSTS.includes(host))
})

const determineHost = determineHostWith({ hosts: HOSTS, cache, bailout })
test('should NOT bailout if no ownerToHost is provided', async () => {
const fetchMock = async (url) => {
assert.equal(url, 'surUrl1/processes/process-123')
return new Response(JSON.stringify({ owner: { address: 'owner2' } }))
}

assert(await determineHost({ processId: 'process-123', failoverAttempt: 0 }))
assert.equal(await determineHost({ processId: 'process-123', failoverAttempt: 0 }), 'subrouterUrl1')
const determineHostEmptyMapping = determineHostWith({
hosts: HOSTS,
bailout: bailoutWith({
fetch: fetchMock,
surUrl: 'surUrl1',
ownerToHost: {}
})
})
const host = await determineHostEmptyMapping({ processId: 'process-123', failoverAttempt: 0 })
assert.ok(HOSTS.includes(host))

const determineHostNoMapping = determineHostWith({
hosts: HOSTS,
bailout: bailoutWith({
fetch: fetchMock,
surUrl: 'surUrl1',
ownerToHost: undefined
})
})
const host1 = await determineHostNoMapping({ processId: 'process-123', failoverAttempt: 0 })
assert.ok(HOSTS.includes(host1))
})
})

test('should not redirect to the subrouterUrl', async () => {
const fetchMock = async (url) => {
assert.equal(url, 'surUrl1/processes/process-123')
/**
* Here the owner does not match any in the list
* this will cause it to not redirect to the subrouter
*/
return new Response(JSON.stringify({ owner: { address: 'owner3' } }))
}

const bailout = bailoutWith({
fetch: fetchMock,
surUrl: 'surUrl1',
subrouterUrl: 'subrouterUrl1',
owners: ['owner1', 'owner2']
/**
* @deprecated - this functionality is subsumed by ownerToHost
* and will eventually be removed, along with the tests
*/
describe('subRouter - DEPRECATED', () => {
test('should redirect to the subrouterUrl', async () => {
const fetchMock = async (url) => {
assert.equal(url, 'surUrl1/processes/process-123')
return new Response(JSON.stringify({ owner: { address: 'owner2' } }))
}

const bailout = bailoutWith({
fetch: fetchMock,
surUrl: 'surUrl1',
subrouterUrl: 'subrouterUrl1',
owners: ['owner1', 'owner2']
})

const determineHost = determineHostWith({ hosts: HOSTS, bailout })

assert(await determineHost({ processId: 'process-123', failoverAttempt: 0 }))
assert.equal(await determineHost({ processId: 'process-123', failoverAttempt: 0 }), 'subrouterUrl1')
})

const determineHost = determineHostWith({ hosts: HOSTS, cache, bailout })
test('should not redirect to the subrouterUrl', async () => {
const fetchMock = async (url) => {
assert.equal(url, 'surUrl1/processes/process-123')
/**
* Here the owner does not match any in the list
* this will cause it to not redirect to the subrouter
*/
return new Response(JSON.stringify({ owner: { address: 'owner3' } }))
}

assert(await determineHost({ processId: 'process-123', failoverAttempt: 0 }))
assert.equal(await determineHost({ processId: 'process-123', failoverAttempt: 0 }), 'http://fizz.buzz')
})
const bailout = bailoutWith({
fetch: fetchMock,
surUrl: 'surUrl1',
subrouterUrl: 'subrouterUrl1',
owners: ['owner1', 'owner2']
})

test('should redirect to the specific host for the process', async () => {
const bailout = bailoutWith({ processToHost: { 'process-123': 'https://specific.host' } })
const determineHost = determineHostWith({ hosts: HOSTS, bailout })

const determineHost = determineHostWith({ hosts: HOSTS, cache, bailout })
assert.equal(await determineHost({ processId: 'process-123', failoverAttempt: 0 }), 'https://specific.host')
assert(await determineHost({ processId: 'process-123', failoverAttempt: 0 }))
assert.equal(await determineHost({ processId: 'process-123', failoverAttempt: 0 }), 'http://fizz.buzz')
})
})
})

Expand Down
Loading

0 comments on commit 7cc5543

Please sign in to comment.