concurrency.mjs 5.2 KB

1234567891011121314151617181920212223242526272829
  1. import fs from 'node:fs';import assert from 'node:assert/strict';import {randomBytes} from 'node:crypto';import {withGateway} from './server-util.mjs';
  2. const schema='speed_fixture_'+randomBytes(8).toString('hex'),tables=['CommonModel','CourseAppointment','LessonRecord','MemoryPracticeRecord','LessonRecordFact','ReviewReward','ReviewRewardCorrection','TeachingCompensationRule','CourseTeachingProfile','CoachPayRate','TeachingCourseCategory','PayrollLine','PayrollBatch','LegacyDirectRun','LegacyDirectEvidence','LegacySyncConflict','PayrollCutoffDecision','PayrollCutoffApproval','_User','Node','CourseBinding'];
  3. let setup=`CREATE SCHEMA ${schema};SET LOCAL search_path=${schema},public;SET LOCAL TimeZone='Etc/GMT-8';SET LOCAL extra_float_digits=1;`;
  4. for(const tab of tables)setup+=`CREATE TABLE "${tab}"("objectId" text PRIMARY KEY,company text,value text,"modelId" numeric DEFAULT 54);INSERT INTO "${tab}"("objectId",company,value) VALUES('a','a','old'),('b','b','other');`;
  5. setup+=`INSERT INTO "CommonModel"("objectId",company,value,"modelId") VALUES('r60','a','old',60.0),('r59','a','old',59.0);CREATE TABLE "Function"(path text,code text);INSERT INTO "Function" VALUES('xiaoshu/ops/gateway-v3','fixture');CREATE VIEW "ReviewRewardIncome" AS SELECT * FROM "ReviewReward";`;
  6. setup+=['payroll-incremental-reads.sql','payroll-verified-preview.sql'].map(f=>fs.readFileSync('scripts/sql/'+f,'utf8').replaceAll('public.',schema+'.').replaceAll("'public'",String.fromCharCode(39)+schema+String.fromCharCode(39))).join('\n');
  7. setup+=fs.readFileSync('scripts/sql/payroll-source-fast.sql','utf8').match(/CREATE OR REPLACE FUNCTION xs_payroll_sources_current[\s\S]*?\n\$\$;/)[0];
  8. async function run(p){
  9. if(p.action==='setup')return Psql.tx(async tx=>{await tx.none(setup);return true;});
  10. if(p.action==='drop'){await Psql.none('DROP SCHEMA '+schema+' CASCADE');return true;}
  11. return Psql.tx(async tx=>{
  12. await tx.none(`SET LOCAL search_path=${schema},public;SET LOCAL TimeZone='Etc/GMT-8';SET LOCAL extra_float_digits=1;`);
  13. if(p.action==='read')return tx.one("SELECT xs_payroll_data_version('a') AS digest,xs_payroll_preview_cache_read('a','month:11') AS cache");
  14. if(p.action==='write-cache')return tx.one("SELECT xs_payroll_preview_cache_write('a','month:11',$1,$2::jsonb) AS saved",[p.stamp,JSON.stringify({snapshot:p.snapshot,result:{amount:99}})]);
  15. if(p.action==='source-current')return tx.one("SELECT xs_payroll_sources_current('a',$1) AS current",[p.snapshot]);
  16. if(p.action==='scope-change'){const tables={review:'MemoryPracticeRecord',appointment:'CourseAppointment',lesson:'LessonRecord'};if(tables[p.scope])await tx.none('UPDATE \"'+tables[p.scope]+'\" SET value=$1 WHERE company=$2',[p.value,'a']);else await tx.none('UPDATE "CommonModel" SET "modelId"=$1::numeric,value=$2 WHERE "objectId"=$3',[p.scope==='common54'?'54.0':p.scope==='common59'?'59.0':'60.0',p.value,p.scope==='common54'?'a':p.scope==='common59'?'r59':'r60']);return true;}
  17. if(p.action==='quick'){await tx.none('UPDATE "CourseAppointment" SET value=$1 WHERE company=$2',['later commit','a']);return true;}
  18. if(p.action==='mutate'){await tx.none('UPDATE "LessonRecord" SET value=$1 WHERE company=$2',[p.value,'a']);await tx.one('SELECT pg_sleep(2.5)');if(p.rollback)throw Error('intentional rollback');return true;}
  19. throw Error('Unknown fixture operation');
  20. });
  21. }
  22. const source='const schema='+JSON.stringify(schema)+';const setup='+JSON.stringify(setup)+';'+run.toString();
  23. const results={};await withGateway(source,async call=>{await call({action:'setup'});try{
  24. const original=await call({action:'read'});assert.equal((await call({action:'write-cache',stamp:original.cache.stamp,snapshot:original.cache.snapshot})).saved,true);
  25. const mutation=call({action:'mutate',value:'committed'});await new Promise(r=>setTimeout(r,300));const during=await call({action:'read'});assert.equal(during.digest,original.digest);assert.equal(during.cache.payload.amount,99);await call({action:'quick'});const later=await call({action:'read'});assert.equal((await call({action:'write-cache',stamp:later.cache.stamp,snapshot:later.cache.snapshot})).saved,true);assert.equal((await call({action:'read'})).cache.payload.amount,99);await mutation;results.outOfOrderCommit=true;
  26. const after=await call({action:'read'});assert.notEqual(after.digest,original.digest);assert.equal(after.cache.payload,null);assert.equal((await call({action:'write-cache',stamp:original.cache.stamp,snapshot:original.cache.snapshot})).saved,false);results.commitVisibility=true;results.racingPublishRejected=true;
  27. await assert.rejects(call({action:'mutate',value:'rolled back',rollback:true}),/intentional rollback/);assert.equal((await call({action:'read'})).digest,after.digest);results.rollback=true;for(const scope of ['review','common60','common54','common59','appointment','lesson']){const saved=(await call({action:'read'})).cache.snapshot;await call({action:'scope-change',scope,value:'scope-'+scope});assert.equal((await call({action:'source-current',snapshot:saved})).current,['review','common60'].includes(scope));}results.scopedInvalidation=true;
  28. }finally{await call({action:'drop'});}});
  29. fs.writeFileSync('perf/admin-speed-round2-2026-11-10/concurrency-result.json',JSON.stringify({at:new Date().toISOString(),isolatedSchemaDropped:true,...results},null,2)+'\n');console.log(JSON.stringify(results));