| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- import * as fc from 'fast-check';
- /**
- * **Feature: backend-frontend-integration, Property 10: Concurrent Stock Update Safety**
- * **Validates: Requirements 9.3**
- *
- * *For any* concurrent order creation requests for the same product where total
- * requested quantity exceeds stock, at most floor(stock / quantity) orders SHALL succeed.
- */
- // 模拟商品数据
- interface MockProduct {
- id: string;
- name: string;
- stock: number;
- version: number;
- }
- // 模拟库存更新请求
- interface StockUpdateRequest {
- productId: string;
- quantity: number;
- expectedVersion?: number;
- }
- // 模拟更新结果
- interface StockUpdateResult {
- success: boolean;
- productId: string;
- newStock?: number;
- newVersion?: number;
- error?: string;
- }
- /**
- * 纯函数:模拟乐观锁库存更新
- * 模拟并发场景下的库存扣减
- */
- function simulateOptimisticLockUpdate(
- product: MockProduct,
- stockDelta: number,
- expectedVersion?: number
- ): { success: boolean; product?: MockProduct; error?: string } {
- // 检查版本号
- if (expectedVersion !== undefined && product.version !== expectedVersion) {
- return {
- success: false,
- error: `Version conflict: expected ${expectedVersion}, got ${product.version}`
- };
- }
- // 计算新库存
- const newStock = product.stock + stockDelta;
- // 验证库存不能为负
- if (newStock < 0) {
- return {
- success: false,
- error: `Insufficient stock: current ${product.stock}, delta ${stockDelta}`
- };
- }
- // 更新成功
- return {
- success: true,
- product: {
- ...product,
- stock: newStock,
- version: product.version + 1
- }
- };
- }
- /**
- * 纯函数:模拟并发库存扣减
- * 多个请求同时尝试扣减同一商品的库存
- */
- function simulateConcurrentStockDeduction(
- initialProduct: MockProduct,
- requests: StockUpdateRequest[]
- ): {
- successCount: number;
- failCount: number;
- finalStock: number;
- finalVersion: number;
- results: StockUpdateResult[];
- } {
- let currentProduct = { ...initialProduct };
- const results: StockUpdateResult[] = [];
- let successCount = 0;
- let failCount = 0;
- // 模拟并发:每个请求都基于初始版本号尝试更新
- // 只有第一个成功的请求会更新版本号,后续请求会因版本冲突失败
- for (const request of requests) {
- const result = simulateOptimisticLockUpdate(
- currentProduct,
- -request.quantity,
- request.expectedVersion ?? initialProduct.version // 使用初始版本号模拟并发
- );
- if (result.success && result.product) {
- currentProduct = result.product;
- successCount++;
- results.push({
- success: true,
- productId: request.productId,
- newStock: result.product.stock,
- newVersion: result.product.version
- });
- } else {
- failCount++;
- results.push({
- success: false,
- productId: request.productId,
- error: result.error
- });
- }
- }
- return {
- successCount,
- failCount,
- finalStock: currentProduct.stock,
- finalVersion: currentProduct.version,
- results
- };
- }
- /**
- * 纯函数:模拟带重试的并发库存扣减
- * 每个请求在版本冲突时会重试
- */
- function simulateConcurrentWithRetry(
- initialProduct: MockProduct,
- requests: Array<{ quantity: number; maxRetries: number }>
- ): {
- successCount: number;
- totalDeducted: number;
- finalStock: number;
- } {
- let currentProduct = { ...initialProduct };
- let successCount = 0;
- let totalDeducted = 0;
- for (const request of requests) {
- let retries = 0;
- let success = false;
- while (retries <= request.maxRetries && !success) {
- const result = simulateOptimisticLockUpdate(
- currentProduct,
- -request.quantity,
- currentProduct.version // 使用当前版本号
- );
- if (result.success && result.product) {
- currentProduct = result.product;
- successCount++;
- totalDeducted += request.quantity;
- success = true;
- } else if (result.error?.includes('Version conflict')) {
- retries++;
- // 重试时重新获取当前版本
- } else {
- // 库存不足,不重试
- break;
- }
- }
- }
- return {
- successCount,
- totalDeducted,
- finalStock: currentProduct.stock
- };
- }
- describe('ProductService Property Tests', () => {
- describe('Property 10: Concurrent Stock Update Safety', () => {
- it('并发扣减时,成功订单数不超过 floor(stock / quantity)', () => {
- fc.assert(
- fc.property(
- fc.integer({ min: 10, max: 100 }), // 初始库存
- fc.integer({ min: 1, max: 10 }), // 每个订单扣减数量
- fc.integer({ min: 2, max: 10 }), // 并发请求数
- (initialStock, quantity, requestCount) => {
- const product: MockProduct = {
- id: 'test-product',
- name: 'Test Product',
- stock: initialStock,
- version: 0
- };
- // 创建并发请求,都使用初始版本号
- const requests: StockUpdateRequest[] = Array(requestCount).fill(null).map(() => ({
- productId: product.id,
- quantity,
- expectedVersion: 0 // 模拟并发:所有请求都基于初始版本
- }));
- const result = simulateConcurrentStockDeduction(product, requests);
- // 最大可能成功的订单数
- const maxPossibleSuccess = Math.floor(initialStock / quantity);
- // 验证:成功订单数不超过理论最大值
- // 由于乐观锁,实际上只有第一个请求会成功(因为版本冲突)
- return result.successCount <= maxPossibleSuccess;
- }
- ),
- { numRuns: 100 }
- );
- });
- it('乐观锁确保只有一个并发请求成功(无重试)', () => {
- fc.assert(
- fc.property(
- fc.integer({ min: 10, max: 100 }), // 初始库存
- fc.integer({ min: 1, max: 5 }), // 每个订单扣减数量
- fc.integer({ min: 2, max: 10 }), // 并发请求数
- (initialStock, quantity, requestCount) => {
- const product: MockProduct = {
- id: 'test-product',
- name: 'Test Product',
- stock: initialStock,
- version: 0
- };
- // 所有请求都使用相同的初始版本号(模拟真正的并发)
- const requests: StockUpdateRequest[] = Array(requestCount).fill(null).map(() => ({
- productId: product.id,
- quantity,
- expectedVersion: 0
- }));
- const result = simulateConcurrentStockDeduction(product, requests);
- // 由于乐观锁,只有第一个请求会成功
- // 后续请求都会因版本冲突失败
- return result.successCount === 1;
- }
- ),
- { numRuns: 100 }
- );
- });
- it('带重试的并发扣减最终库存一致性', () => {
- fc.assert(
- fc.property(
- fc.integer({ min: 10, max: 100 }), // 初始库存
- fc.integer({ min: 1, max: 5 }), // 每个订单扣减数量
- fc.integer({ min: 2, max: 5 }), // 并发请求数
- (initialStock, quantity, requestCount) => {
- const product: MockProduct = {
- id: 'test-product',
- name: 'Test Product',
- stock: initialStock,
- version: 0
- };
- // 带重试的请求
- const requests = Array(requestCount).fill(null).map(() => ({
- quantity,
- maxRetries: 5
- }));
- const result = simulateConcurrentWithRetry(product, requests);
- // 验证:最终库存 = 初始库存 - 总扣减量
- const expectedFinalStock = initialStock - result.totalDeducted;
-
- // 验证:最终库存不为负
- if (result.finalStock < 0) return false;
-
- // 验证:库存一致性
- return result.finalStock === expectedFinalStock;
- }
- ),
- { numRuns: 100 }
- );
- });
- it('库存不足时扣减失败', () => {
- fc.assert(
- fc.property(
- fc.integer({ min: 1, max: 10 }), // 初始库存
- fc.integer({ min: 11, max: 100 }), // 扣减数量(大于库存)
- (initialStock, quantity) => {
- const product: MockProduct = {
- id: 'test-product',
- name: 'Test Product',
- stock: initialStock,
- version: 0
- };
- const result = simulateOptimisticLockUpdate(product, -quantity);
- // 验证:库存不足时应该失败
- return !result.success && result.error?.includes('Insufficient stock');
- }
- ),
- { numRuns: 100 }
- );
- });
- it('版本冲突时更新失败', () => {
- fc.assert(
- fc.property(
- fc.integer({ min: 10, max: 100 }), // 初始库存
- fc.integer({ min: 1, max: 5 }), // 扣减数量
- fc.integer({ min: 1, max: 10 }), // 当前版本
- fc.integer({ min: 0, max: 10 }), // 期望版本(可能不匹配)
- (initialStock, quantity, currentVersion, expectedVersion) => {
- // 只测试版本不匹配的情况
- if (currentVersion === expectedVersion) return true;
- const product: MockProduct = {
- id: 'test-product',
- name: 'Test Product',
- stock: initialStock,
- version: currentVersion
- };
- const result = simulateOptimisticLockUpdate(product, -quantity, expectedVersion);
- // 验证:版本不匹配时应该失败
- return !result.success && result.error?.includes('Version conflict');
- }
- ),
- { numRuns: 100 }
- );
- });
- it('成功更新后版本号递增', () => {
- fc.assert(
- fc.property(
- fc.integer({ min: 10, max: 100 }), // 初始库存
- fc.integer({ min: 1, max: 5 }), // 扣减数量
- fc.integer({ min: 0, max: 100 }), // 初始版本
- (initialStock, quantity, initialVersion) => {
- const product: MockProduct = {
- id: 'test-product',
- name: 'Test Product',
- stock: initialStock,
- version: initialVersion
- };
- const result = simulateOptimisticLockUpdate(product, -quantity, initialVersion);
- if (result.success && result.product) {
- // 验证:版本号递增1
- return result.product.version === initialVersion + 1;
- }
- return true;
- }
- ),
- { numRuns: 100 }
- );
- });
- it('多次顺序更新的版本号连续递增', () => {
- fc.assert(
- fc.property(
- fc.integer({ min: 100, max: 1000 }), // 初始库存
- fc.array(fc.integer({ min: 1, max: 10 }), { minLength: 1, maxLength: 10 }), // 扣减数量列表
- (initialStock, quantities) => {
- let currentProduct: MockProduct = {
- id: 'test-product',
- name: 'Test Product',
- stock: initialStock,
- version: 0
- };
- let expectedVersion = 0;
- for (const quantity of quantities) {
- const result = simulateOptimisticLockUpdate(
- currentProduct,
- -quantity,
- currentProduct.version
- );
- if (result.success && result.product) {
- expectedVersion++;
- if (result.product.version !== expectedVersion) {
- return false;
- }
- currentProduct = result.product;
- } else {
- // 库存不足,停止
- break;
- }
- }
- return true;
- }
- ),
- { numRuns: 100 }
- );
- });
- it('并发场景下总扣减量不超过初始库存', () => {
- fc.assert(
- fc.property(
- fc.integer({ min: 10, max: 100 }), // 初始库存
- fc.integer({ min: 1, max: 5 }), // 每个订单扣减数量
- fc.integer({ min: 2, max: 10 }), // 并发请求数
- (initialStock, quantity, requestCount) => {
- const product: MockProduct = {
- id: 'test-product',
- name: 'Test Product',
- stock: initialStock,
- version: 0
- };
- const requests = Array(requestCount).fill(null).map(() => ({
- quantity,
- maxRetries: 10
- }));
- const result = simulateConcurrentWithRetry(product, requests);
- // 验证:总扣减量不超过初始库存
- return result.totalDeducted <= initialStock;
- }
- ),
- { numRuns: 100 }
- );
- });
- });
- });
|