Restricting task execution with throttle

We’ve been habitually misusing serial: 1 to ensure play tasks only execute on one host at a time. That works fine for its intended purpose (rolling restarts in a cluster for example), but our mental model crashes and burns when we get to run_once: true tasks.

I’m looking at throttle: 1 which is kind of like a serial: 1 scoped to single tasks but without splitting your batch hosts. At least it seems to do the Right Thing™ (i.e. least surprises) in relation to run_once: true tasks.

My concern is with the wording in the docs: Restricting execution with throttle. It’s very specifically worded to indicate it works at the block or task level, excluding any mention of play level use — in contrast to, say, serial.

But it seems to work at the play level, and ansible-lint doesn’t bat an eye about it there.

Q1: Are the docs behind, or am I skating on thin ice by using throttle on a play?

Q2: What’s the intended / expected behavior if throttle: 1 is used on an include_* task? I could imagine a useful effect if it effectively restricts execution of the entire include to one host at a time. But throttle restricts the number of “workers”, which I suspect have no host affinity… This is one of those corners where the docs shine no light; and it seems unwise to base new work on results of empirically derived evidence.

Or am I thinking about this the wrong way? What I want to achieve is:

  • restrict execution of selected blocks of multiple tasks to one host at a time
  • without splitting play batches (i.e. run_once is truly once across all target hosts).
$ ansible-doc -t keyword throttle
throttle:
  applies_to:
  - Play
  - Role
  - Block
  - Task
  - Handler
  description: Limit the number of concurrent task runs on task, block and play>
    level. This is independent of the forks and serial settings, but cannot be >
    higher than those limits. For example, if forks is set to 10 and the thrott>
    is set to 15, at most 10 hosts will be operated on in parallel.
  priority: 0
  template: explicit
  type: int

Given that the keyword’s documentation says it works for plays and your experience says that it works for plays, it’s pretty safe to say it works for plays. As with everything it’s not impossible this could change in the future, but it would be a weird thing for the devs to decide to break.

I would expect it to have between zero and no effect. Includes do not execute tasks, they include them.

1 Like

That’s a helpful distinction. Thanks.

Taken together with this “note” toward the bottom of the document I referenced earlier…

When used together with serial, tasks marked as run_once will be run on one host in each serial batch. If the task must run only once regardless of serial mode, use when: inventory_hostname == ansible_play_hosts_all[0] construct.

… suggests that the real answer to the question of how to:

  • run some tasks across all host,
  • then run some more tasks on a subset (maybe one) of those hosts,
  • then run even more tasks across the whole set

is to split the playbook into multiple plays, adjusting hosts, throttle, and strategy as appropriate for each play.

For our current problem, that’s going to require restructuring the problematic role so various parts can be called through different task files. That should be okay though, assuming set_facts carry over from play to play within the playbook. As a bonus, it should make what’s going on blatantly obvious rather than side-effects of unusual combinations of obscure techniques. Future me likes the sound of that.