|
122 | 122 | FORWARD_MODULE = 108 |
123 | 123 | DETACHING = 109 |
124 | 124 | CALL_SERVICE = 110 |
| 125 | +STUB_CALL_SERVICE = 111 |
125 | 126 |
|
126 | 127 | #: Special value used to signal disconnection or the inability to route a |
127 | 128 | #: message, when it appears in the `reply_to` field. Usually causes |
@@ -3432,9 +3433,22 @@ def __init__(self, econtext): |
3432 | 3433 | self.econtext = econtext |
3433 | 3434 | #: Chain ID -> CallError if prior call failed. |
3434 | 3435 | self._error_by_chain_id = {} |
3435 | | - self.recv = Receiver(router=econtext.router, |
3436 | | - handle=CALL_FUNCTION, |
3437 | | - policy=has_parent_authority) |
| 3436 | + self.recv = Receiver( |
| 3437 | + router=econtext.router, |
| 3438 | + handle=CALL_FUNCTION, |
| 3439 | + policy=has_parent_authority, |
| 3440 | + ) |
| 3441 | + #: The :data:`CALL_SERVICE` :class:`Receiver` that will eventually be |
| 3442 | + #: reused by :class:`mitogen.service.Pool`, should it ever be loaded. |
| 3443 | + #: This is necessary for race-free reception of all service requests |
| 3444 | + #: delivered regardless of whether the stub or real service pool are |
| 3445 | + #: loaded. See #547 for related sorrows. |
| 3446 | + Dispatcher._service_recv = Receiver( |
| 3447 | + router=econtext.router, |
| 3448 | + handle=CALL_SERVICE, |
| 3449 | + policy=has_parent_authority, |
| 3450 | + ) |
| 3451 | + self._service_recv.notify = self._on_call_service |
3438 | 3452 | listen(econtext.broker, 'shutdown', self.recv.close) |
3439 | 3453 |
|
3440 | 3454 | @classmethod |
@@ -3475,8 +3489,44 @@ def _dispatch_one(self, msg): |
3475 | 3489 | self._error_by_chain_id[chain_id] = e |
3476 | 3490 | return chain_id, e |
3477 | 3491 |
|
| 3492 | + def _on_call_service(self, recv): |
| 3493 | + """ |
| 3494 | + Notifier for the :data:`CALL_SERVICE` receiver. This is called on the |
| 3495 | + :class:`Broker` thread for any service messages arriving at this |
| 3496 | + context, for as long as no real service pool implementation is loaded. |
| 3497 | +
|
| 3498 | + In order to safely bootstrap the service pool implementation a sentinel |
| 3499 | + message is enqueued on the :data:`CALL_FUNCTION` receiver in order to |
| 3500 | + wake the main thread, where the importer can run without any |
| 3501 | + possibility of suffering deadlock due to concurrent uses of the |
| 3502 | + importer. |
| 3503 | +
|
| 3504 | + Should the main thread be blocked indefinitely, preventing the import |
| 3505 | + from ever running, if it is blocked waiting on a service call, then it |
| 3506 | + means :mod:`mitogen.service` has already been imported and |
| 3507 | + :func:`mitogen.service.get_or_create_pool` has already run, meaning the |
| 3508 | + service pool is already active and the duplicate initialization was not |
| 3509 | + needed anyway. |
| 3510 | +
|
| 3511 | + #547: This trickery is needed to avoid the alternate option of spinning |
| 3512 | + a temporary thread to import the service pool, which could deadlock if |
| 3513 | + a custom import hook executing on the main thread (under the importer |
| 3514 | + lock) would block waiting for some data that was in turn received by a |
| 3515 | + service. Main thread import lock can't be released until service is |
| 3516 | + running, service cannot satisfy request until import lock is released. |
| 3517 | + """ |
| 3518 | + self.recv._on_receive(Message(handle=STUB_CALL_SERVICE)) |
| 3519 | + |
| 3520 | + def _init_service_pool(self): |
| 3521 | + import mitogen.service |
| 3522 | + mitogen.service.get_or_create_pool(router=self.econtext.router) |
| 3523 | + |
3478 | 3524 | def _dispatch_calls(self): |
3479 | 3525 | for msg in self.recv: |
| 3526 | + if msg.handle == STUB_CALL_SERVICE: |
| 3527 | + self._init_service_pool() |
| 3528 | + continue |
| 3529 | + |
3480 | 3530 | chain_id, ret = self._dispatch_one(msg) |
3481 | 3531 | _v and LOG.debug('%r: %r -> %r', self, msg, ret) |
3482 | 3532 | if msg.reply_to: |
@@ -3535,34 +3585,6 @@ def _on_broker_exit(self): |
3535 | 3585 | if not self.config['profiling']: |
3536 | 3586 | os.kill(os.getpid(), signal.SIGTERM) |
3537 | 3587 |
|
3538 | | - #: On Python >3.4, the global importer lock has split into per-module |
3539 | | - #: locks, so there is no guarantee the import statement in |
3540 | | - #: service_stub_main will complete before a second thread attempting the |
3541 | | - #: same import will see a partially initialized module. Therefore serialize |
3542 | | - #: the stub explicitly. |
3543 | | - service_stub_lock = threading.Lock() |
3544 | | - |
3545 | | - def _service_stub_main(self, msg): |
3546 | | - self.service_stub_lock.acquire() |
3547 | | - try: |
3548 | | - import mitogen.service |
3549 | | - pool = mitogen.service.get_or_create_pool(router=self.router) |
3550 | | - pool._receiver._on_receive(msg) |
3551 | | - finally: |
3552 | | - self.service_stub_lock.release() |
3553 | | - |
3554 | | - def _on_call_service_msg(self, msg): |
3555 | | - """ |
3556 | | - Stub service handler. Start a thread to import the mitogen.service |
3557 | | - implementation from, and deliver the message to the newly constructed |
3558 | | - pool. This must be done as CALL_SERVICE for e.g. PushFileService may |
3559 | | - race with a CALL_FUNCTION blocking the main thread waiting for a result |
3560 | | - from that service. |
3561 | | - """ |
3562 | | - if not msg.is_dead: |
3563 | | - th = threading.Thread(target=self._service_stub_main, args=(msg,)) |
3564 | | - th.start() |
3565 | | - |
3566 | 3588 | def _on_shutdown_msg(self, msg): |
3567 | 3589 | if not msg.is_dead: |
3568 | 3590 | _v and LOG.debug('shutdown request from context %d', msg.src_id) |
@@ -3606,11 +3628,6 @@ def _setup_master(self): |
3606 | 3628 | handle=SHUTDOWN, |
3607 | 3629 | policy=has_parent_authority, |
3608 | 3630 | ) |
3609 | | - self.router.add_handler( |
3610 | | - fn=self._on_call_service_msg, |
3611 | | - handle=CALL_SERVICE, |
3612 | | - policy=has_parent_authority, |
3613 | | - ) |
3614 | 3631 | self.master = Context(self.router, 0, 'master') |
3615 | 3632 | parent_id = self.config['parent_ids'][0] |
3616 | 3633 | if parent_id == 0: |
|
0 commit comments