Re: Databases and threading

Jason Madden <[email protected]> Tue, 19 Nov 2019 06:58:42 -0600
Newsgroups gmane.comp.web.zope.zodb
Message-ID <[email protected]>

> On Nov 19, 2019, at 04:44, =C3=89loi Rivard <[email protected]> wrote:
>=20
> Hello, There are a few things I wonder about ZODB and concurrency, and I =
am not sure of the answers the documentation gives.
> The documentation states tha database connections, transactions managers =
and transactions are not thread safe. It also states that we cannot share d=
atabases between processes. But can we share databases an storages between =
threads?=20

A Connection instance, and all the persistent objects accessed through it, =
may be used from exactly one thread (or greenlet!) at a time; the same goes=
 for the transaction/manager associated with the connection.=20

A DB instance is there to vend Connection instances, and it can do so from =
any number of threads concurrently. How the DB, Connection, and storage coo=
perate to make this work are implementation details.

So this example should be fine:

> import ZODB, threading
> db =3D ZODB.DB(None)
> def work():
>     with db.transaction() as conn:
>         do_stuff()
>=20
> threading.Thread(target=3Dwork).start()
> threading.Thread(target=3Dwork).start()


But this would be broken:

    def process_chunk(container, chunk_number):
        chunk =3D container[chunk_number]
        # Do stuff with chunk
   =20
    db =3D get_the_db()
    conn =3D db.open()
    root =3D conn.root()

    threading.Thread(target=3Dprocess_chunk, args=3D(1,)).start()
    threading.Thread(target=3Dprocess_chunk, args=3D(2,)).start()

That's broken because it's sharing the same persistent object (the root) be=
tween threads without any sort of locking to ensure that the underlying Con=
nection (hidden in _p_jar), is only used from one thread at a time.

The cross-thread sharing is pretty blatant here. A more subtle trap is shar=
ed caches of persistent objects.=20

    cache =3D {}
    db =3D get_the_db()
    def process_chunk(chunk_number):
         result =3D []
         conn =3D db.open()
         root =3D conn.root()
         chunk =3D root[chunk_number]
         for part in chunk:
             answer =3D cache.get(part)
             if answer is None
                 answer =3D part.find_or_compute_answer()
                 if answer._p_jar is None:=20
                     conn.add(answer) # could be new, could be previously s=
aved
                 cache[part] =3D answer
             result.append(answer)
         return result

    threading.Thread(target=3Dprocess_chunk, args=3D(1,)).start()
    threading.Thread(target=3Dprocess_chunk, args=3D(2,)).start()

If any of the chunks use the same part, we could be accessing objects from =
different connections in different threads simultaneously.

Sometimes a shared cache like that can be found at a module level. That's d=
estined to eventually result in a ConnectionStateError, but because of ZODB=
's caching, it may not be until the application is under significant load i=
n a production setting. (Don't do that.)

Speaking of ZODB's caching, very often many of the cross-threading issues c=
an be hidden by it: if an object is already in memory, there will be no nee=
d to use the _p_jar at all and ZODB is taken out of the equation entirely. =
Then you're just left with the normal concerns about using objects from mul=
tiple threads. (All of the examples above are probably that way.) That's al=
l fine and good until an object gets ghosted (usually at the most inopportu=
ne time) or your access pattern changes just enough that an object that was=
 usually in memory before, or was never accessed at all before, suddenly ha=
s to be loaded. I suggest having your tests frequently make use of DB.cache=
Minimize() (e.g., between requests/transactions) to help smoke out issues l=
ike that; you can even make adjustments to the DB's connection pool so that=
 sequential requests don't get the same Connection object.

Is all of that to say there's no way to parallelize work using a single Con=
nection and transaction? Not at all. It can be done, but (like anything inv=
olving concurrency) it takes careful design. It's certainly easier to stick=
 to the one-thread-per-connection/one-connection-per-thread/nothing-shared-=
except-the-DB model that's encouraged by default.

HTH,
Jason


--=20
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 e=
mail to [email protected].
To view this discussion on the web visit https://groups.google.com/d/msgid/=
zodb/236EF6D7-AE04-4014-A937-41F53C37CB6D%40nextthought.com.