Skip to content

Commit 3833bbe

Browse files
fix(core,ci): apply pessimistic locking and stabilize database services in CI
1 parent becce09 commit 3833bbe

File tree

5 files changed

+152
-33
lines changed

5 files changed

+152
-33
lines changed

.github/workflows/build_and_test.yml

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,30 +84,36 @@ jobs:
8484
image: mariadb:11.5
8585
env:
8686
MARIADB_ROOT_PASSWORD: password
87+
MARIADB_DATABASE: vendure
88+
MARIADB_USER: vendure
89+
MARIADB_PASSWORD: password
8790
ports:
8891
- 3306
8992
options: --health-cmd="mariadb-admin ping -h localhost -u vendure -ppassword" --health-interval=10s --health-timeout=5s --health-retries=3
9093
mysql:
9194
image: vendure/mysql-8-native-auth:latest
9295
env:
9396
MYSQL_ROOT_PASSWORD: password
97+
MYSQL_DATABASE: vendure
98+
MYSQL_USER: vendure
99+
MYSQL_PASSWORD: password
94100
ports:
95101
- 3306
96-
options: --health-cmd="mysqladmin ping --silent" --health-interval=10s --health-timeout=20s --health-retries=10
102+
options: --health-cmd="mysqladmin ping -h localhost -u vendure -ppassword" --health-interval=10s --health-timeout=20s --health-retries=10
97103
postgres:
98104
image: postgres:16
99105
env:
100106
POSTGRES_USER: vendure
101107
POSTGRES_PASSWORD: password
102108
ports:
103109
- 5432
104-
options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3
110+
options: --health-cmd="pg_isready -U vendure" --health-interval=10s --health-timeout=5s --health-retries=10
105111
elastic:
106112
image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
107113
env:
108114
discovery.type: single-node
109-
bootstrap.memory_lock: true
110-
ES_JAVA_OPTS: -Xms512m -Xmx512m
115+
bootstrap.memory_lock: "false"
116+
ES_JAVA_OPTS: -Xms256m -Xmx256m
111117
# Elasticsearch will force read-only mode when total available disk space is less than 5%. Since we will
112118
# be running on a shared Azure instance with 84GB SSD, we easily go below 5% available even when there are still
113119
# > 3GB free. So we set this value to an absolute one rather than a percentage to prevent all the Elasticsearch
@@ -117,7 +123,7 @@ jobs:
117123
cluster.routing.allocation.disk.watermark.flood_stage: 100mb
118124
ports:
119125
- 9200
120-
options: --health-cmd="curl --silent --fail localhost:9200/_cluster/health" --health-interval=10s --health-timeout=5s --health-retries=3
126+
options: --health-cmd="curl --silent --fail localhost:9200/_cluster/health" --health-interval=10s --health-timeout=10s --health-retries=5
121127
redis:
122128
image: redis:7.4.1
123129
ports:
@@ -139,6 +145,40 @@ jobs:
139145
npm install --os=linux --cpu=x64 sharp
140146
- name: Build
141147
run: npx lerna run ci
148+
- name: Wait for services to be ready
149+
run: |
150+
echo "Waiting for required services..."
151+
# Wait for redis
152+
for i in {1..60}; do
153+
if nc -z localhost $E2E_REDIS_PORT; then break; fi
154+
sleep 1
155+
done
156+
157+
# Wait for DB ports depending on matrix value
158+
if [ "$DB" = "postgres" ]; then
159+
for i in {1..60}; do
160+
if nc -z localhost $E2E_POSTGRES_PORT; then break; fi
161+
sleep 1
162+
done
163+
elif [ "$DB" = "mariadb" ]; then
164+
for i in {1..60}; do
165+
if nc -z localhost $E2E_MARIADB_PORT; then break; fi
166+
sleep 1
167+
done
168+
elif [ "$DB" = "mysql" ]; then
169+
for i in {1..60}; do
170+
if nc -z localhost $E2E_MYSQL_PORT; then break; fi
171+
sleep 1
172+
done
173+
fi
174+
175+
# Wait for Elasticsearch
176+
for i in {1..60}; do
177+
if curl --silent --fail http://localhost:$E2E_ELASTIC_PORT/_cluster/health >/dev/null 2>&1; then break; fi
178+
sleep 1
179+
done
180+
181+
echo "Services appear reachable."
142182
- name: e2e tests
143183
env:
144184
E2E_MYSQL_PORT: ${{ job.services.mysql.ports['3306'] }}

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ services:
5858
environment:
5959
POSTGRES_DB: vendure-dev
6060
POSTGRES_USER: vendure
61-
POSTGRES_PASSWORD: password
61+
POSTGRES_PASSWORD: postgres
6262
PGDATA: /var/lib/postgresql/data
6363
volumes:
6464
- postgres_16_data:/var/lib/postgresql/data

e2e-common/test-config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ function getDbConfig(): DataSourceOptions {
104104
type: 'mariadb',
105105
host: '127.0.0.1',
106106
port: process.env.CI ? +(process.env.E2E_MARIADB_PORT || 3306) : 3306,
107-
username: 'root',
107+
username: 'vendure',
108108
password: 'password',
109109
};
110110
case 'mysql':
@@ -113,7 +113,7 @@ function getDbConfig(): DataSourceOptions {
113113
type: 'mysql',
114114
host: '127.0.0.1',
115115
port: process.env.CI ? +(process.env.E2E_MYSQL_PORT || 3306) : 3306,
116-
username: 'root',
116+
username: 'vendure',
117117
password: 'password',
118118
};
119119
case 'sqljs':
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { createTestEnvironment } from '@vendure/testing';
2+
import * as path from 'path';
3+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
4+
5+
import { initialData } from '../../e2e-common/e2e-initial-data';
6+
import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../e2e-common/test-config';
7+
8+
describe('Order race conditions', () => {
9+
const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
10+
11+
beforeAll(async () => {
12+
await server.init({
13+
initialData,
14+
productsCsvPath: path.join(__dirname, '../../e2e-common/fixtures/e2e-products-full.csv'),
15+
customerCount: 1,
16+
});
17+
await shopClient.asUserWithCredentials('[email protected]', 'test');
18+
}, TEST_SETUP_TIMEOUT_MS);
19+
20+
afterAll(async () => {
21+
await server.destroy();
22+
});
23+
24+
it('handles parallel modifications to the order correctly', async () => {
25+
const ADD_ITEM_TO_ORDER = `
26+
mutation AddItemToOrder($productVariantId: ID!, $quantity: Int!) {
27+
addItemToOrder(productVariantId: $productVariantId, quantity: $quantity) {
28+
... on Order {
29+
id
30+
totalQuantity
31+
total
32+
}
33+
... on ErrorResult {
34+
errorCode
35+
message
36+
}
37+
}
38+
}
39+
`;
40+
41+
const variantId = 'T_1'; // Laptop 13 inch 8GB
42+
const quantityPerRequest = 1;
43+
const concurrency = 5;
44+
45+
// Ejecutamos 5 peticiones simultáneas para añadir el item
46+
const promises: Array<Promise<any>> = [];
47+
for (let i = 0; i < concurrency; i++) {
48+
promises.push(
49+
shopClient.query(ADD_ITEM_TO_ORDER, {
50+
productVariantId: variantId,
51+
quantity: quantityPerRequest,
52+
}),
53+
);
54+
}
55+
56+
await Promise.all(promises);
57+
58+
const { activeOrder } = await shopClient.query(`
59+
query GetActiveOrder { activeOrder { totalQuantity } }
60+
`);
61+
62+
// Si hay condición de carrera, el total será menor a 5 (algunas escrituras se sobrescribieron)
63+
expect(activeOrder.totalQuantity).toBe(concurrency * quantityPerRequest);
64+
});
65+
});

0 commit comments

Comments
 (0)