scheduling algorithm
1 from __future__ import division 2 3 from bisect import insort 4 from collections import deque, OrderedDict 5 from functools import reduce 6 from heapq import heapify, heappop, heappush 7 from math import ceil 8 9 from blist import blist, sorteddict, sortedlist 10 11 def intceil(x): # superfluous in Python 3, ceil is sufficient 12 return int(ceil(x)) 13 14 class Scheduler: 15 def next_internal_event(self): 16 return None 17 18 19 class PS(Scheduler): 20 def __init__(self): 21 self.running = set() 22 23 def enqueue(self, t, jobid, size): 24 self.running.add(jobid) 25 26 def dequeue(self, t, jobid): 27 try: 28 self.running.remove(jobid) 29 except KeyError: 30 raise ValueError("dequeuing missing job") 31 32 def schedule(self, t): 33 running = self.running 34 if running: 35 share = 1 / len(running) 36 return {jobid: share for jobid in running} 37 else: 38 return {} 39 40 class GPS(Scheduler): 41 def __init__(self): 42 self.running = {} 43 44 def enqueue(self, t, jobid, size, priority=1): 45 self.running[jobid] = priority 46 47 def dequeue(self, t, jobid): 48 try: 49 del self.running[jobid] 50 except KeyError: 51 raise ValueError("dequeuing missing job") 52 53 def schedule(self, t): 54 running = self.running 55 if running: 56 share = 1 / sum(running.values()) 57 return {jobid: weight * share for jobid, weight in running.items()} 58 else: 59 return {} 60 61 class FIFO(Scheduler): 62 def __init__(self): 63 self.jobs = deque() 64 65 def enqueue(self, t, jobid, size): 66 self.jobs.append(jobid) 67 68 def dequeue(self, t, jobid): 69 try: 70 self.jobs.remove(jobid) 71 except ValueError: 72 raise ValueError("dequeuing missing job") 73 74 def schedule(self, t): 75 jobs = self.jobs 76 if jobs: 77 return {jobs[0]: 1} 78 else: 79 return {} 80 81 82 class SRPT(Scheduler): 83 def __init__(self): 84 self.jobs = [] 85 self.last_t = 0 86 87 def update(self, t): 88 delta = t - self.last_t 89 if delta == 0: 90 return 91 jobs = self.jobs 92 if jobs: 93 jobs[0][0] -= delta 94 self.last_t = t 95 96 def enqueue(self, t, jobid, job_size): 97 self.update(t) 98 heappush(self.jobs, [job_size, jobid]) 99 100 def dequeue(self, t, jobid): 101 jobs = self.jobs 102 self.update(t) 103 # common case: we dequeue the running job 104 if jobid == jobs[0][1]: 105 heappop(jobs) 106 return 107 # we still care if we dequeue a job not running (O(n)) -- 108 # could be made more efficient, but still O(n) because of the 109 # search, by exploiting heap properties (i.e., local heappop) 110 try: 111 idx = next(i for i, v in jobs if v[1] == jobid) 112 except StopIteration: 113 raise ValueError("dequeuing missing job") 114 jobs[idx], jobs[-1] = jobs[-1], jobs[idx] 115 jobs.pop() 116 heapify(jobs) 117 118 def schedule(self, t): 119 self.update(t) 120 jobs = self.jobs 121 if jobs: 122 return {jobs[0][1]: 1} 123 else: 124 return {} 125 126 127 class SRPT_plus_PS(Scheduler): 128 129 def __init__(self, eps=1e-6): 130 self.jobs = [] 131 self.last_t = 0 132 self.late = set() 133 self.eps = eps 134 135 def update(self, t): 136 delta = t - self.last_t 137 jobs = self.jobs 138 delta /= 1 + len(self.late) # key difference with SRPT #1 139 if jobs: 140 jobs[0][0] -= delta 141 while jobs and jobs[0][0] < self.eps: 142 _, jobid = heappop(jobs) 143 self.late.add(jobid) 144 self.last_t = t 145 146 def next_internal_event(self): 147 jobs = self.jobs 148 if not jobs: 149 return None 150 return jobs[0][0] * (1 + len(self.late)) 151 152 def schedule(self, t): 153 self.update(t) 154 jobs = self.jobs 155 late = self.late 156 scheduled = late.copy() # key difference with SRPT #2 157 if jobs: 158 scheduled.add(jobs[0][1]) 159 if not scheduled: 160 return {} 161 share = 1 / len(scheduled) 162 return {jobid: share for jobid in scheduled} 163 164 def enqueue(self, t, jobid, job_size): 165 self.update(t) 166 heappush(self.jobs, [job_size, jobid]) 167 168 def dequeue(self, t, jobid): 169 self.update(t) 170 late = self.late 171 if jobid in late: 172 late.remove(jobid) 173 return 174 # common case: we dequeue the running job 175 jobs = self.jobs 176 if jobid == jobs[0][1]: 177 heappop(jobs) 178 return 179 # we still care if we dequeue a job not running (O(n)) -- 180 # could be made more efficient, but still O(n) because of the 181 # search, by exploiting heap properties (i.e., local heappop) 182 try: 183 idx = next(i for i, v in jobs if v[1] == jobid) 184 except StopIteration: 185 raise ValueError("dequeuing missing job") 186 jobs[idx], jobs[-1] = jobs[-1], jobs[idx] 187 jobs.pop() 188 heapify(jobs) 189 190 191 class FSP(Scheduler): 192 193 def __init__(self, eps=1e-6): 194 195 # [remaining, jobid] queue for the *virtual* scheduler 196 self.queue = blist() 197 198 # Jobs that should have finished in the virtual time, 199 # but didn't in the real (happens only in case of estimation 200 # errors) 201 # Keys are jobids (ordered by the time they became late), values are 202 # not significant. 203 self.late = OrderedDict() 204 205 # last time we run the schedule function 206 self.last_t = 0 207 208 # Jobs that are running in the real time 209 self.running = set() 210 211 # Jobs that have less than eps work to do are considered done 212 # (deals with floating point imprecision) 213 self.eps = eps 214 215 def enqueue(self, t, jobid, size): 216 self.update(t) # needed to age only existing jobs in the virtual queue 217 insort(self.queue, [size, jobid]) 218 self.running.add(jobid) 219 220 def dequeue(self, t, jobid): 221 # the job remains in the virtual scheduler! 222 self.running.remove(jobid) 223 224 late = self.late 225 if jobid in late: 226 late.pop(jobid) 227 228 def update(self, t): 229 230 delta = t - self.last_t 231 232 queue = self.queue 233 234 if queue: 235 running = self.running 236 late = self.late 237 eps = self.eps 238 fair_share = delta / len(queue) 239 fair_plus_eps = fair_share + eps 240 241 # jobs in queue[:idx] are done in the virtual scheduler 242 idx = 0 243 for vrem, jobid in queue: 244 if vrem > fair_plus_eps: 245 break 246 idx += 1 247 if jobid in running: 248 late[jobid] = True 249 if idx: 250 del queue[:idx] 251 252 if fair_share > 0: 253 for vrem_jobid in queue: 254 vrem_jobid[0] -= fair_share 255 256 self.last_t = t 257 258 def schedule(self, t): 259 260 self.update(t) 261 262 late = self.late 263 if late: 264 return {next(iter(late)): 1} 265 266 running = self.running 267 if not running: 268 return {} 269 270 jobid = next(jobid for _, jobid in self.queue if jobid in running) 271 return {jobid: 1} 272 273 def next_internal_event(self): 274 275 queue = self.queue 276 277 if not queue: 278 return None 279 280 return queue[0][0] * len(queue) 281 282 283 class FSP_plus_PS(FSP): 284 285 def __init__(self, *args, **kwargs): 286 287 FSP.__init__(self, *args, **kwargs) 288 self.late = dict(self.late) # we don't need the order anymore! 289 290 def schedule(self, t): 291 292 self.update(t) 293 294 late = self.late 295 if late: 296 share = 1 / len(late) 297 return {jobid: share for jobid in late} 298 299 running = self.running 300 if not running: 301 return {} 302 303 jobid = next(jobid for _, jobid in self.queue if jobid in running) 304 return {jobid: 1} 305 306 307 class FSPE_PS_DC(FSP_plus_PS): 308 309 def schedule(self, t): 310 311 self.update(t) 312 queue = self.queue 313 running = self.running 314 scheduled = set(self.late) 315 try: 316 scheduled.add(next(jobid for _, jobid in self.queue 317 if jobid in running)) 318 except StopIteration: 319 pass 320 if scheduled: 321 share = 1 / len(scheduled) 322 return {jobid: share for jobid in scheduled} 323 else: 324 return {} 325 326 327 class LAS(Scheduler): 328 329 def __init__(self, eps=1e-6): 330 331 # job attained service is represented as (real attained service // eps) 332 # (not perfectly precise but avoids problems with floats) 333 self.eps = eps 334 335 # sorted dictionary for {attained: {jobid}} 336 self.queue = sorteddict() 337 338 # {jobid: attained} dictionary 339 self.attained = {} 340 341 # result of the last time the schedule() method was called 342 # grouped by {attained: [service, {jobid}]} 343 self.scheduled = {} 344 # This is the entry point for doing XXX + LAS schedulers: 345 # it's sufficient to touch here 346 347 # last time when the schedule was changed 348 self.last_t = 0 349 350 def enqueue(self, t, jobid, size): 351 352 self.queue.setdefault(0, set()).add(jobid) 353 self.attained[jobid] = 0 354 355 def dequeue(self, t, jobid): 356 357 att = self.attained.pop(jobid) 358 q = self.queue[att] 359 if len(q) == 1: 360 del self.queue[att] 361 else: 362 q.remove(jobid) 363 364 def update(self, t): 365 366 delta = intceil((t - self.last_t) / self.eps) 367 queue = self.queue 368 attained = self.attained 369 set_att = set(attained) 370 371 for att, sub_schedule in self.scheduled.items(): 372 373 jobids = reduce(set.union, (jobids for _, jobids in sub_schedule)) 374 375 # remove jobs from queue 376 377 try: 378 q_att = queue[att] 379 except KeyError: 380 pass # all jobids have terminated 381 else: 382 q_att -= jobids 383 if not q_att: 384 del queue[att] 385 386 # recompute attained values, re-put in queue, 387 # and update values in attained 388 389 for service, jobids in sub_schedule: 390 391 jobids &= set_att # exclude completed jobs 392 if not jobids: 393 continue 394 new_att = att + intceil(service * delta) 395 396 # let's coalesce pieces of work differing only by eps, to avoid 397 # rounding errors 398 attvals = [new_att, new_att - 1, new_att + 1] 399 try: 400 new_att = next(v for v in attvals if v in queue) 401 except StopIteration: 402 pass 403 404 queue.setdefault(new_att, set()).update(jobids) 405 for jobid in jobids: 406 attained[jobid] = new_att 407 self.last_t = t 408 409 def schedule(self, t): 410 411 self.update(t) 412 413 try: 414 attained, jobids = self.queue.items()[0] 415 except IndexError: 416 service = 0 417 jobids = set() 418 self.scheduled = {} 419 else: 420 service = 1 / len(jobids) 421 self.scheduled = {attained: [(service, jobids.copy())]} 422 423 return {jobid: service for jobid in jobids} 424 425 def next_internal_event(self): 426 427 queue = self.queue 428 429 if len(queue) >= 2: 430 qitems = queue.items() 431 running_attained, running_jobs = qitems[0] 432 waiting_attained, _ = qitems[1] 433 diff = waiting_attained - running_attained 434 return diff * len(running_jobs) * self.eps 435 else: 436 return None 437 438 439 class SRPT_plus_LAS(Scheduler): 440 441 def __init__(self, eps=1e-6): 442 443 # job that should have finished, but didn't 444 # (because of estimation errors) 445 self.late = set() 446 447 # [remaining, jobid] heap for the SRPT scheduler 448 self.queue = sortedlist() 449 450 # last time we run the update function 451 self.last_t = 0 452 453 # Jobs that have less than eps work to do are considered done 454 # (deals with floating point imprecision) 455 self.eps = eps 456 457 # {jobid: att} where att is jobid's attained service 458 self.attained = {} 459 460 # queue for late jobs, sorted by attained service 461 self.late_queue = sorteddict() 462 463 # last result of calling the schedule function 464 self.scheduled = {} 465 466 def enqueue(self, t, jobid, size): 467 468 size_int = intceil(size / self.eps) 469 self.queue.add([size_int, jobid]) 470 self.attained[jobid] = 0 471 472 def dequeue(self, t, jobid): 473 474 att = self.attained.pop(jobid) 475 late = self.late 476 if jobid in late: 477 late_queue = self.late_queue 478 late.remove(jobid) 479 latt = late_queue[att] 480 if len(latt) == 1: 481 del late_queue[att] 482 else: 483 latt.remove(jobid) 484 else: 485 queue = self.queue 486 for i, (_, jid) in enumerate(queue): 487 if jid == jobid: 488 del queue[i] 489 break 490 491 def update(self, t): 492 493 attained = self.attained 494 eps = self.eps 495 late = self.late 496 late_queue = self.late_queue 497 queue = self.queue 498 scheduled = self.scheduled 499 500 delta = intceil((t - self.last_t) / eps) 501 502 # Real attained service 503 504 def qinsert(jobid, att): 505 # coalesce pieces of work differing only by eps to avoid rounding 506 # errors -- return the chosen attained work value 507 attvals = [att, att - 1, att + 1] 508 try: 509 att = next(v for v in attvals if v in late_queue) 510 except StopIteration: 511 late_queue[att] = {jobid} 512 else: 513 late_queue[att].add(jobid) 514 return att 515 516 for jobid, service in scheduled.items(): 517 try: 518 old_att = self.attained[jobid] 519 except KeyError: # dequeued job 520 continue 521 work = intceil(delta * service) 522 new_att = old_att + work 523 if jobid in self.late: 524 l_old = late_queue[old_att] 525 l_old.remove(jobid) 526 if not l_old: 527 del late_queue[old_att] 528 new_att = qinsert(jobid, new_att) 529 else: 530 idx, rem = next((i, r) for i, (r, jid) 531 in enumerate(queue) 532 if jid == jobid) 533 new_rem = rem - work 534 if new_rem <= 0: 535 del queue[idx] 536 late.add(jobid) 537 new_att = qinsert(jobid, new_att) 538 else: 539 if idx == 0: 540 queue[0][0] = new_rem 541 else: 542 del queue[idx] 543 queue.add([new_rem, jobid]) 544 attained[jobid] = new_att 545 546 self.last_t = t 547 548 def schedule(self, t): 549 550 self.update(t) 551 552 queue = self.queue 553 late_queue = self.late_queue 554 555 jobs = {queue[0][1]} if queue else set() 556 if late_queue: 557 jobs.update(next(iter(late_queue.values()))) 558 559 if jobs: 560 service = 1 / len(jobs) 561 res = {jobid: service for jobid in jobs} 562 else: 563 res = {} 564 565 self.scheduled = res 566 return res 567 568 def next_internal_event(self): 569 570 queue = self.queue 571 572 if queue: 573 return queue[0][0] * (len(self.late) + 1) * self.eps 574 575 576 class FSP_plus_LAS(Scheduler): 577 578 def __init__(self, eps=1e-6): 579 580 # [remaining, jobid] queue for the *virtual* scheduler 581 self.queue = blist() 582 583 # Jobs that should have finished in the virtual time, 584 # but didn't in the real (happens only in case of estimation 585 # errors) 586 self.late = set() 587 588 # last time we run the schedule function 589 self.last_t = 0 590 591 # Jobs that are running in the real time 592 self.running = set() 593 594 # Jobs that have less than eps work to do are considered done 595 # (deals with floating point imprecision) 596 self.eps = eps 597 598 # queue for late jobs, sorted by attained service 599 self.late_queue = sorteddict() 600 601 # {jobid: att} where att is jobid's attained service 602 self.attained = {} 603 604 # last result of calling the schedule function 605 self.scheduled = {} 606 607 def enqueue(self, t, jobid, size): 608 609 self.update(t) # needed to age only existing jobs in the virtual queue 610 insort(self.queue, [intceil(size / self.eps), jobid]) 611 self.running.add(jobid) 612 self.attained[jobid] = 0 613 614 def dequeue(self, t, jobid): 615 616 late = self.late 617 618 self.running.remove(jobid) 619 if jobid in late: 620 late_queue = self.late_queue 621 late.remove(jobid) 622 att = self.attained[jobid] 623 latt = late_queue[att] 624 if len(latt) == 1: 625 del late_queue[att] 626 else: 627 latt.remove(jobid) 628 629 def update(self, t): 630 631 attained = self.attained 632 eps = self.eps 633 late = self.late 634 late_queue = self.late_queue 635 queue = self.queue 636 637 delta = intceil((t - self.last_t) / eps) 638 639 # Real attained service 640 641 def qinsert(jobid, att): 642 # coalesce pieces of work differing only by eps to avoid rounding 643 # errors -- return the chosen attained work value 644 attvals = [att, att - 1, att + 1] 645 try: 646 att = next(v for v in attvals if v in late_queue) 647 except StopIteration: 648 late_queue[att] = {jobid} 649 else: 650 late_queue[att].add(jobid) 651 return att 652 653 if delta: 654 for jobid, service in self.scheduled.items(): 655 if jobid in self.late: 656 old_att = self.attained[jobid] 657 l_old = late_queue[old_att] 658 l_old.remove(jobid) 659 if not l_old: 660 del late_queue[old_att] 661 new_att = old_att + intceil(delta * service) 662 new_att = qinsert(jobid, new_att) 663 attained[jobid] = new_att 664 else: 665 attained[jobid] += intceil(delta * service) 666 667 # Virtual scheduler 668 669 if queue: 670 running = self.running 671 fair_share = intceil(delta / len(queue)) 672 fair_plus_eps = fair_share + 1 673 674 # jobs in queue[:idx] are done in the virtual scheduler 675 idx = 0 676 for vrem, jobid in queue: 677 if vrem > fair_plus_eps: 678 break 679 idx += 1 680 if jobid in running: 681 late.add(jobid) 682 attained[jobid] = qinsert(jobid, attained[jobid]) 683 684 if idx: 685 del queue[:idx] 686 687 if fair_share > 0: 688 for vrem_jobid in queue: 689 vrem_jobid[0] -= fair_share 690 691 self.last_t = t 692 693 def schedule(self, t): 694 695 self.update(t) 696 697 late_queue = self.late_queue 698 running = self.running 699 700 if late_queue: 701 jobs = next(iter(late_queue.values())) 702 service = 1 / len(jobs) 703 res = {jobid: service for jobid in jobs} 704 elif not running: 705 res = {} 706 else: 707 jobid = next(jobid for _, jobid in self.queue if jobid in running) 708 res = {jobid: 1} 709 self.scheduled = res 710 711 return res 712 713 def next_internal_event(self): 714 715 eps = self.eps 716 late_queue = self.late_queue 717 queue = self.queue 718 719 res = None 720 721 if queue: 722 # time at which a job becomes late 723 res = queue[0][0] * len(queue) * eps 724 if len(late_queue) >= 2: 725 # time at which scheduled late jobs reach the service of 726 # others 727 qitems = late_queue.items() 728 running_attained, running_jobs = qitems[0] 729 waiting_attained, _ = qitems[1] 730 diff = waiting_attained - running_attained 731 delta = diff * len(running_jobs) * eps 732 if not res or res > delta: 733 res = delta 734 return res 735 736 737 class PSBS(Scheduler): 738 739 def __init__(self, eps=1e-6): 740 # heap of (gtime, jobid, weight) for the virtual time 741 self.queue = [] 742 743 # heap of (gtime, jobid, weight) for jobs that are in the virtual 744 # time, done in the real time and were at the head of 745 # self.queue 746 self.early = [] 747 748 # time allotted to the "ghost job" 749 self.gtime = 0 750 751 # {jobid: weight} for jobs that are finished in the virtual 752 # time, but not in the real (happens only in case of 753 # estimation errors) 754 self.late = {} 755 756 # last time we run the update method 757 self.last_t = 0 758 759 # Jobs that are running in the real time 760 self.running = set() 761 762 # Jobs that have less than eps work to do in virtual time are 763 # considered done (deals with floating point imprecision) 764 self.eps = eps 765 766 # equivalent to sum(late.values()) 767 self.late_w = 0 768 769 # equivalent to sum(w for _, _, w in queue + early) 770 self.virtual_w = 0 771 772 def enqueue(self, t, jobid, size, w=1): 773 774 if w <= 0: 775 raise ValueError("w needs to be positive") 776 777 self.update(t) # we need to age only existing jobs in the virtual queue 778 heappush(self.queue, (self.gtime + size / w, jobid, w)) 779 self.virtual_w += w 780 self.running.add(jobid) 781 782 def dequeue(self, t, jobid): 783 # job remains in the virtual time! 784 self.running.remove(jobid) 785 late = self.late 786 if jobid in self.late: 787 self.late_w -= late[jobid] 788 del late[jobid] 789 790 def update(self, t): 791 792 delta = t - self.last_t 793 794 virtual_w = self.virtual_w 795 796 if self.virtual_w > 0: 797 queue = self.queue 798 early = self.early 799 running = self.running 800 late = self.late 801 802 fair_share = delta / virtual_w 803 804 self.gtime = gtime = self.gtime + fair_share 805 gtime_plus_eps = gtime + self.eps 806 807 # deal with jobs that are done in the virtual scheduler 808 while queue and queue[0][0] < gtime_plus_eps: 809 _, jobid, w = heappop(queue) 810 self.virtual_w -= w 811 if jobid in running: 812 late[jobid] = w 813 self.late_w += w 814 while early and early[0][0] < gtime_plus_eps: 815 _, _, w = heappop(early) 816 self.virtual_w -= w 817 818 # reset to avoid precision erros due to floating point 819 if not queue and not early: 820 self.virtual_w = 0 821 else: 822 assert self.virtual_w > 0 823 824 self.last_t = t 825 826 def schedule(self, t): 827 828 self.update(t) 829 830 late = self.late 831 if late: 832 late_w = self.late_w 833 return {jobid: w / late_w for jobid, w in late.items()} 834 835 running = self.running 836 if not running: 837 return {} 838 839 queue = self.queue 840 early = self.early 841 while queue[0][1] not in running: 842 heappush(early, heappop(queue)) 843 return {queue[0][1]: 1} 844 845 def next_internal_event(self): 846 847 virtual_w = self.virtual_w 848 if virtual_w == 0: 849 return None 850 851 queue = self.queue 852 early = self.early 853 if queue: 854 if early: 855 v = min(queue[0][0], early[0][0]) 856 else: 857 v = queue[0][0] 858 else: 859 v = early[0][0] 860 861 return (v - self.gtime) * self.virtual_w 862 863 WFQE_GPS = PSBS 864 865 class FSPE_PS(WFQE_GPS): 866 def enqueue(self, t, jobid, size, w=None): 867 super(FSPE_PS, self).enqueue(t, jobid, size, 1) 868 869 class WSRPTE_GPS(Scheduler): 870 871 def __init__(self, eps=1e-6): 872 self.jobs = [] 873 self.last_t = 0 874 self.late = {} 875 self.late_w = 0 876 self.eps = eps 877 878 def update(self, t): 879 delta = t - self.last_t 880 jobs = self.jobs 881 if jobs: 882 rpt_over_w, w, _ = jobs[0] 883 work = delta / (w + self.late_w) 884 jobs[0][0] -= work / w 885 while jobs and jobs[0][0] < self.eps: 886 _, w, jobid = heappop(jobs) 887 self.late[jobid] = w 888 self.late_w += w 889 self.last_t = t 890 891 def next_internal_event(self): 892 jobs = self.jobs 893 if not jobs: 894 return None 895 rpt_over_w, w, _ = jobs[0] 896 return rpt_over_w * w * w / (w + self.late_w) # = rpt * w / (w + late_w) 897 898 def schedule(self, t): 899 self.update(t) 900 jobs = self.jobs 901 tot_w = self.late_w 902 if jobs: 903 _, w, jobid = jobs[0] 904 tot_w += w 905 schedule = {jobid: w / tot_w} 906 else: 907 schedule = {} 908 for jobid, w in self.late.items(): 909 schedule[jobid] = w / tot_w 910 return schedule 911 912 def enqueue(self, t, jobid, job_size, w=1): 913 self.update(t) 914 heappush(self.jobs, [job_size / w, w, jobid]) 915 916 def dequeue(self, t, jobid): 917 self.update(t) 918 late = self.late 919 if jobid in late: 920 del late[jobid] 921 return 922 # common case: we dequeue the running job 923 jobs = self.jobs 924 if jobid == jobs[0][2]: 925 heappop(jobs) 926 return 927 # we still care if we dequeue a job not running (O(n)) -- 928 # could be made more efficient, but still O(n) because of the 929 # search, by exploiting heap properties (i.e., local heappop) 930 try: 931 idx = next(i for i, v in jobs if v[2] == jobid) 932 except StopIteration: 933 raise ValueError("dequeuing missing job") 934 jobs[idx], jobs[-1] = jobs[-1], jobs[idx] 935 jobs.pop() 936 heapify(jobs)