Skip to content
Open
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
63 changes: 58 additions & 5 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,30 +84,39 @@ jobs:
image: mariadb:11.5
env:
MARIADB_ROOT_PASSWORD: password
MARIADB_DATABASE: vendure
MARIADB_USER: vendure
MARIADB_PASSWORD: password
# Ensure InnoDB is used for locking support
MARIADB_MYSQL_LOCALHOST_USER: 1
MARIADB_INITDB_SKIP_TZINFO: 1
ports:
- 3306
options: --health-cmd="mariadb-admin ping -h localhost -u vendure -ppassword" --health-interval=10s --health-timeout=5s --health-retries=3
mysql:
image: vendure/mysql-8-native-auth:latest
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: vendure
MYSQL_USER: vendure
MYSQL_PASSWORD: password
ports:
- 3306
options: --health-cmd="mysqladmin ping --silent" --health-interval=10s --health-timeout=20s --health-retries=10
options: --health-cmd="mysqladmin ping -h localhost -u vendure -ppassword" --health-interval=10s --health-timeout=20s --health-retries=10
postgres:
image: postgres:16
env:
POSTGRES_USER: vendure
POSTGRES_PASSWORD: password
ports:
- 5432
options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3
options: --health-cmd="pg_isready -U vendure" --health-interval=10s --health-timeout=5s --health-retries=10
elastic:
image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
env:
discovery.type: single-node
bootstrap.memory_lock: true
ES_JAVA_OPTS: -Xms512m -Xmx512m
bootstrap.memory_lock: "false"
ES_JAVA_OPTS: -Xms256m -Xmx256m
# Elasticsearch will force read-only mode when total available disk space is less than 5%. Since we will
# be running on a shared Azure instance with 84GB SSD, we easily go below 5% available even when there are still
# > 3GB free. So we set this value to an absolute one rather than a percentage to prevent all the Elasticsearch
Expand All @@ -117,7 +126,7 @@ jobs:
cluster.routing.allocation.disk.watermark.flood_stage: 100mb
ports:
- 9200
options: --health-cmd="curl --silent --fail localhost:9200/_cluster/health" --health-interval=10s --health-timeout=5s --health-retries=3
options: --health-cmd="curl --silent --fail localhost:9200/_cluster/health" --health-interval=10s --health-timeout=10s --health-retries=5
redis:
image: redis:7.4.1
ports:
Expand All @@ -139,6 +148,50 @@ jobs:
npm install --os=linux --cpu=x64 sharp
- name: Build
run: npx lerna run ci
- name: Wait for services to be ready
env:
E2E_MYSQL_PORT: ${{ job.services.mysql.ports['3306'] }}
E2E_MARIADB_PORT: ${{ job.services.mariadb.ports['3306'] }}
E2E_POSTGRES_PORT: ${{ job.services.postgres.ports['5432'] }}
E2E_ELASTIC_PORT: ${{ job.services.elastic.ports['9200'] }}
E2E_REDIS_PORT: ${{ job.services.redis.ports['6379'] }}
DB: ${{ matrix.db }}
run: |
echo "Waiting for required services..."
# Wait for redis
for i in {1..30}; do
if nc -z localhost $E2E_REDIS_PORT; then echo "Redis Ready"; break; fi
if [ $i -eq 30 ]; then echo "Redis Timeout"; exit 1; fi
sleep 1
done

# Wait for DB ports depending on matrix value
if [ "$DB" = "postgres" ]; then
for i in {1..30}; do
if nc -z localhost $E2E_POSTGRES_PORT; then echo "Postgres Ready"; break; fi
if [ $i -eq 30 ]; then echo "Postgres Timeout"; exit 1; fi
sleep 1
done
elif [ "$DB" = "mariadb" ]; then
for i in {1..30}; do
if nc -z localhost $E2E_MARIADB_PORT; then echo "MariaDB Ready"; break; fi
if [ $i -eq 30 ]; then echo "MariaDB Timeout"; exit 1; fi
sleep 1
done
elif [ "$DB" = "mysql" ]; then
for i in {1..30}; do
if nc -z localhost $E2E_MYSQL_PORT; then echo "MySQL Ready"; break; fi
if [ $i -eq 30 ]; then echo "MySQL Timeout"; exit 1; fi
sleep 1
done
fi

