Repository navigation
Conversation
|
The failing integration jobs here all fail in |
Minion.handle_event handled _minion_mine by calling the synchronous _mine_send, which opened a ReqChannel and blocked in send() on the minion's main event loop. That loop is shared by every master of a multi-master minion, so while one master was unreachable each mine run stopped the minion from processing anything else until the send gave up, 15 seconds to almost 2 minutes per run. _mine_send is now a coroutine using an AsyncReqChannel on the minion's loop, and handle_event awaits it. Events are dispatched as separate tasks, so the await does not hold up other events. Fixes saltstack#70421
49e36eb to
a7ed94d
Compare
|
Please resolve conflicts |
|
Could we also keep Mine sends ordered per master and limit how many can pile up? Otherwise, an older update could finish after a newer one or a delete and bring stale data back. Also, TCP’s send timeout only starts after the connection succeeds, so sends can hang indefinitely when a master is down. |
With _mine_send awaited from its own handle_event task, sends for one minion ID could overlap and finish in any order, so a slow update could reach the master after a newer update or a mine.delete and bring the old data back. The blocking send this replaced never overlapped. Each send now holds a lock per master and minion ID from opening the channel until the send returns, so sends for one ID go out to a master one at a time, in the order the events arrived. A deltaproxy's sub-proxies have their own IDs and don't wait on each other. Once three sends for an ID are pending for a master, whether it is down or just slow, later ones are dropped with a warning instead of piling up. A send to a master that is down can take a long time, and with the TCP transport it waits until the master is back, because the send timeout only starts once the connection is up (saltstack#66966). On failover eval_master points self.opts at the new master in place, so each send takes a copy of the opts when its event arrives and queues for that master. Sends still waiting on the old master don't hold up the new one and don't reach it after newer data.
|
@charzl Both hold. The new commit handles the ordering and the pile-up. Each mine send now holds a lock keyed on the master URI and minion ID from opening the channel until it's closed, retries included, so sends for one ID go to a master one at a time in the order the events arrived. At most three are pending per ID and master (one in flight, two waiting). Past that the new send is dropped with a warning ( The key uses the ID from the load, so deltaproxy sub-proxies don't wait on each other. On failover On TCP: Lines 3100 to 3117 in 621f88e That came in with #64705 (9683260), so 3007.0 onwards, and it's #66966. Job returns and AsyncAuth.sign_in() hang the same way.
I didn't wrap the send in I'd rather fix the timeout in @twangboy I merged 3008.x into the branch; the conflict was only the imports in |
What does this PR do?
Makes
Minion._mine_send()a coroutine that sends on anAsyncReqChannelbound to the minion's loop, and awaits it fromhandle_event. Before, it opened a synchronousReqChanneland blocked insend()on the minion's main event loop, which a multi-master minion shares between all its masters. While a master was unreachable that took 15 seconds to almost 2 minutes per mine run, and the minion didn't answer the other masters in the meantime.Events reach
handle_eventas separate tasks (on_recv_handlerinsalt/transport/tcp.py), so awaiting the send there doesn't hold up other events.This also takes away the trigger for #70353 on this path: the
SyncWrapperbehind theReqChannelwas closed while the minion's loop was running, which left anAsyncAuthtask and its socket behind. #70415 fixes that inSyncWrapper. The two PRs don't depend on each other.With the loop no longer blocked, a mine run can start while the previous one is still waiting on an unreachable master. Sends for one minion ID go to a master one at a time, in the order the events arrived, so a newer update or a
mine.deleteisn't sent while an older update is still in flight. At most three are pending per minion ID and master; later ones are dropped with a warning, the same as a send that gives up on an unreachable master. Each send queues for the master that was current when its event arrived, so after a failover the sends stuck on the old master don't hold up the new one. A deltaproxy's sub-proxies have their own IDs and don't wait on each other.With the TCP transport a send to a master that is down waits until the master is back, because the send timeout only starts once the connection is up (#66966). This PR doesn't change the transport; the cap limits how many sends that ties up.
What issues does this PR fix or reference?
Fixes #70421
Refs #70353, #70415, #66966
Previous Behavior
While a master was unreachable, each mine run (
mine_intervalormine.update) blocked the minion's event loop until the send gave up, so the minion didn't process jobs from the masters that were up.New Behavior
The mine send runs on the event loop without blocking it. Sends for one minion ID still go to the master one at a time and in order, and once three are pending for an ID and master, new ones are dropped with a warning.
Testing
tests/pytests/unit/test_minion.py::test_minion_mine_event_does_not_block_io_loopfails on 3008.x (assert 0 >= 10: no heartbeat ticks while the send runs) and passes with this change.test_mine_send_triesis updated for the coroutine.test_mine_sends_for_one_minion_id_are_ordered,test_mine_send_queued_after_earlier_one_finished_keeps_order,test_mine_sends_after_failover_do_not_wait_on_old_master,test_mine_sends_pending_for_one_minion_id_are_capped,test_mine_sends_for_different_minion_ids_do_not_waitandtest_mine_send_state_is_cleared. All but the different-IDs one fail without the ordering commit.tests/pytests/unit/test_minion.py,test_minion_resources.py,test_proxy_minion.pyandmodules/test_mine.pywith--run-slow: 216 passed on Python 3.11, 3.12 and 3.14.End to end, re-run on 2026-10-30 with this branch's
salt/minion.pychange, against the 3008.3 onedir packages (Python 3.14.7, Ubuntu 24.04 containers), with the setup from #70421: two masters,master_tries: -1,ping_interval: 5, atest.pingmine function, and minion -> master1 traffic on 4505/4506 dropped with iptables. 3008.x'ssalt/minion.pyhas moved on since 3008.3, so the PR's diff to that file is applied to the 3008.3 copy (it applies cleanly) and mounted over the onedir one. A loop runssalt '*' test.pingon master2 every 11 seconds or so (every 41 on plain 3008.3 over TCP, where each one ran into its 20 second timeout).mine_interval, outagetest.pingvia master2 during the outagezmq_ctx_term()For comparison, in the 2026-10-31 runs for #70415 (zeromq,
mine_interval: 5, same sampler), plain 3008.3 wedged 12/12 about 22 minutes into the outage, and with #70415 alone 14 of 215 samples got no answer from any minion, all during mine runs.On zeromq each mine send to master1 gave up after 15 to 30 seconds with
Unable to send mine data to master., so the cap never came into play, and master1 had current mine data again from the first mine run after it was back. On TCP the sends to master1 never time out (#66966): the cap kicked in about 3 minutes into the outage, each later mine run loggedNot sending mine data for <id> to master <master>, 3 earlier mine sends are still pending., and apart from the two minutes after the disconnect (about 14 minutes in) each minion had a single connection attempt to master1 open. Plain 3008.3 on TCP stopped answering at the first mine run of the outage and didn't answer again until master1 was back.Merge requirements satisfied?
Commits signed with GPG?
No