Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
150 commits
Select commit Hold shift + click to select a range
c6be0eb
Bring in search logic
thehabes Oct 15, 2025
a11c7e6
It searches!
thehabes Oct 15, 2025
4cefb0b
idNegotiation on search results
thehabes Oct 15, 2025
5ac7e49
All the search logic
thehabes Oct 15, 2025
e0f5b00
Lint, and add support for passing search options into the endpoint
thehabes Oct 15, 2025
c148362
polish
thehabes Oct 15, 2025
e3da99e
Update API documentation
thehabes Oct 15, 2025
9bb1234
polish
thehabes Oct 15, 2025
8d38409
polish
thehabes Oct 15, 2025
c376bd4
polish
thehabes Oct 15, 2025
e3f0553
polish
thehabes Oct 15, 2025
77d5430
polish
thehabes Oct 15, 2025
ab2505e
polish
thehabes Oct 15, 2025
d3386eb
polish
thehabes Oct 15, 2025
cadc2f1
polish
thehabes Oct 15, 2025
efb7dbf
exists test for new routes
thehabes Oct 15, 2025
e5f2486
Update public/API.html
thehabes Oct 15, 2025
052be8c
Update public/API.html
thehabes Oct 15, 2025
618a3f3
Update public/API.html
thehabes Oct 15, 2025
4a0093d
Update controllers/search.js
thehabes Oct 15, 2025
477abfa
Update controllers/search.js
thehabes Oct 15, 2025
f1b79f7
get rid of utils. prefix from createExpressError
thehabes Oct 15, 2025
6d063b2
Update public/API.html
thehabes Oct 15, 2025
065e6bb
Update public/API.html
thehabes Oct 15, 2025
4108379
slop formatting
thehabes Oct 15, 2025
b45e2fc
Touch ups to API.html as discussed at standup.
thehabes Oct 16, 2025
18afbd4
bump version because of new search feature
thehabes Oct 16, 2025
b28d7bc
initia idea
thehabes Oct 17, 2025
a6e60c3
tests for cache
thehabes Oct 20, 2025
a8d368c
gog routes too
thehabes Oct 20, 2025
0e18316
cleanup
thehabes Oct 20, 2025
970eaed
fix cachiung
thehabes Oct 20, 2025
793fd62
oh baby a lot going on here
thehabes Oct 20, 2025
1e52989
merge in main
thehabes Oct 20, 2025
9016fd8
structure
thehabes Oct 20, 2025
84158db
Update cache/__tests__/cache.test.js
thehabes Oct 20, 2025
24cf701
Changes from testing
thehabes Oct 21, 2025
c05d4d5
Changes from testing
thehabes Oct 21, 2025
15370ec
Changes from testing
thehabes Oct 21, 2025
f0d31ba
changes from testing
thehabes Oct 21, 2025
ec744af
Update docs for limit control
thehabes Oct 21, 2025
0deea37
update tests
thehabes Oct 21, 2025
1f3fc8c
changes from testing
thehabes Oct 21, 2025
856cd1c
changes from testing
thehabes Oct 21, 2025
ebd9b04
Move test files
thehabes Oct 21, 2025
6cf9e21
documentation
thehabes Oct 21, 2025
05bf04c
cleanup
thehabes Oct 21, 2025
4f0ba84
add status
thehabes Oct 21, 2025
dd90275
changes from testing
thehabes Oct 21, 2025
c8e7a45
changes from testing
thehabes Oct 21, 2025
2e39802
remove this from details
thehabes Oct 21, 2025
1c81ebf
reduce logs
thehabes Oct 21, 2025
c4cdcd5
amendments
thehabes Oct 21, 2025
5558b46
updated integration test
thehabes Oct 22, 2025
bcd7829
closer to real stress tests
thehabes Oct 23, 2025
46943e6
Metrics
thehabes Oct 23, 2025
777f9aa
Catch those hits
thehabes Oct 24, 2025
f75d04e
changes from testing scripts in local environment
thehabes Oct 24, 2025
030366a
changes from testing scripts in local environment
thehabes Oct 24, 2025
2973d61
changes from testing scripts in local environment
thehabes Oct 24, 2025
2ba15f8
Changes from testing in local environment
thehabes Oct 24, 2025
ca97954
Changes from testing in local environment
thehabes Oct 24, 2025
4a793be
Changes from testing in local environment
thehabes Oct 24, 2025
b8a70b0
Changes from testing in local environment
thehabes Oct 24, 2025
11d815c
requirements for running the .sh files in localhost environments
thehabes Oct 24, 2025
e9666c3
requirements for running the .sh files in localhost environments
thehabes Oct 24, 2025
aa934da
requirements for running the .sh files in localhost environments
thehabes Oct 24, 2025
1fca678
requirements for running the .sh files in localhost environments
thehabes Oct 24, 2025
20da77d
updates from testing
thehabes Oct 24, 2025
f14072d
updates from testing
thehabes Oct 24, 2025
128c3e7
Changes from running between environments
thehabes Oct 24, 2025
14d25f9
Changes from running between environments
thehabes Oct 24, 2025
d2f6358
Changes from testing across environments
thehabes Oct 24, 2025
1904584
changes from testing across environments
thehabes Oct 24, 2025
ebcc2da
changes from testing across environments
thehabes Oct 24, 2025
7cfed96
Changes from testing across environments
thehabes Oct 24, 2025
02e1a01
Changes from testing across environments
thehabes Oct 24, 2025
0dfedd8
Changes from testing across environments
thehabes Oct 24, 2025
c4373b8
log touchups
thehabes Oct 24, 2025
b8f6b13
This should just be a warning not a failure
thehabes Oct 24, 2025
82a46d2
touchup
thehabes Oct 27, 2025
86760d4
Deeper check for queries, more consideration around __rerum and _id p…
thehabes Oct 27, 2025
fa6e2cf
CACHING switch
thehabes Oct 27, 2025
3f1f399
Clean out /cache/clear route and logic
thehabes Oct 27, 2025
26bba5e
documentation
thehabes Oct 27, 2025
750f518
no more cacheClear
thehabes Oct 27, 2025
4e17463
Dang need it for tests
thehabes Oct 27, 2025
cdf121b
Don't test these
thehabes Oct 27, 2025
79040af
fix tests
thehabes Oct 27, 2025
ed823c9
Merge remote-tracking branch 'refs/remotes/origin/224-caching' into 2…
thehabes Oct 27, 2025
18896ad
Fix tests
thehabes Oct 27, 2025
6409fd1
cache action checks
thehabes Oct 28, 2025
760a53f
Point to devstore
thehabes Oct 28, 2025
ec2f952
Fixes from testing against devstore
thehabes Oct 28, 2025
bd23fed
try again
thehabes Oct 28, 2025
b8716a8
Add debug logs for dev
thehabes Oct 28, 2025
5380d9b
debugging
thehabes Oct 28, 2025
468810d
Fix uninitialized variable error in cache-metrics.sh
thehabes Oct 28, 2025
39a7ea7
Add PM2 cluster synchronization for cache operations
thehabes Oct 28, 2025
975b177
Fix PM2 cluster sync - use cache.cache.size instead of cache.length()
thehabes Oct 28, 2025
22b0ed1
Add debug logging to PM2 cache sync + optimize test pagination
thehabes Oct 28, 2025
96e514c
Remove non-functional PM2 sync code, document cluster behavior
thehabes Oct 28, 2025
a839f2a
debugging
thehabes Oct 28, 2025
3c31de9
Implement PM2 cluster cache synchronization
thehabes Oct 28, 2025
e81b3e6
Fix async cache tests and add local fallback for PM2 cluster cache
thehabes Oct 28, 2025
f6e82f7
update test
thehabes Oct 29, 2025
972806d
remove old files
thehabes Oct 29, 2025
0c7eba9
No devstore for now
thehabes Oct 29, 2025
ef67fde
Redo for the clustering
thehabes Oct 30, 2025
2c34ba5
LRU behavior
thehabes Oct 30, 2025
ce86f16
reset
thehabes Oct 30, 2025
c41cf82
geez
thehabes Oct 30, 2025
5b84dac
Changes from testing
thehabes Oct 31, 2025
81cf0d8
not sure
thehabes Nov 1, 2025
28510de
close
thehabes Nov 2, 2025
82d00cc
close
thehabes Nov 2, 2025
59fafc0
clauded around
thehabes Nov 2, 2025
10498d9
Fix cache-metrics.sh to not count clock skew as operation failures
Nov 3, 2025
3b63ff4
cleaning up and preparing for dev
Nov 3, 2025
3834d60
big clean
thehabes Nov 3, 2025
91f967b
little cleanup
Nov 3, 2025
d509c14
Support for non-cluster environment scenarios.
Nov 3, 2025
6753267
Getting ready for dev
Nov 4, 2025
f89a434
changes for security
Nov 4, 2025
466e4c4
extra lines
Nov 4, 2025
4c6f5de
force deploy
Nov 4, 2025
09efb40
This is working when mounted against the GoG app!
Nov 4, 2025
7e5d5b0
From testing and tests
Nov 5, 2025
b7b8007
cmon GitHub
Nov 5, 2025
8d52fb9
cmon GitHub
Nov 5, 2025
4e27427
consolodate tests and for the love of GH don't break GitHub cmon GitHub
Nov 5, 2025
c2cf181
looking good
Nov 5, 2025
9fc3c93
looking good
Nov 5, 2025
f8348b5
changes from testing, and new reports
Nov 5, 2025
2508c00
wsl clock check
Nov 5, 2025
277e488
it's working! Need to get things ready for an official review still
Nov 5, 2025
6379d15
it's working! Need to get things ready for an official review still
Nov 5, 2025
22ead64
Bring in claude with a yaml file. It can be called like copilot can …
Nov 6, 2025
18d0a05
update claude yaml
Nov 6, 2025
293e589
Clean up cache implementation: remove unused code and deprecated para…
thehabes Nov 6, 2025
ae2d163
ah yea token got it
Nov 6, 2025
00e9156
Merge branch '224-cluster-caching' of https://github.com/CenterForDig…
Nov 6, 2025
10ba3c6
key
Nov 6, 2025
8f1f088
listen to me
Nov 6, 2025
43393b1
Merge branch 'main' into 224-cluster-caching
Nov 6, 2025
5bcc193
Merge branch 'main' into 224-cluster-caching
Nov 6, 2025
c48a516
changes from close review and testing
Nov 6, 2025
c0d0631
Stress tested improvements
Nov 6, 2025
3542184
changes from testing
Nov 7, 2025
4a6acaf
hmm
Nov 8, 2025
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
8 changes: 7 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@
"Bash(cat:*)",
"Bash(./cache-metrics.sh:*)",
"Bash(./cache-metrics-worst-case.sh:*)",
"Bash(./rerum-metrics.sh:*)"
"Bash(./rerum-metrics.sh:*)",
"Bash(/tmp/test_cache.sh:*)",
"Bash(/tmp/test_cache_timing.sh:*)",
"Bash(/tmp/immediate_test.sh)",
"Bash(/tmp/cache_stress_test.sh)",
"Bash(python3:*)",
"Bash(/tmp/focused_race_test.sh)"
],
"deny": [],
"ask": []
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/claude.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ on:
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event.pull_request.draft == false) &&
((github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))))
runs-on: ubuntu-latest
permissions:
contents: write
Expand Down
356 changes: 356 additions & 0 deletions cache/__tests__/cache-limits.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
/**
* Cache limit enforcement tests for PM2 Cluster Cache
* Verifies maxLength, maxBytes, and TTL limits are properly configured and enforced
* @author thehabes
*/

// Ensure cache runs in local mode (not PM2 cluster) for tests
// This must be set before importing cache to avoid IPC timeouts
delete process.env.pm_id

import { jest } from '@jest/globals'
import cache from '../index.js'

/**
* Helper to wait for cache operations to complete
*/
async function waitForCache(ms = 100) {
return new Promise(resolve => setTimeout(resolve, ms))
}

/**
* Helper to get actual cache size from PM2 cluster cache
*/
async function getCacheSize() {
try {
const keysMap = await cache.clusterCache.keys()
const uniqueKeys = new Set()
for (const instanceKeys of Object.values(keysMap)) {
if (Array.isArray(instanceKeys)) {
instanceKeys.forEach(key => {
if (!key.startsWith('_stats_worker_')) {
uniqueKeys.add(key)
}
})
}
}
return uniqueKeys.size
} catch (err) {
return cache.allKeys.size
}
}

/**
* Configuration test data for parameterized tests
* Each entry defines: property name, default value, and environment variable
*/
const cacheConfigTests = [
{
property: 'maxLength',
defaultValue: 1000,
envVar: 'CACHE_MAX_LENGTH',
description: 'maximum number of cached entries'
},
{
property: 'maxBytes',
defaultValue: 1000000000,
envVar: 'CACHE_MAX_BYTES',
description: 'maximum cache size in bytes (1GB)'
},
{
property: 'ttl',
defaultValue: 86400000,
envVar: 'CACHE_TTL',
description: 'time-to-live in milliseconds (24 hours)'
}
]

describe('Cache TTL (Time-To-Live) Limit Enforcement', () => {
beforeEach(async () => {
await cache.clear()
await waitForCache(100)
}, 10000)

afterEach(async () => {
// Clean up stats interval to prevent hanging processes
if (cache.statsInterval) {
clearInterval(cache.statsInterval)
cache.statsInterval = null
}
await cache.clear()
}, 10000)

it('should respect default TTL from constructor', async () => {
const key = cache.generateKey('id', `default-ttl-${Date.now()}`)

await cache.set(key, { data: 'uses default ttl' })
await waitForCache(200) // Increased for CI/CD environment

// Should exist within TTL (uses configured default from cache/index.js)
const value = await cache.get(key)
expect(value).toEqual({ data: 'uses default ttl' })

// Verify TTL configuration directly on cache object (avoid getStats() timeout)
const expectedTTL = parseInt(process.env.CACHE_TTL ?? 86400000)
expect(cache.ttl).toBe(expectedTTL)
})

it('should enforce TTL across different cache key types', async () => {
const shortTTL = 800
const testId = Date.now()

// Set entries with short TTL
await cache.set(
cache.generateKey('query', { type: 'Test', testId }),
[{ id: 1 }],
shortTTL
)
await cache.set(
cache.generateKey('search', { searchText: 'test', testId }),
[{ id: 2 }],
shortTTL
)
await cache.set(
cache.generateKey('id', `ttl-${testId}`),
{ id: 3 },
shortTTL
)
await waitForCache(50)

// All should exist initially
expect(await cache.get(cache.generateKey('query', { type: 'Test', testId }))).toBeTruthy()
expect(await cache.get(cache.generateKey('search', { searchText: 'test', testId }))).toBeTruthy()
expect(await cache.get(cache.generateKey('id', `ttl-${testId}`))).toBeTruthy()

// Wait for TTL to expire
await new Promise(resolve => setTimeout(resolve, shortTTL + 300))

// All should be expired
expect(await cache.get(cache.generateKey('query', { type: 'Test', testId }))).toBeNull()
expect(await cache.get(cache.generateKey('search', { searchText: 'test', testId }))).toBeNull()
expect(await cache.get(cache.generateKey('id', `ttl-${testId}`))).toBeNull()
}, 8000)
})