# Wait for Elasticsearch
for i in {1..30}; do
if curl --silent --fail "http://localhost:$E2E_ELASTIC_PORT/_cluster/health?wait_for_status=yellow&timeout=1s" >/dev/null 2>&1; then echo "Elasticsearch Ready"; break; fi
if [ $i -eq 30 ]; then echo "Elasticsearch Timeout"; exit 1; fi
sleep 1
done
- name: e2e tests
env:
E2E_MYSQL_PORT: ${{ job.services.mysql.ports['3306'] }}
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ services:
MARIADB_USER: vendure
MARIADB_PASSWORD: password
MARIADB_ROOT_PASSWORD: password
command: ["mysqld", "--innodb_file_per_table=1", "--default-storage-engine=InnoDB"]
volumes:
- 'mariadb_data:/var/lib/mysql'
ports:
Expand Down
40 changes: 38 additions & 2 deletions e2e-common/test-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,24 +97,60 @@ function getDbConfig(): DataSourceOptions {
port: process.env.CI ? +(process.env.E2E_POSTGRES_PORT || 5432) : 5432,
username: 'vendure',
password: 'password',
// Connection timeouts to prevent aborted connections
connectTimeout: 60000,
// Keep connections alive
keepConnectionAlive: true,
extra: {
// Connection pool settings
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 60000,
},
};
case 'mariadb':
return {
synchronize: true,
type: 'mariadb',
host: '127.0.0.1',
port: process.env.CI ? +(process.env.E2E_MARIADB_PORT || 3306) : 3306,
username: 'root',
username: 'vendure',
password: 'password',
extra: {
// Ensure tables use InnoDB for locking support
initSql: "SET default_storage_engine=InnoDB;",
// Connection pool settings
connectionLimit: 10,
waitForConnections: true,
queueLimit: 0,
},
Comment on lines +119 to +126
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check if initSql is used anywhere in the codebase
rg -i "initSql" --type ts --type js

Repository: vendurehq/vendure

Length of output: 246


🏁 Script executed:

# Check the surrounding code context for mysql2 configuration patterns
cd e2e-common && head -150 test-config.ts | tail -50

Repository: vendurehq/vendure

Length of output: 2087


🌐 Web query:

mysql2 connection options initSql documentation

💡 Result:

Short answer: mysql2 has no built‑in connection option named initSql / initSQL. To run initialization SQL when a connection is created, acquire the connection and run the statements (or listen for pool events and run them on new connections). See mysql2 docs for createPool/createConnection and examples, and the classic node-mysql pool "connection" event pattern for running session init statements. [1][2]

References

  • mysql2 docs (createConnection / createPool examples). [1]
  • node-mysql (pool events) — use pool.on('connection', conn => conn.query('SET ...')) as the pattern to run init SQL on new pooled connections. [2]

🌐 Web query:

TypeORM mysql2 driver initialization commands SET storage engine

💡 Result:

Use the TypeORM DataSource option extra to pass the driver's initial SQL command (the underlying mysql/mysql2 client will run it on connect). Example — set default storage engine to InnoDB:

TypeScript:
const AppDataSource = new DataSource({
type: "mysql",
host: "localhost",
port: 3306,
username: "user",
password: "pw",
database: "db",
extra: {
init_command: "SET default_storage_engine=INNODB"
}
});

Notes:

  • TypeORM forwards extra to the mysql/mysql2 client. [1][2]
  • The driver/client executes init_command on each new connection (so remove it after migrations if you don't want it run for every session). [2][3]

Sources:
[1] TypeORM — MySQL driver docs (data-source / connection options).
[2] TypeORM — DataSource / DataSourceOptions (extra passes options to underlying driver).
[3] mysql client docs (init_command / initial command executed on connect).


Replace initSql with init_command in both MariaDB and MySQL configurations.

The option initSql is not recognized by mysql2 or TypeORM. The correct option name is init_command. Without this correction, the storage engine setting is silently ignored, and tables may default to MyISAM on some MySQL configurations, which does not support row-level locking and will break the pessimistic locking (SELECT ... FOR UPDATE) implementation.

Change both occurrences from:

initSql: "SET default_storage_engine=InnoDB;",

to:

init_command: "SET default_storage_engine=INNODB",

This applies to lines 119-126 (MariaDB) and 141-148 (MySQL).

🤖 Prompt for AI Agents
In @e2e-common/test-config.ts around lines 119 - 126, Replace the unrecognized
option name initSql with the correct init_command in the configuration objects
inside the extra block (the object that currently contains initSql,
connectionLimit, waitForConnections, queueLimit); update both occurrences used
for MariaDB and MySQL so the storage engine is actually set (change initSql:
"SET default_storage_engine=InnoDB;" to init_command: "SET
default_storage_engine=INNODB").

// Connection timeouts to prevent aborted connections
connectTimeout: 60000,
acquireTimeout: 60000,
// Keep connections alive
keepConnectionAlive: true,
};
case 'mysql':
return {
synchronize: true,
type: 'mysql',
host: '127.0.0.1',
port: process.env.CI ? +(process.env.E2E_MYSQL_PORT || 3306) : 3306,
username: 'root',
username: 'vendure',
password: 'password',
extra: {
// Ensure tables use InnoDB for locking support
initSql: "SET default_storage_engine=InnoDB;",
// Connection pool settings
connectionLimit: 10,
waitForConnections: true,
queueLimit: 0,
},
// Connection timeouts to prevent aborted connections
connectTimeout: 60000,
acquireTimeout: 60000,
// Keep connections alive
keepConnectionAlive: true,
};
case 'sqljs':
default:
Expand Down
65 changes: 65 additions & 0 deletions packages/core/e2e/race-condition.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { createTestEnvironment } from '@vendure/testing';
import * as path from 'path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

import { initialData } from '../../e2e-common/e2e-initial-data';
import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../e2e-common/test-config';

describe('Order race conditions', () => {
const { server, adminClient, shopClient } = createTestEnvironment(testConfig);

beforeAll(async () => {
await server.init({
initialData,
productsCsvPath: path.join(__dirname, '../../e2e-common/fixtures/e2e-products-full.csv'),
customerCount: 1,
});
await shopClient.asUserWithCredentials('[email protected]', 'test');
}, TEST_SETUP_TIMEOUT_MS);

afterAll(async () => {
await server.destroy();
});

it('handles parallel modifications to the order correctly', async () => {
const ADD_ITEM_TO_ORDER = `
mutation AddItemToOrder($productVariantId: ID!, $quantity: Int!) {
addItemToOrder(productVariantId: $productVariantId, quantity: $quantity) {
... on Order {
id
totalQuantity
total
}
... on ErrorResult {
errorCode
message
}
}
}
`;

const variantId = 'T_1'; // Laptop 13 inch 8GB
const quantityPerRequest = 1;
const concurrency = 5;

// Executing 5 Simulataneous requests to add an item
const promises: Array<Promise<any>> = [];
for (let i = 0; i < concurrency; i++) {
promises.push(
shopClient.query(ADD_ITEM_TO_ORDER, {
productVariantId: variantId,
quantity: quantityPerRequest,
}),
);
}

await Promise.all(promises);

const { activeOrder } = await shopClient.query(`
query GetActiveOrder { activeOrder { totalQuantity } }
`);

// Si hay condición de carrera, el total será menor a 5 (algunas escrituras se sobrescribieron)
expect(activeOrder.totalQuantity).toBe(concurrency * quantityPerRequest);
});
});
Loading