python - Increment a counter and trigger an action when a threshold is exceeded -
i have model this
class thingy(models.model): # ... failures_count = models.integerfield()
i have concurrent processes (celery tasks) need this:
- do kind of processing
- if processing fails increment
failures_counter
of respectivethingy
- if
failures_counter
exceeds thresholdthingy
, issue warning, 1 warning.
i have ideas how without race condition, example using explicit locks (via select_for_update
):
@transaction.commit_on_success def report_failure(thingy_id): current, = (thingy.objects .select_for_update() .filter(id=thingy_id) .values_list('failures_count'))[0] if current == threshold: issue_warning_for(thingy_id) thingy.objects.filter(id=thingy_id).update( failures_count=f('failures_count') + 1 )
or using redis (it's there) synchronization:
@transaction.commit_on_success def report_failure(thingy_id): thingy.objects.filter(id=thingy_id).update( failures_count=f('failures_count') + 1 ) value = thingy.objects.get(id=thingy_id).only('failures_count').failures_count if value >= threshold: if redis.incr('issued_warning_%s' % thingy_id) == 1: issue_warning_for(thingy_id)
both solutions use locks. i'm using postgresql, there way achieve without locking?
i'm editing question include answer (thanks sean vieira, see answer below). question asked way avoid locking , answer optimal in leverages multi-version concurrency control (mvcc) implemented postgresql.
this specific question explicitly allowed using postgresql features, , though many rdbmss implement update ... returning
, not standard sql , not supported django's orm out of box, requires using raw sql via raw()
. same sql statement work in other rdbmss every engine requires own discussion regarding synchronization, transactions isolation , concurrency models (e.g. mysql myisam still use locks).
def report_failure(thingy_id): transaction.commit_on_success(): failure_count = thingy.objects.raw(""" update thingy set failure_count = failure_count + 1 id = %s returning failure_count; """, [thingy_id])[0].failure_count if failure_count == threshold: issue_warning_for(thingy_id)
as far can tell django's orm doesn't support out of box - however, doesn't mean can't done, need dip down sql level (exposed, in django's orm via manager
's raw
method) make work.
if using postgressql >= 8.2 can use returning
final value failure_count
without additional locking (the db still lock, long enough set value, no additional time lost communicating you):
# assumptions: ids valid , ids unique # more defenses necessary if either of these assumptions # not true. failure_count = thingy.objects.raw(""" update thingy set failure_count = failure_count + 1 id = %s returning failure_count; """, [thingy_id])[0].failure_count if failure_count == threshold: issue_warning_for(thingy_id)
Comments
Post a Comment