describe('Cache maxLength Limit Enforcement', () => {
beforeEach(async () => {
await cache.clear()
await waitForCache(100)
}, 10000)

afterEach(async () => {
// Clean up stats interval to prevent hanging processes
if (cache.statsInterval) {
clearInterval(cache.statsInterval)
cache.statsInterval = null
}
await cache.clear()
}, 10000)

it('should track current cache length', async () => {
const testId = Date.now()

// Add entries
await cache.set(cache.generateKey('id', `len-1-${testId}`), { id: 1 })
await cache.set(cache.generateKey('id', `len-2-${testId}`), { id: 2 })
await cache.set(cache.generateKey('id', `len-3-${testId}`), { id: 3 })
await waitForCache(250)

// Check that length is tracked via allKeys (reliable method)
expect(cache.allKeys.size).toBeGreaterThanOrEqual(3)
})

it('should enforce maxLength limit with LRU eviction', async () => {
// Save original limit
const originalMaxLength = cache.maxLength

// Set very low limit for testing
cache.maxLength = 5
const testId = Date.now()

try {
// Add 5 entries (should all fit)
for (let i = 1; i <= 5; i++) {
await cache.set(cache.generateKey('id', `limit-${testId}-${i}`), { id: i })
await waitForCache(50)
}

// Check we have 5 entries
const sizeAfter5 = await getCacheSize()
expect(sizeAfter5).toBeLessThanOrEqual(5)

// Add 6th entry - should trigger eviction
await cache.set(cache.generateKey('id', `limit-${testId}-6`), { id: 6 })
await waitForCache(100)

// Should still be at or under limit (eviction enforced)
const sizeAfter6 = await getCacheSize()
expect(sizeAfter6).toBeLessThanOrEqual(5)

// Verify limit is being enforced (size didn't grow beyond maxLength)
expect(sizeAfter6).toBe(sizeAfter5) // Size stayed the same despite adding entry
} finally {
// Restore original limit
cache.maxLength = originalMaxLength
}
}, 10000)
})

describe('Cache maxBytes Limit Enforcement', () => {
beforeEach(async () => {
await cache.clear()
await waitForCache(100)
}, 10000)

afterEach(async () => {
// Clean up stats interval to prevent hanging processes
if (cache.statsInterval) {
clearInterval(cache.statsInterval)
cache.statsInterval = null
}
await cache.clear()
}, 10000)

it('should enforce maxBytes limit with LRU eviction', async () => {
// Save original limits
const originalMaxBytes = cache.maxBytes
const originalMaxLength = cache.maxLength

// Set very low byte limit for testing
cache.maxBytes = 5000 // 5KB
cache.maxLength = 100 // High enough to not interfere
const testId = Date.now()

try {
// Create a large object (approximately 2KB each)
const largeObject = {
id: 1,
data: 'x'.repeat(1000),
timestamp: Date.now()
}

// Calculate approximate size
const approxSize = cache._calculateSize(largeObject)
const maxEntries = Math.floor(cache.maxBytes / approxSize)

// Add more entries than should fit
const entriesToAdd = maxEntries + 3
for (let i = 1; i <= entriesToAdd; i++) {
await cache.set(
cache.generateKey('id', `bytes-${testId}-${i}`),
{ ...largeObject, id: i }
)
await waitForCache(50)
}

// Wait a bit for evictions to process
await waitForCache(500)

// Check that cache size is under limit (eviction enforced)
const finalSize = await getCacheSize()
expect(finalSize).toBeLessThanOrEqual(maxEntries)

// Verify bytes didn't grow unbounded
expect(cache.totalBytes).toBeLessThanOrEqual(cache.maxBytes)
} finally {
// Restore original limits
cache.maxBytes = originalMaxBytes
cache.maxLength = originalMaxLength
}
}, 20000)
})

describe('Cache Limit Breaking Change Detection', () => {
it('should have valid limit configuration and respect environment variables', () => {
// Verify cache respects env vars if set, or uses reasonable defaults
const expectedMaxLength = parseInt(process.env.CACHE_MAX_LENGTH ?? 1000)
const expectedMaxBytes = parseInt(process.env.CACHE_MAX_BYTES ?? 1000000000)
const expectedTTL = parseInt(process.env.CACHE_TTL ?? 86400000)

expect(cache.maxLength).toBe(expectedMaxLength)
expect(cache.maxBytes).toBe(expectedMaxBytes)
expect(cache.ttl).toBe(expectedTTL)

// Verify limits are positive and reasonable
expect(cache.maxLength).toBeGreaterThan(0)
expect(cache.maxLength).toBeLessThan(10000) // < 10 thousand

expect(cache.maxBytes).toBeGreaterThan(0)
expect(cache.maxBytes).toBeLessThan(10000000000) // < 10GB

expect(cache.ttl).toBeGreaterThan(0)
expect(cache.ttl).toBeLessThanOrEqual(86400000) // ≤ 24 hours
})

it('should correctly calculate size for deeply nested query objects', async () => {
await cache.clear()

// Create queries with deeply nested properties (5+ levels)
const deeplyNestedQuery = cache.generateKey('query', {
__cached: {
'level1.level2.level3.level4.level5': 'deepValue',
'body.target.source.metadata.author.name': 'John Doe',
'nested.array.0.property.value': 123
},
limit: 100,
skip: 0
})

// Create a large result set with nested objects
const nestedResults = Array.from({ length: 50 }, (_, i) => ({
id: `obj${i}`,
level1: {
level2: {
level3: {
level4: {
level5: 'deepValue',
additionalData: new Array(100).fill('x').join('')
}
}
}
},
body: {
target: {
source: {
metadata: {
author: {
name: 'John Doe',
email: 'john@example.com'
}
}
}
}
}
}))

await cache.set(deeplyNestedQuery, nestedResults)

// Verify the cache entry exists
expect(await cache.get(deeplyNestedQuery)).not.toBeNull()

// Add more deeply nested queries until we approach maxBytes
const queries = []
for (let i = 0; i < 10; i++) {
const key = cache.generateKey('query', {
__cached: {
[`level1.level2.level3.property${i}`]: `value${i}`,
'deep.nested.structure.array.0.id': i
},
limit: 100,
skip: 0
})
queries.push(key)
await cache.set(key, nestedResults)
}

// Verify cache entries exist - check a few queries to confirm caching works
expect(await cache.get(deeplyNestedQuery)).not.toBeNull()
expect(await cache.get(queries[queries.length - 1])).not.toBeNull()

// Verify maxBytes enforcement: cache operations should continue working
// even if some entries were evicted due to byte limits
const midpoint = Math.floor(queries.length / 2)
expect(await cache.get(queries[midpoint])).toBeTruthy()
})
})

Loading