Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ur): allow configuring ownerToHost #808 #812

Merged
merged 1 commit into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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