1

I have modeled the following test program on the relevant Python 3.9.2 documentation regarding the synchronization of data among remote processes. As nearly as I can tell, though, it doesn't actually work, so I assume there's something I don't know. The documentation is not explicit about the deployment of SyncManager objects among remote processes, but they are, after all, instances of a subclass of BaseManager, so one must assume that the same technique should work.

After the code below comes shell outputs showing three concurrent invocations that presumably demonstrate the problem I'm having. Although connections are being made to the server, the dict isn't being synchronized. The question is: why?

#!/usr/bin/env python3
# <zteeq.py>
import multiprocessing as mp
import multiprocessing.shared_memory as sm
import multiprocessing.managers as mgrs
import os, sys

#######################################################
class CatalogManager( mgrs.SyncManager): pass
CatalogManager.register( 'get_catalog', dict, mgrs.DictProxy)

#######################################################
class ShareCatalog():
    #######################################################
    def __init__( self,
        catalogManagerAddress,
        catalogManagerAuthkey,
        **kwargs
    ):
        self.catalogManagerAddress = catalogManagerAddress
        self.catalogManagerAuthkey = catalogManagerAuthkey

    #######################################################
    def start( self):
        self.catalogManager = CatalogManager(
            self.catalogManagerAddress,
            self.catalogManagerAuthkey,
        )
        try:
            self.catalogManager.connect()
            print( 'connected self.catalogManager')
        except ConnectionRefusedError:
            catalogManagerServer = self.catalogManager.get_server()
            print( 'starting self.catalogManager')
            catalogManagerServer.serve_forever()
        self.catalog = self.catalogManager.get_catalog()

###
        print( 'pid %d: first stop: %r' % ( os.getpid(), str( self.catalog)))
        input()
###
        if 'streams' not in self.catalog:
            print( 'adding streams')
            self.catalog[ 'streams'] = {}
###
        print( 'pid %d: second stop: %r' % ( os.getpid(), str( self.catalog)))
        input()
###
#######################################################
if __name__ == '__main__':
    mp.set_start_method( 'spawn')
    shareCatalog = ShareCatalog(
        ( '127.0.1.1', 43210),
        b'abc',
    )
    shareCatalog.start()
#</zteeq.py>

In the first shell, the SyncManager server starts:

# ./zteeq.py
starting self.catalogManager

Leaving that running, I start the program again in a second shell:

# ./zteeq.py
connected self.catalogManager
pid 2486196: first stop: '{}'

adding streams
pid 2486196: second stop: "{'streams': {}}"

So far, so good. I leave that running and invoke a third time. But the third invocation knows nothing about what the second invocation did; there's no "streams" key in the shared dictionary:

# ./zteeq.py
connected self.catalogManager
pid 2492338: first stop: '{}'

What am I missing?

(Python 3.9.2) (Linux 5.10.0-4-amd64 #1 SMP Debian 5.10.19-1 (2021-03-02) x86_64 GNU/Linux)

Remark: In general, the documentation seems to assume that all SyncManager objects will be created by a shortcut called "multiprocessing.Manager()" which does not provide for the specification of remote socket communications. I assume such objects are intended to be forkishly inherited by all the processes that will use it, as is shown in all the examples I have found so far. But that's not what I'm trying to do.

1 Answers1

1

Some of the inferences I made turned out to be incorrect. I wish the solution below didn't look so clunky and redundant, but I think it's best to post it this way because the redundancy itself is informative. (The necessity for the redundancy surprised me, and I'm still thinking about it.) The documentation's suggestion to leave contained objects unmanaged and then simply to tweak the managed container in order to tell the manager to update the clients just didn't work. I don't know why; probably another incorrect inference or misunderstanding. Anyway, the following actually worked.

#!/usr/bin/env python3
import multiprocessing as mp
import multiprocessing.shared_memory as sm
import multiprocessing.managers as mgrs
import os, sys

#######################################################

#######################################################
class ShareCatalog():
    #######################################################
    def __init__( self,
        catalogManagerAddress,
        catalogManagerAuthkey,
        **kwargs
    ):
        self.catalogManagerAddress = catalogManagerAddress
        self.catalogManagerAuthkey = catalogManagerAuthkey

    #######################################################
    def start( self, server):
        class CatalogManager( mgrs.SyncManager): pass
        if server:
            catalogDict = {
                'streams': {},
            }
            CatalogManager.register( 'get_catalog', lambda:catalogDict, mgrs.DictProxy)
            CatalogManager.register( 'get_streams', lambda:catalogDict[ 'streams'], mgrs.DictProxy)
            self.catalogManager = CatalogManager(
                self.catalogManagerAddress,
                self.catalogManagerAuthkey,
            )
            catalogManagerServer = self.catalogManager.get_server()
            print( 'starting self.catalogManager')
            catalogManagerServer.serve_forever()
        else:  ## not server
            CatalogManager.register( 'get_catalog')
            CatalogManager.register( 'get_streams')
            self.catalogManager = CatalogManager(
                self.catalogManagerAddress,
                self.catalogManagerAuthkey,
            )
            self.catalogManager.connect()
            print( 'connected self.catalogManager')
            self.catalog = self.catalogManager.get_catalog()
            self.streams = self.catalogManager.get_streams()
###
        ctr = -1
        while True:
            print( 'pid %d: first stop: %r' % ( os.getpid(), str( self.catalog)))
            input()
            ctr += 1
            self.catalog.setdefault(
                ( os.getpid(), ctr,),
                None,
            )
            self.streams[ ctr] = None
            print( 'pid %d: second stop: %r' % ( os.getpid(), str( self.catalog)))
            input()
###
#######################################################
if __name__ == '__main__':
    mp.set_start_method( 'spawn')
    shareCatalog = ShareCatalog(
        ( '127.0.1.1', 43210),
        b'abc',
    )
    shareCatalog.start( eval( sys.argv[ 1]))

As before, first invocation starts the server:

# ./zteeq.py True  ## True means "be the server"
starting self.catalogManager

Second invocation:

# ./zteeq.py False
connected self.catalogManager
pid 2767634: first stop: "{'streams': {}}"

pid 2767634: second stop: "{'streams': {0: None}, (2767634, 0): None}"

pid 2767634: first stop: "{'streams': {0: None}, (2767634, 0): None}"

pid 2767634: second stop: "{'streams': {0: None, 1: None}, (2767634, 0): None, (2767634, 1): None}"

Leaving the second invocation running, here's the third invocation:

# ./zteeq.py False
connected self.catalogManager
pid 2767704: first stop: "{'streams': {0: None, 1: None}, (2767634, 0): None, (2767634, 1): None}"

pid 2767704: second stop: "{'streams': {0: None, 1: None}, (2767634, 0): None, (2767634, 1): None, (2767704, 0): None}"
pid 2767704: first stop: "{'streams': {0: None, 1: None}, (2767634, 0): None, (2767634, 1): None, (2767704, 0): None}"

Coming back to the second invocation, we see the third invocation's changes after pressing Enter:

# ./zteeq.py False
connected self.catalogManager
pid 2767634: first stop: "{'streams': {}}"

pid 2767634: second stop: "{'streams': {0: None}, (2767634, 0): None}"

pid 2767634: first stop: "{'streams': {0: None}, (2767634, 0): None}"

pid 2767634: second stop: "{'streams': {0: None, 1: None}, (2767634, 0): None, (2767634, 1): None}"

pid 2767634: first stop: "{'streams': {0: None, 1: None}, (2767634, 0): None, (2767634, 1): None, (2767704, 0): None}"

Good enough to use, anyway!