Context
oorq suporta jobs amb on_commit=True: el job no s'ha d'encuar fins que la transacció d'OpenERP confirma el commit. Aquesta semàntica és correcta i necessària per evitar que els workers vegin dades no commitejades o jobs associats a transaccions que finalment fan rollback.
El problema actual és que, quan una transacció genera molts jobs on_commit=True, el procés ERP acumula massa dades en memòria i pot petar la RAM.
Implementació actual
A oorq/decorators.py, ProcessJobs manté una estructura global:
class ProcessJobs(object):
JOBS_TO_PROCESS = {}
Els jobs pendents es guarden per id(cursor):
JobToProcess = namedtuple('JobToProcess', ['job', 'queue', 'at_front'])
Amb on_commit=True, el codi fa aproximadament:
job = Job.create(...)
ProcessJobs.add_job(transaction_id, job, q, self.at_front)
job.meta['requeue'] = self.requeue
job.save()
set_hash_job(job)
I al commit:
for job, queue, at_front in jobs:
queue.enqueue_job(job, at_front=at_front)
És a dir:
- el job es crea com a objecte
rq.Job,
- el job es guarda a Redis amb
job.save(),
- però encara no s'afegeix a la queue fins al commit,
- mentrestant, el procés Python manté en memòria la instància completa de
Job dins JOBS_TO_PROCESS.
Què es guarda en memòria
Per cada job pendent de commit es manté:
JobToProcess,
- la instància completa
rq.Job,
- l'objecte
Queue,
at_front,
- i, dins el
Job, els arguments serialitzables:
(
conf_attrs, # còpia de tools.config.options
dbname,
uid,
osv_object,
fname,
) + args[3:]
A més:
kwargs, incloent context, sudo, current_task_id, etc.
meta, timeout, result_ttl, depends_on, id, estat intern RQ...
set_hash_job(job) crida job.get_call_string(), que pot construir una representació textual dels arguments i provocar pics temporals de memòria/CPU.
El punt delicat és que conf_attrs i args/kwargs queden enganxats a cada instància Job. Si hi ha molts jobs dins una mateixa transacció, la RAM creix linealment.
Estimació de RAM
Depèn molt del payload real de cada job:
- job petit: ~5-20 KB/job,
- job amb
context, vals, llistes d'IDs o payloads mitjans: ~20-100 KB/job,
- job amb payload gran: centenars de KB/job.
Exemples orientatius:
10.000 jobs * 10 KB = ~100 MB
50.000 jobs * 10 KB = ~500 MB
100.000 jobs * 20 KB = ~2 GB
Si els jobs porten arguments grans, el problema pot aparèixer amb pocs milers de jobs.
Proposta incremental
No canviar encara a un outbox transaccional complet. Com a primera fase, reduir la memòria mantenint la semàntica actual.
Canvi proposat
En lloc de guardar això en memòria:
JobToProcess(job, queue, at_front)
guardar només dades mínimes:
JobToProcess(job_id, queue_name, at_front)
El job ja existeix a Redis perquè el codi actual fa job.save() abans del commit. Per tant, al commit es pot recuperar:
job = Job.fetch(job_id, connection=redis_conn)
queue = Queue(queue_name, connection=redis_conn)
queue.enqueue_job(job, at_front=at_front)
Això evita retenir la instància completa rq.Job, els seus args/kwargs i la queue en memòria durant tota la transacció.
Rollback i savepoints
Ara mateix, si hi ha rollback, els jobs no s'encuen, però com que ja s'han guardat a Redis poden quedar jobs orfes no enqueued.
Amb el canvi proposat, aprofitar que tenim job_id i netejar explícitament:
- en
rollback: fer Job.fetch(job_id).delete() per tots els jobs pendents de la transacció,
- en
rollback_savepoint: eliminar de Redis els jobs descartats pel savepoint,
- en
commit: recuperar job de Redis i encuar-lo.
No afegir de moment transaction_id ni transaction_uuid al meta; s'ha descartat aquesta part per no barrejar una identitat de transacció poc clara amb el contracte actual.
Limitacions conegudes
Aquesta fase no soluciona perfectament el cas en què el procés mor entre job.save() i commit/rollback.
En aquest cas poden quedar jobs a Redis que no estan en cap queue. Això ja pot passar amb el comportament actual. La proposta, com a mínim, redueix RAM i neteja millor en rollback/savepoint normals.
Per cobrir crashes caldria una fase posterior amb cleanup periòdic o un patró d'outbox transaccional persistent.
Fases de solució
Fase 1 — Reduir memòria sense canviar arquitectura
Fase 2 — Observabilitat i protecció
Fase 3 — Neteja d'orfes Redis
Fase 4 — Outbox transaccional persistent, si cal
Si el volum de jobs continua sent molt alt o es necessita semàntica robusta davant crashes, migrar a un patró d'outbox en PostgreSQL:
on_commit=True insereix una fila en una taula outbox dins la mateixa transacció ERP,
- si hi ha rollback, PostgreSQL elimina automàticament la fila,
- un drainer idempotent encola a Redis per lots,
- suport de retry, estat, errors i batching.
Aquesta fase és més neta arquitectònicament, però implica més canvi. No és necessària per atacar el problema immediat de RAM.
Criteris d'acceptació
Context
oorqsuporta jobs ambon_commit=True: el job no s'ha d'encuar fins que la transacció d'OpenERP confirma elcommit. Aquesta semàntica és correcta i necessària per evitar que els workers vegin dades no commitejades o jobs associats a transaccions que finalment fan rollback.El problema actual és que, quan una transacció genera molts jobs
on_commit=True, el procés ERP acumula massa dades en memòria i pot petar la RAM.Implementació actual
A
oorq/decorators.py,ProcessJobsmanté una estructura global:Els jobs pendents es guarden per
id(cursor):Amb
on_commit=True, el codi fa aproximadament:I al commit:
És a dir:
rq.Job,job.save(),JobdinsJOBS_TO_PROCESS.Què es guarda en memòria
Per cada job pendent de commit es manté:
JobToProcess,rq.Job,Queue,at_front,Job, els arguments serialitzables:( conf_attrs, # còpia de tools.config.options dbname, uid, osv_object, fname, ) + args[3:]A més:
kwargs, incloentcontext,sudo,current_task_id, etc.meta,timeout,result_ttl,depends_on,id, estat intern RQ...set_hash_job(job)cridajob.get_call_string(), que pot construir una representació textual dels arguments i provocar pics temporals de memòria/CPU.El punt delicat és que
conf_attrsiargs/kwargsqueden enganxats a cada instànciaJob. Si hi ha molts jobs dins una mateixa transacció, la RAM creix linealment.Estimació de RAM
Depèn molt del payload real de cada job:
context,vals, llistes d'IDs o payloads mitjans: ~20-100 KB/job,Exemples orientatius:
Si els jobs porten arguments grans, el problema pot aparèixer amb pocs milers de jobs.
Proposta incremental
No canviar encara a un outbox transaccional complet. Com a primera fase, reduir la memòria mantenint la semàntica actual.
Canvi proposat
En lloc de guardar això en memòria:
guardar només dades mínimes:
El job ja existeix a Redis perquè el codi actual fa
job.save()abans del commit. Per tant, al commit es pot recuperar:Això evita retenir la instància completa
rq.Job, els seus args/kwargs i la queue en memòria durant tota la transacció.Rollback i savepoints
Ara mateix, si hi ha rollback, els jobs no s'encuen, però com que ja s'han guardat a Redis poden quedar jobs orfes no enqueued.
Amb el canvi proposat, aprofitar que tenim
job_idi netejar explícitament:rollback: ferJob.fetch(job_id).delete()per tots els jobs pendents de la transacció,rollback_savepoint: eliminar de Redis els jobs descartats pel savepoint,commit: recuperar job de Redis i encuar-lo.No afegir de moment
transaction_idnitransaction_uuidalmeta; s'ha descartat aquesta part per no barrejar una identitat de transacció poc clara amb el contracte actual.Limitacions conegudes
Aquesta fase no soluciona perfectament el cas en què el procés mor entre
job.save()icommit/rollback.En aquest cas poden quedar jobs a Redis que no estan en cap queue. Això ja pot passar amb el comportament actual. La proposta, com a mínim, redueix RAM i neteja millor en rollback/savepoint normals.
Per cobrir crashes caldria una fase posterior amb cleanup periòdic o un patró d'outbox transaccional persistent.
Fases de solució
Fase 1 — Reduir memòria sense canviar arquitectura
JobToProcessper guardarjob_id,queue_name,at_fronten comptes dejob,queue,at_front.commit, recuperarJobiQueuedes de Redis abans d'encuar.rollback, eliminar de Redis els jobs pendents no enqueued.rollback_savepoint, eliminar de Redis els jobs descartats pel savepoint.Fase 2 — Observabilitat i protecció
on_commit.Fase 3 — Neteja d'orfes Redis
Fase 4 — Outbox transaccional persistent, si cal
Si el volum de jobs continua sent molt alt o es necessita semàntica robusta davant crashes, migrar a un patró d'outbox en PostgreSQL:
on_commit=Trueinsereix una fila en una taula outbox dins la mateixa transacció ERP,Aquesta fase és més neta arquitectònicament, però implica més canvi. No és necessària per atacar el problema immediat de RAM.
Criteris d'acceptació
on_commit=Truedins una transacció no reté instàncies completes derq.Joben memòria.job_id,queue_name,at_fronti savepoints).commit.rollback.rollback_savepointno queden enqueued i es netegen de Redis.on_commit=True.