Issue: Make signals respect transactions?
Author: josefdlange
State: closed
Created: 2016-01-21 00:52:41

I have a feeling the answer will be "no, definitely not" but I thought I'd pick your brain on this. 

I've got a variety of functions that do a variety of things, some calling others, etc, and some of these functions in effect send push notifications and the like to clients based on the data they've created/changed/interpreted. The clients then receive those notifications and query my web application for data in some cases. Sometimes, the push messages get to the clients so quickly that in fact the work which is happening inside a transaction isn't yet complete and the transaction not yet committed to the DB, which causes the clients to be very sad and think they've been sent a message for nonexistent data.

I'm using `falcon` which could in theory allow me to pass its request context structure down into all my functions, but that doesn't work with the decoupled nature of signals, and a majority of these messages are sent via model signals, since typically I want to update my end-users based on some change in data. I'm trying to think if there is a pragmatic way to implement a `post_commit` signal to respect a transaction's committing. The key would be somehow having the model identify that it is within a transaction, and then enqueue the `@post_commit`-decorated function in the transaction object at save time to be called when `commit()` is called.

Seems messy. Seems silly. But I'm curious if it's _possible_. My solution right now is to defer sending the message into `celery` with a delay, but that's not really a solution, it's more of a shitty hack.

An alternate thought is to be able to get a list of rows/Model instances affected by a transaction we're about to commit, but that seems equally silly...

Anyway, curious if you have any ideas.


Or, just a thought that popped into my head, maybe on [`transaction`](https://github.com/coleifer/peewee/blob/master/peewee.py#L3995) we could add in the ability to set a callable value to be executed on commit? (and rollback I suppose).


Peewee is active record, so the concept of tracking models throughout a transaction is not really included in the design. SQLAlchemy, on the other hand, supports this type of thing. Django, too, might via a post-commit signal handler but I'm not positive.

What I'd suggest to you is to implement a wrapper of `transaction` that accepts a list of callbacks...you might write something like:

``` python
class MyTransaction(transaction):
    def __init__(self, db):
        super(MyTransaction, self).__init__(db)
        self.callbacks = []

    def bind(self, callback):
        self.callbacks.append(callback)

    def commit(self):
        super(MyTransaction, self).commit()
        for callback in self.callbacks:
            callback()
```

Peewee provides the primitives, I guess is what I'm saying, and it should be possible for you to build the type of system you want on top of them. Whether they are a part of the core or not is a judgment call, and perhaps I'll add a post-commit hook but this should hopefully help you get started.


I'd also note that using `transaction` is really not preferred, the preferred way is to use `db.atomic()` (which can be a context manager or decorator). The reason being that `atomic()` will use the appropriate APIs -- transactions and savepoints -- depending on the level of nesting. Whereas if you're just using transaction() only the outer-most layer is run in a transaction and any nested layers are effectively no-ops.


Yes, I do use atomic(), which I see just obfuscates when to use transaction() vs savepoint(). As a curiosity since I don't have the code in front of me, does a savepoint know about its parent transaction?

> On Jan 21, 2016, at 9:04 AM, Charles Leifer notifications@github.com wrote:
> 
> I'd also note that using transaction is really not preferred, the preferred way is to use db.atomic() (which can be a context manager or decorator). The reason being that atomic() will use the appropriate APIs -- transactions and savepoints -- depending on the level of nesting. Whereas if you're just using transaction() only the outer-most layer is run in a transaction and any nested layers are effectively no-ops.
> 
> —
> Reply to this email directly or view it on GitHub.