fix(corrections): race condition modele_status + logs diagnostiques
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
63bc43ddcf
commit
14d8d73991
2 changed files with 65 additions and 18 deletions
|
|
@ -138,7 +138,9 @@ describe('correctionController.correctEE — Sprint 3.6a', () => {
|
||||||
expect(result.data.score).toBe(14)
|
expect(result.data.score).toBe(14)
|
||||||
}
|
}
|
||||||
|
|
||||||
// La persistance de la correction inclut les nouveaux champs + statuts pending
|
// La persistance de la correction inclut les nouveaux champs.
|
||||||
|
// Les statuts ne sont PAS dans l'update principal (race condition — les
|
||||||
|
// jobs async les pilotent exclusivement). DEFAULT 'pending' côté migration.
|
||||||
const persisted = supabaseMock.updates.find(
|
const persisted = supabaseMock.updates.find(
|
||||||
(u) => u.table === 'productions' && u.data.score !== undefined,
|
(u) => u.table === 'productions' && u.data.score !== undefined,
|
||||||
)
|
)
|
||||||
|
|
@ -147,13 +149,14 @@ describe('correctionController.correctEE — Sprint 3.6a', () => {
|
||||||
score: 14,
|
score: 14,
|
||||||
nclc: 9,
|
nclc: 9,
|
||||||
nclc_cible: 9,
|
nclc_cible: 9,
|
||||||
exercices_status: 'pending',
|
|
||||||
modele_status: 'pending',
|
|
||||||
})
|
})
|
||||||
expect(persisted!.data.revelation).toBeDefined()
|
expect(persisted!.data.revelation).toBeDefined()
|
||||||
expect(persisted!.data.diagnostic).toBeDefined()
|
expect(persisted!.data.diagnostic).toBeDefined()
|
||||||
expect(persisted!.data.conseil_nclc).toBeDefined()
|
expect(persisted!.data.conseil_nclc).toBeDefined()
|
||||||
expect(persisted!.data.erreurs_codes).toBeDefined()
|
expect(persisted!.data.erreurs_codes).toBeDefined()
|
||||||
|
// Race condition : ces champs ne doivent PAS être touchés par l'update principal.
|
||||||
|
expect(persisted!.data.modele_status).toBeUndefined()
|
||||||
|
expect(persisted!.data.exercices_status).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('modele_status passe à "ready" quand le job réussit', async () => {
|
it('modele_status passe à "ready" quand le job réussit', async () => {
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,13 @@ export async function correctEE(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Persister la correction (statuts modèle/exercices : pending — les jobs sont lancés juste après)
|
// 4. Persister la correction.
|
||||||
|
// ⚠️ RACE CONDITION — ne PAS inclure `modele_status` ni `exercices_status`
|
||||||
|
// ici : runModeleJob (lancé en parallèle option b) peut avoir déjà terminé
|
||||||
|
// et écrit 'ready' avant que cet update ne s'exécute. Écraser avec 'pending'
|
||||||
|
// perdrait le résultat.
|
||||||
|
// Les colonnes *_status sont initialisées à 'pending' par la migration
|
||||||
|
// (DEFAULT) et gérées exclusivement par runModeleJob / runExercicesJob.
|
||||||
const { error: updateError } = await supabase
|
const { error: updateError } = await supabase
|
||||||
.from('productions')
|
.from('productions')
|
||||||
.update({
|
.update({
|
||||||
|
|
@ -149,8 +155,6 @@ export async function correctEE(
|
||||||
conseil_nclc: rapport.conseil_nclc,
|
conseil_nclc: rapport.conseil_nclc,
|
||||||
erreurs_codes: rapport.erreurs_codes,
|
erreurs_codes: rapport.erreurs_codes,
|
||||||
rapport: JSON.stringify(rapport),
|
rapport: JSON.stringify(rapport),
|
||||||
exercices_status: 'pending',
|
|
||||||
modele_status: 'pending',
|
|
||||||
})
|
})
|
||||||
.eq('id', simulationId)
|
.eq('id', simulationId)
|
||||||
|
|
||||||
|
|
@ -190,17 +194,35 @@ interface ModeleJobInput {
|
||||||
|
|
||||||
async function runModeleJob(input: ModeleJobInput): Promise<void> {
|
async function runModeleJob(input: ModeleJobInput): Promise<void> {
|
||||||
const { simulationId, tache, sujet, texte, nclcObtenu } = input
|
const { simulationId, tache, sujet, texte, nclcObtenu } = input
|
||||||
|
console.log('[runModeleJob] START', { simulationId, tache, nclcObtenu })
|
||||||
try {
|
try {
|
||||||
const modele = await generateProductionModele({ tache, sujet, texte, nclcObtenu })
|
const modele = await generateProductionModele({ tache, sujet, texte, nclcObtenu })
|
||||||
await supabase
|
console.log('[runModeleJob] DeepSeek OK, updating productions', {
|
||||||
|
simulationId,
|
||||||
|
modeleWordCount: modele.tcf_word_count,
|
||||||
|
})
|
||||||
|
const { error: updateErr, data: updateData } = await supabase
|
||||||
.from('productions')
|
.from('productions')
|
||||||
.update({ modele, modele_status: 'ready' })
|
.update({ modele, modele_status: 'ready' })
|
||||||
.eq('id', simulationId)
|
.eq('id', simulationId)
|
||||||
} catch {
|
.select('id, modele_status')
|
||||||
await supabase
|
console.log('[runModeleJob] update result', { simulationId, updateErr, updateData })
|
||||||
.from('productions')
|
} catch (err) {
|
||||||
.update({ modele_status: 'error' })
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
.eq('id', simulationId)
|
const stack = err instanceof Error ? err.stack : undefined
|
||||||
|
console.error('[runModeleJob] CAUGHT ERROR', { simulationId, message, stack })
|
||||||
|
try {
|
||||||
|
const { error: fallbackErr } = await supabase
|
||||||
|
.from('productions')
|
||||||
|
.update({ modele_status: 'error' })
|
||||||
|
.eq('id', simulationId)
|
||||||
|
console.log('[runModeleJob] fallback update result', { simulationId, fallbackErr })
|
||||||
|
} catch (fallbackExc) {
|
||||||
|
console.error('[runModeleJob] FALLBACK UPDATE THREW', {
|
||||||
|
simulationId,
|
||||||
|
message: fallbackExc instanceof Error ? fallbackExc.message : String(fallbackExc),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,21 +234,43 @@ interface ExercicesJobInput {
|
||||||
|
|
||||||
async function runExercicesJob(input: ExercicesJobInput): Promise<void> {
|
async function runExercicesJob(input: ExercicesJobInput): Promise<void> {
|
||||||
const { simulationId, tache, rapport } = input
|
const { simulationId, tache, rapport } = input
|
||||||
|
console.log('[runExercicesJob] START', {
|
||||||
|
simulationId,
|
||||||
|
tache,
|
||||||
|
erreursCodesCount: rapport.erreurs_codes.length,
|
||||||
|
})
|
||||||
try {
|
try {
|
||||||
const exercices = await generateExercices({
|
const exercices = await generateExercices({
|
||||||
tache,
|
tache,
|
||||||
erreursCodes: rapport.erreurs_codes,
|
erreursCodes: rapport.erreurs_codes,
|
||||||
criteres: rapport.criteres,
|
criteres: rapport.criteres,
|
||||||
})
|
})
|
||||||
await supabase
|
console.log('[runExercicesJob] DeepSeek OK, updating productions', {
|
||||||
|
simulationId,
|
||||||
|
exercicesCount: exercices.length,
|
||||||
|
})
|
||||||
|
const { error: updateErr, data: updateData } = await supabase
|
||||||
.from('productions')
|
.from('productions')
|
||||||
.update({ exercices, exercices_status: 'ready' })
|
.update({ exercices, exercices_status: 'ready' })
|
||||||
.eq('id', simulationId)
|
.eq('id', simulationId)
|
||||||
} catch {
|
.select('id, exercices_status')
|
||||||
await supabase
|
console.log('[runExercicesJob] update result', { simulationId, updateErr, updateData })
|
||||||
.from('productions')
|
} catch (err) {
|
||||||
.update({ exercices_status: 'error' })
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
.eq('id', simulationId)
|
const stack = err instanceof Error ? err.stack : undefined
|
||||||
|
console.error('[runExercicesJob] CAUGHT ERROR', { simulationId, message, stack })
|
||||||
|
try {
|
||||||
|
const { error: fallbackErr } = await supabase
|
||||||
|
.from('productions')
|
||||||
|
.update({ exercices_status: 'error' })
|
||||||
|
.eq('id', simulationId)
|
||||||
|
console.log('[runExercicesJob] fallback update result', { simulationId, fallbackErr })
|
||||||
|
} catch (fallbackExc) {
|
||||||
|
console.error('[runExercicesJob] FALLBACK UPDATE THREW', {
|
||||||
|
simulationId,
|
||||||
|
message: fallbackExc instanceof Error ? fallbackExc.message : String(fallbackExc),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue