Skip to content

Reducir consumo de RAM en jobs on_commit guardando referencias mínimas #134

@pilipilisbot

Description

@pilipilisbot

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

  • Canviar JobToProcess per guardar job_id, queue_name, at_front en comptes de job, queue, at_front.
  • Al commit, recuperar Job i Queue des de Redis abans d'encuar.
  • Al rollback, eliminar de Redis els jobs pendents no enqueued.
  • Al rollback_savepoint, eliminar de Redis els jobs descartats pel savepoint.
  • Mantenir compatibilitat amb Python 2.7 i RQ 1.3.0.
  • Afegir tests de commit/rollback/savepoint.

Fase 2 — Observabilitat i protecció

  • Logar nombre de jobs pendents per transacció.
  • Afegir warning configurable quan una transacció acumula molts jobs on_commit.
  • Afegir límit opcional configurable per tallar abans de matar el procés per RAM.
  • Mesurar mida aproximada del payload serialitzat per job en mode debug o mètrica opcional.

Fase 3 — Neteja d'orfes Redis

  • Identificar jobs guardats a Redis però no presents a cap queue/registry.
  • Definir TTL o cleanup periòdic segur per jobs pendents no enqueued antics.
  • Evitar esborrar jobs legítims queued/started/finished/failed.

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ó

  • Generar molts jobs on_commit=True dins una transacció no reté instàncies completes de rq.Job en memòria.
  • El consum de RAM per job pendent baixa a dades mínimes (job_id, queue_name, at_front i savepoints).
  • Els jobs s'encuen només després de commit.
  • Els jobs no s'encuen si hi ha rollback.
  • Els jobs descartats per rollback_savepoint no queden enqueued i es netegen de Redis.
  • Es mantenen els tests actuals de on_commit=True.
  • La solució és compatible amb Python 2.7 / OpenERP v5 / RQ 1.3.0.

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions