Skip to content

Commit 338426a

Browse files
committed
fix: resolve query cache collision and bulk refresh issues
- Fix query cache key generation with deep object sorting Previously only top-level keys were sorted, causing cache collisions between similar queries (e.g. $phrase vs $phrase_prefix) - Remove manual refresh handling in patch-bulk Elasticsearch bulk API natively supports refresh parameter - Improve GitHub Actions Elasticsearch health check Wait for yellow/green cluster status instead of just connectivity
1 parent 752dafd commit 338426a

File tree

4 files changed

+95
-116
lines changed

4 files changed

+95
-116
lines changed

.github/workflows/test-matrix.yml

Lines changed: 60 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: Test Matrix
22

33
on:
44
push:
5-
branches: [ main, master, dove ]
5+
branches: [main, master, dove]
66
pull_request:
7-
branches: [ main, master, dove ]
7+
branches: [main, master, dove]
88

99
jobs:
1010
test:
@@ -18,48 +18,68 @@ jobs:
1818
name: Node ${{ matrix.node-version }} - ES ${{ matrix.elasticsearch-version }}
1919

2020
steps:
21-
- uses: actions/checkout@v3
21+
- uses: actions/checkout@v3
2222

23-
- name: Use Node.js ${{ matrix.node-version }}
24-
uses: actions/setup-node@v3
25-
with:
26-
node-version: ${{ matrix.node-version }}
23+
- name: Use Node.js ${{ matrix.node-version }}
24+
uses: actions/setup-node@v3
25+
with:
26+
node-version: ${{ matrix.node-version }}
2727

