- Je weigert cyclische of onvolledige plannen en kiest een deterministische topologische volgorde.
- Je onderscheidt wachten, uitvoeren, slagen en falen, en begrenst retries.
Een assistent heeft gegevens gelezen en een berekening voltooid, maar de laatste stap faalt. Alles opnieuw uitvoeren verspilt werk en kan bij schrijfacties een tweede effect veroorzaken. Een hervatbaar plan bewaart daarom afhankelijkheden én uitvoeringstoestand.
Zie een plan als een recept met voorbereidingen: het antwoord kan pas nadat de berekening en het opzoekwerk klaar zijn. De pijlen zeggen wat eerst moet gebeuren. Ze bewijzen niet dat de taken nuttig of toegestaan zijn.
1. Maak de afhankelijkheden zichtbaar
Een gerichte acyclische graaf (DAG) heeft pijlen zonder kringloop. Ons plan bestaat uit lezen a, rekenen b, opzoeken c en antwoorden d. De getallen zijn aangenomen duren in werkeenheden:
+--> b (4) --+
a (2) -+ +--> d (1)
+--> c (3) --+
Taken b en c wachten op a; d wacht op b én c. Alleen a heeft geen voorgangers. Bouw ditzelfde plan in code; de invoerlijst staat expres niet in uitvoervolgorde.
def task(key, needs, duration):
return {
'id': key, 'needs': needs,
'duration': duration,
'retry_safe': True
}
plan = [
task('d', ['b', 'c'], 1),
task('c', ['a'], 3),
task('b', ['a'], 4),
task('a', [], 2)
]
print(topological_plan(plan))
# ['a', 'b', 'c', 'd']
Schrijf topological_plan met de begrensde taakvorm uit de opdracht. Kies telkens één alfabetisch kleinste beschikbare ID en voeg die aan done toe. De volgende ronde beoordeelt opnieuw welke taken beschikbaar zijn.
| Ronde | Beschikbaar | Gekozen |
|---|---|---|
| 1 | a | a |
| 2 | b, c | b |
| 3 | c | c |
| 4 | d | d |
Alle beschikbare taken als één alfabetisch blok toevoegen lijkt logisch, maar voldoet niet aan ons contract. Na a kan een nieuwe kleinere kandidaat klaarstaan vóór de eerder beschikbare z:
small = [
task('z', [], 1),
task('b', ['a'], 1),
task('a', [], 1)
]
print(topological_plan(small))
# ['a', 'b', 'z']
Andere topologische volgordes kunnen wiskundig geldig zijn. Onze keuze maakt precies deze oefening reproduceerbaar, ook bij een anders geordende invoerlijst.
012. Bewaar status én pogingenteller
Een gekozen volgorde is nog geen uitvoerlog. Bewaar pending, running, done of failed plus het aantal begonnen pogingen. Pending betekent nul; alle andere statussen betekenen minstens één poging en vereisen voltooide voorgangers.
Stel dat a en c klaar zijn, b eenmaal faalde en d nog niet gestart is. b mag opnieuw als retry_safe waar is en het pogingenbudget ruimte heeft. d blijft wachten, ook wanneer zijn eigen teller nog nul is.
states = {
'a': 'done', 'b': 'failed',
'c': 'done', 'd': 'pending'}
attempts = {
'a': 1, 'b': 1, 'c': 1, 'd': 0}
r = schedulable_plan(
plan, states, attempts)
print(r['ready'], r['blocked'])
attempts['b'] = 2
r = schedulable_plan(
plan, states, attempts, 3)
print(r['ready'])
r = schedulable_plan(
plan, states, attempts, 2)
print(r['exhausted'], r['blocked'])
# ['b'] ['d']
# ['b']
# ['b'] ['d']
Schrijf schedulable_plan met de vijf alfabetische categorieën uit de opdracht. Running en done behouden hun categorie; een mislukte taak zonder veilige herhaling of resterende pogingen wordt exhausted. De uitvoerder verhoogt zelf de teller bij starten en bewaart het latere resultaat.
Een exception kan optreden ná een effect. Failed bewijst dus niet dat niets veranderde; retry_safe moet uit vertrouwde kennis van de handeling komen. Geslaagde taken terugzetten naar pending vernietigt juist de informatie die hervatten mogelijk maakt.
De visualisatie gebruikt dezelfde afhankelijkheden en duren, met a als read, b als calculate, c als lookup en d als answer. Kies verschillende momentopnamen en volg welke taken nog mogen starten.
read voedt twee takken: calculate en lookup. answer wacht op beide. Bekijk welke taken in deze momentopname mogen starten.
Duren: read 2, calculate 4, lookup 3, answer 1. Kosten gelden voor één volledige foutloze uitvoering; retries komen erbij. De ideale parallelle tijd veronderstelt onbeperkte workers, geen overhead en onafhankelijke effecten.
3. Reken totale werklast en kritisch pad apart uit
Met één worker kost het plan 2+4+3+1=10 eenheden. Bij onbeperkt parallel werk eindigt a op 2; b eindigt op 2+4=6 en c op 2+3=5. d wacht tot beide voorgangers klaar zijn en eindigt op max(6,5)+1=7.
Noem de duur van taak v d_v en haar eindtijd F_v. P(v) bevat de voorgangers; het maximum van een lege verzameling eindtijden is hier nul:
De som van alle duren is T_s; de ideale parallelle tijd T_p is de grootste eindtijd. Het kritieke pad a→b→d bepaalt hier die ondergrens:
cost = plan_cost(plan)
for key in ['a', 'b', 'c', 'd']:
print(key, cost['finish'][key])
print(cost['serial'],
cost['parallel'])
# a 2
# b 6
# c 5
# d 7
# 10 7
Schrijf plan_cost in topologische volgorde. Dit rooster neemt vaste duren, onbeperkte workers en geen communicatie of retries aan. Het is geen gemeten versnelling; conflicterende effecten en beperkte middelen kunnen de echte tijd verhogen.
Een kringloop heeft geen geldige volledige volgorde. Ook ontbrekende of dubbele IDs en ongeldige taakvelden horen vóór uitvoering te falen:
cycle = [
task('x', ['y'], 1),
task('y', ['x'], 1)
]
try:
topological_plan(cycle)
except ValueError:
print('cyclus')
# cyclus
03Wat neem je mee?
- Afhankelijkheden bepalen beschikbaarheid; ons contract kiest daarbinnen telkens één kleinste ID.
- Toestand en pogingentellers voorkomen blind herhalen van eerder werk.
- Parallelle tijd en totale werklast zijn verschillende grootheden.
Hierna bewaren we context met bronversies en vervaltijden, zodat hervatten ook op actuele informatie berust.