Re: How cache invalidations work (and hack to listen in) (was Re: Applause for ZODB! And a question: How do you get pending change info from a transaction object?)

Jim Fulton <[email protected]> Sun, 6 May 2018 10:43:54 -0600
Newsgroups gmane.comp.web.zope.zodb
Message-ID <CAPDm-FgKxohjzh9bMxcSPQ0zOivuB1bGb+9ixArrDpH1+KgY5g@mail.gmail.com>
On Sat, May 5, 2018 at 9:33 PM, Jason Madden <[email protected]>
wrote:

>
> > ???
> > Why doesn't the toy color in Terminal 3 change to green?
> > ???
>
>
> Because a ZODB connection (roughly)


Roughly?


> exposes a consistent view (version) of the database as-of the point in
> time at which it was opened, regardless of what other changes are happening
> and being committed by other connections, even if they change objects this
> connection has already looked at. That's what MVCC is about: Multi-Version
> Concurrency Control. In a traditional RDBMS, it's something like a SQL
> connection operating in REPEATABLE READ isolation mode. That's the only way
> that a connection's views of all the objects it may access is
> self-consistent.
>
> The invalidation is to inform *other* connections that are not yet open
> (sitting in the DB pool) or haven't accessed 'root.toy2' that if they have
> cached data for 'root.toy2', they need to discard it because it's stale.
>
> [snip]
>

Yup. If you've started a transaction (implicitly, in the case above), you
won't see new changes until you start a new one by calling
transaction.begin().

> As you can see, the custom invalidate function does trigger automatically
> when transaction.commit() happens in the other terminal. But why does
> invalidate get called twice in Terminal 3?
>
> Terminal 3 has two active connections (one at the repl, one in script.py),
> neither of which caused the invalidation, so both of them need to know to
> invalidate data in their cache. Hence two messages.
>
> (Invalidations are sent at the level of the storage server, 1-1 with a
> ZODB.DB. A typical application process would have a single DB object and
> use it to open and pool multiple ZODB connections, so in that case you
> would see only one invalidation message. But ZEO.connection() is a
> convenience API that opens a unique storage server and ZODB.DB object and
> returns a single connection, so you have two storage servers opened.)
>
> > Also, on both terminals, it hangs right after "print(connection)" and
> never finishes executing "toy = connection.get(oid)".
> >
> > It's like the connection cannot get the oid and it stalls.
>
> You're deep into implementation details here (and I may get some details
> wrong too, but I think I have the gist correct; Jim can correct me if I
> mess up salient details), but basically I think you're deadlocked.


Ouch. You're right.  So much for my hack advice. :)  I've typically used
this to notify clients, which, separately call back in to get updated data.

I'll have to take this into account when I implement a proper API, which I
fear I won't get to as soon as I'd thought.

MVCCAdapter.invalidate is called in response to an (asynchronous) message
> from the server...that monkey-patched method turns around and tries to use
> a connection to get an object from the server (because it's not in cache)
> which requires sending a message to the server (asynchronously) and
> (synchronously) waiting for an (asynchronous) response, but we're already
> handling one asynchronous message. I believe threads and locks are involved
> (and possibly asyncio event loops)---presumably something along that chain
> is not re-entrant.
>

Probably.


>
> What does a ^C show?
>
> MVCCAdapter uses 'invalidate' to simply queue the list of pending
> invalidations for a connection. The *next* time the connection is opened
> and begins a transaction, it uses that queue to (in local memory only)
> invalidate the cache. Thus neatly sidestepping any question of crossing
> asynchronous barriers.
>

David,

If you want to continue with the hack, I suggest running the
invalidation-processing logic in a separate thread and notifying the thread
via a Queue.

When I've used this, all I needed to do was send a signal to the clients
and let them poll for new data asynchronously, which may be an option for
you. IDK.


> (I know this is toy code, but I'll note it's not generally safe to open a
> connection at module-scope and expect it to work long term, especially
> without committing the transaction it's joined to.


Absolutely. David, recall I suggested something along the lines of:

def notified(oid):
      with mydb.transaction() as conn:
          ob = conn.get(oid)
          ... do whatever with ob

I didn't explain all of those pieces.

First:

mydb = ZEO.DB(8090)

You'll want to create a database object so you can open multiple
connections.  As Jason pointed out, the connection method is just a
convenience function that's a shorthand for:

mydb = ZEO.DB(8090)
connection = mydb.open()

The transaction method:

   - opens a connection,
   - starts a transaction,  and after the code suite:
   - commits the transaction if there isn't an error and aborts it
   otherwise, and
   - closes the connection.

By starting a new transaction, you see up-to-date data.

Jim

-- 
Jim Fulton
http://jimfulton.info

-- 
You received this message because you are subscribed to the Google Groups "zodb" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [email protected].
For more options, visit https://groups.google.com/d/optout.