28-
- name: Start Elasticsearch ${{ matrix.elasticsearch-version }}
29-
run: |
30-
docker run -d \
31-
--name elasticsearch \
32-
-p 9200:9200 \
33-
-e "discovery.type=single-node" \
34-
-e "xpack.security.enabled=false" \
35-
-e "xpack.security.enrollment.enabled=false" \
36-
docker.elastic.co/elasticsearch/elasticsearch:${{ matrix.elasticsearch-version }}
28+
- name: Start Elasticsearch ${{ matrix.elasticsearch-version }}
29+
run: |
30+
docker run -d \
31+
--name elasticsearch \
32+
-p 9200:9200 \
33+
-e "discovery.type=single-node" \
34+
-e "xpack.security.enabled=false" \
35+
-e "xpack.security.enrollment.enabled=false" \
36+
docker.elastic.co/elasticsearch/elasticsearch:${{ matrix.elasticsearch-version }}
3737
38-
- name: Wait for Elasticsearch
39-
run: |
40-
for i in {1..30}; do
41-
if curl -s "http://localhost:9200/_cluster/health" > /dev/null 2>&1; then
42-
echo "Elasticsearch is ready"
43-
break
44-
fi
45-
echo "Waiting for Elasticsearch..."
46-
sleep 5
47-
done
38+
- name: Wait for Elasticsearch
39+
run: |
40+
echo "Waiting for Elasticsearch to be ready..."
41+
for i in {1..60}; do
42+
# Check cluster health status
43+
HEALTH=$(curl -s "http://localhost:9200/_cluster/health" 2>/dev/null || echo "")
44+
if [ ! -z "$HEALTH" ]; then
45+
STATUS=$(echo $HEALTH | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
46+
echo "Attempt $i: Cluster status is '$STATUS'"
4847
49-
- name: Install dependencies
50-
run: npm ci
48+
# Wait for yellow or green status (yellow is ok for single-node)
49+
if [ "$STATUS" = "yellow" ] || [ "$STATUS" = "green" ]; then
50+
echo "Elasticsearch is ready!"
51+
# Give it a bit more time to fully stabilize
52+
sleep 5
53+
curl -s "http://localhost:9200/_cluster/health?pretty"
54+
break
55+
fi
56+
else
57+
echo "Attempt $i: Elasticsearch not responding yet..."
58+
fi
5159
52-
- name: Build
53-
run: npm run build
60+
if [ $i -eq 60 ]; then
61+
echo "ERROR: Elasticsearch failed to become ready after 5 minutes"
62+
docker logs elasticsearch
63+
exit 1
64+
fi
5465
55-
- name: Run tests
56-
run: |
57-
ES_VERSION=${{ matrix.elasticsearch-version }} \
58-
ELASTICSEARCH_URL=http://localhost:9200 \
59-
npm run mocha
66+
sleep 5
67+
done
6068
61-
- name: Upload coverage
62-
if: matrix.node-version == '20' && matrix.elasticsearch-version == '8.15.0'
63-
uses: codecov/codecov-action@v3
64-
with:
65-
file: ./coverage/lcov.info
69+
- name: Install dependencies
70+
run: npm ci
71+
72+
- name: Build
73+
run: npm run build
74+
75+
- name: Run tests
76+
run: |
77+
ES_VERSION=${{ matrix.elasticsearch-version }} \
78+
ELASTICSEARCH_URL=http://localhost:9200 \
79+
npm run mocha
80+
81+
- name: Upload coverage
82+
if: matrix.node-version == '20' && matrix.elasticsearch-version == '8.15.0'
83+
uses: codecov/codecov-action@v3
84+
with:
85+
file: ./coverage/lcov.info

package-lock.json

Lines changed: 0 additions & 40 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/methods/patch-bulk.ts

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -55,36 +55,16 @@ function prepareBulkUpdateParams(
5555
operations: Array<Record<string, unknown>>,
5656
index: string,
5757
requestParams: ElasticsearchServiceParams
58-
): { params: Record<string, unknown>; needsRefresh: boolean } {
58+
): Record<string, unknown> {
5959
// PERFORMANCE: Merge esParams with per-operation refresh override
60-
const params = Object.assign(
60+
// Note: Elasticsearch bulk API supports refresh parameter directly
61+
return Object.assign(
6162
{
6263
index,
6364
body: operations
6465
},
6566
mergeESParamsWithRefresh(service.esParams, requestParams)
6667
)
67-
68-
// Remove refresh from bulk params but return it separately
69-
const needsRefresh = params.refresh as boolean
70-
delete params.refresh
71-
72-
return { params, needsRefresh }
73-
}
74-
75-
/**
76-
* Handles refresh if needed after bulk operation
77-
*/
78-
async function handleRefresh(
79-
service: ElasticAdapterInterface,
80-
bulkResult: unknown,
81-
needsRefresh: boolean,
82-
index: string
83-
): Promise<unknown> {
84-
if (needsRefresh) {
85-
await service.Model.indices.refresh({ index })
86-
}
87-
return bulkResult
8868
}
8969

9070
/**
@@ -201,19 +181,14 @@ export async function patchBulk(
201181
const operations = createBulkOperations(service, found, data, index)
202182

203183
// Step 3: Prepare and execute bulk update
204-
const { params: bulkUpdateParams, needsRefresh } = prepareBulkUpdateParams(
205-
service,
206-
operations,
207-
index,
208-
params
209-
)
210-
211-
let bulkResult = (await service.Model.bulk(bulkUpdateParams as never)) as unknown as Record<string, unknown>
184+
const bulkUpdateParams = prepareBulkUpdateParams(service, operations, index, params)
212185

213-
// Step 4: Handle refresh if needed
214-
bulkResult = (await handleRefresh(service, bulkResult, needsRefresh, index)) as Record<string, unknown>
186+
const bulkResult = (await service.Model.bulk(bulkUpdateParams as never)) as unknown as Record<
187+
string,
188+
unknown
189+
>
215190

216-
// Step 5: Get updated document IDs
191+
// Step 4: Get updated document IDs
217192
const updatedIds = getUpdatedIds(bulkResult)
218193

219194
if (updatedIds.length === 0) {

src/utils/parse-query.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,39 @@ const queryCache = new Map<string, { result: ESQuery | null; timestamp: number }
1313
const CACHE_MAX_SIZE = 1000
1414
const CACHE_MAX_AGE = 5 * 60 * 1000 // 5 minutes
1515

16+
/**
17+
* Recursively sort object keys for deterministic JSON serialization
18+
* @param obj - Object to normalize
19+
* @returns Normalized object with sorted keys
20+
*/
21+
function normalizeObject(obj: unknown): unknown {
22+
if (obj === null || typeof obj !== 'object') {
23+
return obj
24+
}
25+
26+
if (Array.isArray(obj)) {
27+
return obj.map(normalizeObject)
28+
}
29+
30+
const sorted: Record<string, unknown> = {}
31+
Object.keys(obj as object)
32+
.sort()
33+
.forEach((key) => {
34+
sorted[key] = normalizeObject((obj as Record<string, unknown>)[key])
35+
})
36+
37+
return sorted
38+
}
39+
1640
/**
1741
* Generate a stable hash for a query object
1842
* @param query - Query object to hash
1943
* @param idProp - ID property name
2044
* @returns Hash string
2145
*/
2246
function hashQuery(query: Record<string, unknown>, idProp: string): string {
23-
// Create deterministic string representation
24-
const normalized = JSON.stringify(query, Object.keys(query).sort())
47+
// Create deterministic string representation with deep key sorting
48+
const normalized = JSON.stringify(normalizeObject(query))
2549
return createHash('sha256').update(`${normalized}:${idProp}`).digest('hex').slice(0, 16)
2650
}
2751

0 commit comments

Comments
 (0)