mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
157 lines
5.5 KiB
Python
157 lines
5.5 KiB
Python
"""Real processes: a worker killed at each point is recovered by the next;
|
|
one worker per host; concurrent admissions and submissions never double up."""
|
|
|
|
import multiprocessing
|
|
import os
|
|
import shutil
|
|
import signal
|
|
import tempfile
|
|
import time
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.db import connections
|
|
from django.test import TransactionTestCase
|
|
|
|
from operations import claims, jobs
|
|
from operations.models import Claim, Operation, WorkerLease
|
|
from operations.tests import fakes, proc_kinds # noqa: F401
|
|
|
|
|
|
def _worker(data):
|
|
connections.close_all()
|
|
from operations.worker import Worker
|
|
os._exit(Worker(tick=0.05, heartbeat_every=0.2, grace=1, data=data).run())
|
|
|
|
|
|
def _admit(key, results):
|
|
connections.close_all()
|
|
try:
|
|
claims.admit({key: Claim.EXCLUSIVE}, holder=f"p{os.getpid()}")
|
|
results.put("admitted")
|
|
except claims.Busy:
|
|
results.put("busy")
|
|
connections.close_all()
|
|
results.close()
|
|
results.join_thread() # the feeder thread must deliver before os._exit
|
|
os._exit(0)
|
|
|
|
|
|
def _submit(user_pk, results):
|
|
connections.close_all()
|
|
user = get_user_model().objects.get(pk=user_pk)
|
|
try:
|
|
results.put(str(jobs.submit("test.noop", user, {"uuid": "race"}, "key-race0001").pk))
|
|
except Exception as err:
|
|
results.put(f"error:{type(err).__name__}")
|
|
connections.close_all()
|
|
results.close()
|
|
results.join_thread()
|
|
os._exit(0)
|
|
|
|
|
|
class ProcessTestCase(TransactionTestCase):
|
|
def setUp(self):
|
|
self.ctx = multiprocessing.get_context("fork")
|
|
self.user = get_user_model().objects.create_user("proc_user", "p@example.com", "pw")
|
|
self.dir = tempfile.mkdtemp()
|
|
self.children = []
|
|
|
|
def tearDown(self):
|
|
for p in self.children:
|
|
if p.is_alive():
|
|
p.kill()
|
|
p.join(10)
|
|
shutil.rmtree(self.dir, ignore_errors=True)
|
|
|
|
def spawn(self, target, *args):
|
|
connections.close_all()
|
|
p = self.ctx.Process(target=target, args=args)
|
|
p.start()
|
|
self.children.append(p)
|
|
return p
|
|
|
|
def wait(self, predicate, seconds=30):
|
|
end = time.monotonic() + seconds
|
|
while time.monotonic() < end:
|
|
if predicate():
|
|
return True
|
|
time.sleep(0.1)
|
|
return False
|
|
|
|
def touch(self, name):
|
|
open(os.path.join(self.dir, name), "w").close()
|
|
|
|
def exists(self, name):
|
|
return os.path.exists(os.path.join(self.dir, name))
|
|
|
|
def test_concurrent_admissions_admit_once(self):
|
|
results = self.ctx.Queue()
|
|
for _ in range(8):
|
|
self.spawn(_admit, "instance:race", results)
|
|
outcomes = sorted(results.get(timeout=30) for _ in range(8))
|
|
self.assertEqual(outcomes.count("admitted"), 1, outcomes)
|
|
for p in self.children:
|
|
p.join(10)
|
|
self.assertEqual(p.exitcode, 0)
|
|
|
|
def test_concurrent_duplicate_submissions_return_one_job(self):
|
|
results = self.ctx.Queue()
|
|
for _ in range(4):
|
|
self.spawn(_submit, self.user.pk, results)
|
|
outcomes = {results.get(timeout=30) for _ in range(4)}
|
|
self.assertEqual(len(outcomes), 1, outcomes)
|
|
self.assertFalse(any(o.startswith("error:") for o in outcomes), outcomes)
|
|
self.assertEqual(Operation.objects.count(), 1)
|
|
|
|
def test_a_second_worker_on_the_host_exits_with_1(self):
|
|
first = self.spawn(_worker, self.dir)
|
|
time.sleep(1)
|
|
second = self.spawn(_worker, self.dir)
|
|
second.join(15)
|
|
self.assertEqual(second.exitcode, 1)
|
|
os.kill(first.pid, signal.SIGTERM)
|
|
first.join(15)
|
|
self.assertEqual(first.exitcode, 0)
|
|
|
|
def test_a_worker_of_another_installation_does_not_take_a_live_lease(self):
|
|
first = self.spawn(_worker, self.dir)
|
|
self.assertTrue(self.wait(lambda: WorkerLease.objects.exists()))
|
|
holder = WorkerLease.objects.get().holder
|
|
other = tempfile.mkdtemp()
|
|
self.addCleanup(shutil.rmtree, other, True)
|
|
second = self.spawn(_worker, other) # its own flock: same hostname, another data directory
|
|
time.sleep(2)
|
|
self.assertEqual(WorkerLease.objects.get().holder, holder)
|
|
for p in (first, second):
|
|
os.kill(p.pid, signal.SIGTERM)
|
|
p.join(15)
|
|
self.assertEqual(p.exitcode, 0)
|
|
|
|
def kill_at(self, point):
|
|
op = jobs.submit("test.filesteps", self.user, {"dir": self.dir}, "key-proc0001")
|
|
worker = self.spawn(_worker, self.dir)
|
|
self.assertTrue(self.wait(lambda: self.exists("started")))
|
|
if point == "after_effect":
|
|
self.touch("go-intent")
|
|
self.assertTrue(self.wait(lambda: self.exists("effect")))
|
|
os.kill(worker.pid, signal.SIGKILL)
|
|
worker.join(10)
|
|
self.touch("go-intent")
|
|
self.touch("go-finish")
|
|
nxt = self.spawn(_worker, self.dir)
|
|
try:
|
|
self.assertTrue(self.wait(lambda: Operation.objects.get(pk=op.pk).state in Operation.TERMINAL))
|
|
finally:
|
|
os.kill(nxt.pid, signal.SIGTERM)
|
|
nxt.join(15)
|
|
return Operation.objects.get(pk=op.pk)
|
|
|
|
def test_killed_before_intent_the_job_fails(self):
|
|
op = self.kill_at("before_intent")
|
|
self.assertEqual(op.state, Operation.FAILED)
|
|
self.assertFalse(self.exists("effect"))
|
|
|
|
def test_killed_after_the_effect_the_job_is_recovered_as_succeeded(self):
|
|
op = self.kill_at("after_effect")
|
|
self.assertEqual(op.state, Operation.SUCCEEDED)
|
|
self.assertFalse(Claim.objects.filter(operation=op).exists())
|