@@ -183,3 +183,84 @@ Since this gets executed before nginx is installed by ``apt.packages`` operation
183183 reloaded = True ,
184184 _if = remove_default_site.did_change,
185185 )
186+
187+ .. _loops-cycle-errors :
188+
189+ Loops & Cycle Errors
190+ ~~~~~~~~~~~~~~~~~~~~
191+
192+ In CLI mode ``pyinfra `` builds a single DAG to determine the order in which
193+ operations are executed. This usually produces the order a deploy author
194+ expects, but loops can make the same operation line appear multiple times with
195+ different per-host paths through the deploy. When those paths disagree about
196+ which operation must run first, the ordering graph contains a cycle and
197+ ``pyinfra `` raises a cycle error.
198+
199+ Use ``host.loop `` when a loop contains operations. This gives ``pyinfra `` the
200+ loop position as an ordering hint:
201+
202+ .. code :: python
203+
204+ from pyinfra import host
205+ from pyinfra.operations import server
206+
207+ for i in host.loop(range (0 , 2 )):
208+ server.shell(
209+ name = f " Do a thing { i} " ,
210+ commands = " ls" ,
211+ )
212+
213+ For example, this deploy can generate a cycle because the first operation only
214+ appears on ``@local `` during the first loop iteration:
215+
216+ .. code :: python
217+
218+ from pyinfra import host
219+ from pyinfra.operations import server
220+
221+ for i in range (0 , 2 ):
222+ if i > 0 or (i == 0 and host.name == " @local" ):
223+ server.shell(
224+ name = " A" ,
225+ commands = " ls" ,
226+ )
227+
228+ server.shell(
229+ name = " B" ,
230+ commands = " ls" ,
231+ )
232+
233+ The resulting per-host order is inconsistent:
234+
235+ .. code :: shell
236+
237+ # @local: A -> B -> A-1 -> B-1
238+ # Other: B -> A -> B-1
239+
240+ Combining those host orders means ``A `` must run before ``B `` and ``B `` must
241+ run before ``A ``. Switching the loop to ``host.loop `` includes the loop position
242+ in the operation order and removes the ambiguity:
243+
244+ .. code :: python
245+
246+ from pyinfra import host
247+ from pyinfra.operations import server
248+
249+ for i in host.loop(range (0 , 2 )):
250+ if i > 0 or (i == 0 and host.name == " @local" ):
251+ server.shell(
252+ name = " A" ,
253+ commands = " ls" ,
254+ )
255+
256+ server.shell(
257+ name = " B" ,
258+ commands = " ls" ,
259+ )
260+
261+ The graph can then be resolved consistently:
262+
263+ .. code :: shell
264+
265+ # @local: 0A -> 0B -> 1A -> 1B
266+ # Other: 0B -> 1A -> 1B
0 commit comments