diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/check.py | 425 |
1 files changed, 230 insertions, 195 deletions
diff --git a/tests/check.py b/tests/check.py index 8814add..1e14e12 100644 --- a/tests/check.py +++ b/tests/check.py @@ -3,14 +3,14 @@ aup builds (the sibling checkout, --aup-root). The core's and the store's own tests are core/test-module.c and store/test-tree-store.c. -1. Documents: pure-serve running the document editor written in aup - (aup's examples/editor.aup) answers random requests exactly as the earlier - document server running editor.pure, with one and with three connection +1. Documents: keyed byte documents as a keyed list (DOCS_SOURCE, each edit + checked against the revision it was made on) answer random edits and + reads exactly as a model says, with one and with three connection threads, on persistent and pipelined connections too. -2. Lending: a read lends the stored extents and still carries its version - while the document changes; lent answers count against the payload budget; - a text view is lent while its answer waits and let go when it is read or - its connection closes, the store keeping one mapping again. +2. Lending: a text view lends the state's stored extents while its answer + waits, each answer whole in one version while the text changes, and lets + them go when it is read or its connection closes, the store keeping one + mapping again; lent answers count against the payload budget. 3. Durability: concurrent edits share commits, and every acknowledged edit survives kill -9, with one and with three threads; so do the app's acknowledged posts (keyed lists, JSON bodies with groups). @@ -39,8 +39,8 @@ from pathlib import Path HERE = Path(__file__).resolve().parent ROOT = HERE.parent -# The app that exercises documents: the editor written in aup, from aup's examples. -EDITOR = ROOT.parent / 'aup' / 'examples' / 'editor.aup' +# aup's examples (its checkout beside this one, or --aup-root). +EXAMPLES = ROOT.parent / 'aup' / 'examples' def run(*args, stdin=None, check=True): @@ -67,19 +67,6 @@ class HttpHost: self.info = json.loads(line) # its port, name, public key self.port = self.info['port'] - def request(self, method, key, body=b'', expected=None): - import http.client - headers = {'Content-Length': str(len(body))} - if expected is not None: - headers['If-None-Match' if expected == 0 else 'If-Match'] = '*' if expected == 0 else f'"{expected}"' - connection = http.client.HTTPConnection('127.0.0.1', self.port, timeout=30) - try: - connection.request(method, f'/objects/{key}', body, headers) - response = connection.getresponse() - return response.status, response.getheader('ETag'), response.getheader('X-Pure-Error'), response.read() - finally: - connection.close() - def stop(self): self.process.terminate() _, error = self.process.communicate(timeout=30) @@ -87,48 +74,146 @@ class HttpHost: return [json.loads(line) for line in error.decode().splitlines() if line.startswith('{')] -def server_parity(aup, pure, work, seeds, operations=150, threads=1): - """The document server running editor.aup answers every request exactly - as the server running editor.pure: status, revision, error and body.""" - import struct - module = work / 'editor-server.pmod' - compile_module(aup, EDITOR, module) +DOCS_SOURCE = """record Doc { + id: text + body: bytes + revision: i64 +} +state docs: list of Doc by id + +input edit = message { + id: text + base: i64 + start: u32 + remove: u32 + insert: bytes +} + +on edit { + if docs.has(edit.id) { + let d = docs[edit.id] + require d.revision == edit.base + require edit.start + edit.remove <= d.body.bytes + docs[edit.id].body = d.body[0 .. edit.start] + edit.insert + d.body[edit.start + edit.remove .. d.body.bytes] + docs[edit.id].revision = d.revision + 1 + } else { + require edit.base == 0 && edit.start == 0 && edit.remove == 0 + docs.add(Doc { id: edit.id, body: edit.insert, revision: 1 }) + } + reply edit.insert +} + +view revision(id: text) = docs[id].revision +view size(id: text) = docs[id].body.bytes +view body(id: text) = text(docs[id].body) +""" + + +def edit_body(key, base, start, remove, insert): + """An edit of DOCS_SOURCE as its JSON body.""" + return json.dumps({'id': key, 'base': base, 'start': start, 'remove': remove, 'insert': insert}).encode() + + +def edit_request(key, base, start, remove, insert): + body = edit_body(key, base, start, remove, insert) + return (b'POST /inputs/edit HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\nContent-Length: ' + + str(len(body)).encode() + b'\r\n\r\n' + body) + + +def view_request(view, key): + from urllib.parse import quote + return f'GET /views/{view}?id={quote(key)} HTTP/1.1\r\nHost: x\r\n\r\n'.encode() + + +def docs_call(port, request): + """One raw request on its own connection: status, X-Pure-Error, body.""" + import socket + with socket.create_connection(('127.0.0.1', port), timeout=30) as sock: + sock.sendall(request) + return read_answer(sock.makefile('rb')) + + +def read_answer(reader): + status = int(reader.readline().split()[1]) + length, error = 0, None + for line in iter(reader.readline, b'\r\n'): + name, _, value = line.decode().partition(':') + if name.lower() == 'content-length': length = int(value) + if name.lower() == 'x-pure-error': error = value.strip() + return status, error, reader.read(length) + + +class DocsModel: + """DOCS_SOURCE as Python: an edit's answer (status, error, body) and each document's revision and body.""" + + def __init__(self): + self.docs = {} + + def edit(self, key, base, start, remove, insert): + body, revision = self.docs.get(key, (b'', 0)) + data = insert.encode() + if key in self.docs: + if base != revision or start + remove > len(body): return 422, 'limit', b'' + self.docs[key] = (body[:start] + data + body[start + remove:], revision + 1) + elif base or start or remove: + return 422, 'limit', b'' + else: + self.docs[key] = (data, 1) + return 200, 'none', data + + def view(self, view, key): + if key not in self.docs: return 404, 'no_value', b'' + body, revision = self.docs[key] + if view == 'body': + try: + body.decode() + except UnicodeDecodeError: # cut inside a character: text(...) refuses it + return 400, 'invalid', b'' + return 200, 'none', body + return 200, 'none', str(revision if view == 'revision' else len(body)).encode() + + +def server_documents(aup, pure, work, seeds, operations=150, threads=1): + """Keyed byte documents as a keyed list (027's 4b; DOCS_SOURCE): random + edits (current, stale and future revisions, past the end, texts of any + size up to 5,000 bytes) and reads of a document's revision, size and + body answer exactly as a model says, with one and with three connection + threads; after a restart every document is as acknowledged.""" + module = work / 'docs.pmod' + (work / 'docs.aup').write_text(DOCS_SOURCE) + compile_module(aup, work / 'docs.aup', module) compared = 0 + words = ['', 'a', 'hello', '빨리', 'a "quoted" \\ word', '😀', 'line\nbreak'] for seed in seeds: rng = random.Random(seed + 13000) - old = HttpHost([pure / 'prototypes/connections/build/tree/pure-connections', '--program', pure / 'prototypes/connections/examples/editor.pure', - '--store', work / f'http-old-{seed}-{threads}']) - new = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / f'http-new-{seed}-{threads}', - '--threads', str(threads)]) + store = work / f'docs-{seed}-{threads}' + model = DocsModel() + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', str(threads)]) try: - revision, size = {}, {} for _ in range(operations): - key = rng.choice(['a', 'b', 'long-key_1']) - current, length = revision.get(key, 0), size.get(key, 0) + key = rng.choice(['a', 'b', 'long-key_1', '문서']) + body, current = model.docs.get(key, (b'', 0)) if rng.random() < 0.35: - request = ('GET', key, b'', None) + view = rng.choice(['revision', 'size', 'body']) + want, got = model.view(view, key), docs_call(host.port, view_request(view, key)) else: pick = rng.random() base = current if pick < 0.75 else max(current - 1, 0) if pick < 0.9 else current + 3 - if rng.random() < 0.05: - body = bytes(rng.randrange(256) for _ in range(rng.randint(0, 7))) # too short - else: - insert = bytes(rng.randrange(256) for _ in range(rng.choice([0, 1, 7, 300, 5000]))) - body = struct.pack('<II', rng.randint(0, length + 2), rng.choice([0, 1, 3, length])) + insert - request = ('PUT', key, body, base) - theirs, ours = old.request(*request), new.request(*request) - assert ours == theirs, (seed, request[:2], request[3], ours[:3], theirs[:3]) - if theirs[0] == 200 and theirs[1]: - revision[key] = int(theirs[1].strip('"')) - if request[0] == 'GET': - size[key] = len(theirs[3]) - else: - size[key] = len(old.request('GET', key)[3]) - assert new.request('GET', key) == old.request('GET', key) + insert = ''.join(rng.choice(words) for _ in range(rng.choice([0, 1, 7, 300, 700])))[:5000] + start, remove = rng.randint(0, len(body) + 2), rng.choice([0, 1, 3, len(body)]) + want = model.edit(key, base, start, remove, insert) + got = docs_call(host.port, edit_request(key, base, start, remove, insert)) + assert got == want, (seed, key, got[:2], want[:2]) compared += 1 finally: - old.stop() - new.stop() + host.stop() + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', str(threads)]) + try: + for key in model.docs: + for view in ('revision', 'size', 'body'): + assert docs_call(host.port, view_request(view, key)) == model.view(view, key), (key, view) + finally: + host.stop() return compared @@ -136,122 +221,77 @@ def server_pipelining(aup, pure, work): """One connection, many requests: a keep-alive client and a pipelined burst (all requests in one write, one split across writes) get the same answers, in order, as separate connections would.""" - import http.client import socket - import struct - module = work / 'editor-pipeline.pmod' - compile_module(aup, EDITOR, module) + module = work / 'docs-pipeline.pmod' + (work / 'docs.aup').write_text(DOCS_SOURCE) + compile_module(aup, work / 'docs.aup', module) host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'pipeline']) + answers = [] try: - connection = http.client.HTTPConnection('127.0.0.1', host.port, timeout=30) - answers = [] - for body, headers in [(struct.pack('<II', 0, 0) + b'hello', {'If-None-Match': '*'}), - (struct.pack('<II', 5, 0) + b'!', {'If-Match': '"1"'}), - (struct.pack('<II', 0, 0) + b'x', {'If-Match': '"1"'})]: - connection.request('PUT', '/objects/p', body, headers) - response = connection.getresponse() - answers.append((response.status, response.getheader('ETag'), response.read())) - connection.request('GET', '/objects/p') - response = connection.getresponse() - answers.append((response.status, response.getheader('ETag'), response.read())) - connection.close() - assert answers == [(200, '"1"', b'hello'), (200, '"2"', b'!'), (412, '"2"', b''), (200, '"2"', b'hello!')], answers - - def request(method, key, body=b'', condition=''): - return (f'{method} /objects/{key} HTTP/1.1\r\nHost: x\r\nContent-Length: {len(body)}\r\n{condition}\r\n' - .encode() + body) - pipeline = (request('GET', 'p') + request('PUT', 'q', struct.pack('<II', 0, 0) + b'q1', 'If-None-Match: *\r\n') + - request('GET', 'q') + request('GET', 'missing') + - request('PUT', 'q', struct.pack('<II', 2, 0) + b'!', 'If-Match: "1"\r\n') + request('GET', 'q')) - expected = [(200, b'hello!'), (200, b'q1'), (200, b'q1'), (404, b''), (200, b'!'), (200, b'q1!')] + with socket.create_connection(('127.0.0.1', host.port), timeout=30) as sock: + reader = sock.makefile('rb') + for request in (edit_request('p', 0, 0, 0, 'hello'), edit_request('p', 1, 5, 0, '!'), + edit_request('p', 1, 0, 0, 'x'), view_request('body', 'p')): + sock.sendall(request) + answers.append(read_answer(reader)) + assert answers == [(200, 'none', b'hello'), (200, 'none', b'!'), (422, 'limit', b''), (200, 'none', b'hello!')], answers + pipeline = (view_request('body', 'p') + edit_request('q', 0, 0, 0, 'q1') + view_request('body', 'q') + + view_request('body', 'missing') + edit_request('q', 1, 2, 0, '!') + view_request('body', 'q')) + expected = [(200, 'none', b'hello!'), (200, 'none', b'q1'), (200, 'none', b'q1'), (404, 'no_value', b''), + (200, 'none', b'!'), (200, 'none', b'q1!')] for split in (len(pipeline), 7, 61): # one write, and small writes that cut heads and bodies with socket.create_connection(('127.0.0.1', host.port), timeout=30) as sock: for at in range(0, len(pipeline), split): sock.sendall(pipeline[at:at + split]) reader = sock.makefile('rb') - for status, body in expected: - code = int(reader.readline().split()[1]) - length = 0 - for line in iter(reader.readline, b'\r\n'): - name, _, value = line.decode().partition(':') - if name.lower() == 'content-length': - length = int(value) - assert (code, reader.read(length)) == (status, body), (split, code, status, body) - # The next round starts from the same documents. - host.request('PUT', 'q', struct.pack('<II', 0, 3) + b'q1', int(host.request('GET', 'q')[1].strip('"'))) - expected = [(200, b'hello!'), (412, b''), (200, b'q1'), (404, b''), (412, b''), (200, b'q1')] - pipeline = (request('GET', 'p') + request('PUT', 'q', struct.pack('<II', 0, 0) + b'q1', 'If-None-Match: *\r\n') + - request('GET', 'q') + request('GET', 'missing') + - request('PUT', 'q', struct.pack('<II', 2, 0) + b'!', 'If-Match: "1"\r\n') + request('GET', 'q')) + got = [read_answer(reader) for _ in expected] + assert got == expected, (split, got) + # The next round edits a document made already: its creation is refused, its later edit not. + revision = int(docs_call(host.port, view_request('revision', 'q'))[2]) + pipeline = (view_request('body', 'p') + edit_request('q', 0, 0, 0, 'q1') + view_request('size', 'q') + + view_request('body', 'missing') + edit_request('q', revision, 0, 0, '!') + view_request('size', 'q')) + size = int(docs_call(host.port, view_request('size', 'q'))[2]) + expected = [(200, 'none', b'hello!'), (422, 'limit', b''), (200, 'none', str(size).encode()), + (404, 'no_value', b''), (200, 'none', b'!'), (200, 'none', str(size + 1).encode())] finally: stats = host.stop() assert stats[0]['active'] == 0 and stats[0]['retained_payload_bytes'] == 0, stats return len(answers) + 3 * len(expected) -def server_lent_reads(aup, pure, work): - """A read lends the stored document's extents instead of copying them: a - 1 MiB answer sent slowly still carries the version read while the - document is replaced five times meanwhile.""" - import socket - import struct - module = work / 'editor-lent.pmod' - compile_module(aup, EDITOR, module) - host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'lent']) - big = bytes(range(256)) * 4096 - try: - assert host.request('PUT', 'big', struct.pack('<II', 0, 0) + big, 0)[0] == 200 - with socket.create_connection(('127.0.0.1', host.port), timeout=30) as sock: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4096) - sock.sendall(b'GET /objects/big HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\n\r\n') - data = sock.recv(100) - for revision in range(1, 6): - edit = struct.pack('<II', 0, len(big) if revision == 1 else 1) + b'Z' - assert host.request('PUT', 'big', edit, revision)[0] == 200 - while b'\r\n\r\n' not in data or len(data) < data.index(b'\r\n\r\n') + 4 + len(big): - chunk = sock.recv(65536) - assert chunk, len(data) - data += chunk - assert data[data.index(b'\r\n\r\n') + 4:] == big - assert host.request('GET', 'big') == (200, '"6"', 'none', b'Z') - finally: - stats = host.stop() - assert stats[0]['active'] == 0 and stats[0]['retained_payload_bytes'] == 0, stats - return len(big) - - def server_lent_budget(aup, pure, work): """Lent extents count against the payload budget while an answer waits - on its socket. Two readers pipeline 1 MiB reads without reading: the - kernel takes what it can and each reader leaves one answer waiting. A - third request finds no room (503 payload_budget) until a reader goes.""" + on its socket. Two readers pipeline 1 MiB reads of a text view without + reading: the kernel takes what it can and each reader leaves one answer + waiting. A third request finds no room (503 payload_budget) until a + reader goes.""" import socket - import struct import time - module = work / 'editor-budget.pmod' - compile_module(aup, EDITOR, module) - big = bytes(range(256)) * 4096 + module, source = work / 'note-budget.pmod', work / 'note.aup' + source.write_text(NOTE_SOURCE) + compile_module(aup, source, module) + big = (bytes(range(32, 127)) * ((1 << 20) // 95 + 1))[:1 << 20] # 1 MiB of text budget = 2 * len(big) + len(big) // 2 host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'lent-budget', '--budget', budget]) readers = [] try: - assert host.request('PUT', 'big', struct.pack('<II', 0, 0) + big, 0)[0] == 200 + assert post(host.port, '/inputs/write', {'text': big.decode()})[0] == 200 for _ in range(2): sock = socket.create_connection(('127.0.0.1', host.port), timeout=30) sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4096) - sock.sendall(b'GET /objects/big HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\n\r\n' * 32) + sock.sendall(b'GET /views/page HTTP/1.1\r\nHost: x\r\n\r\n' * 32) readers.append(sock) time.sleep(0.3) - refused = host.request('GET', 'big') - assert refused[0] == 503 and refused[2] == 'payload_budget', refused[:3] + refused = http(host.port, 'GET', '/views/page') + assert refused[:2] == (503, 'payload_budget'), refused[:2] readers.pop().close() for _ in range(100): - answered = host.request('GET', 'big') + answered = http(host.port, 'GET', '/views/page') if answered[0] == 200: break time.sleep(0.05) - assert answered[0] == 200 and answered[3] == big, answered[:3] + assert answered[0] == 200 and answered[3] == big, answered[:2] finally: for sock in readers: sock.close() @@ -351,25 +391,25 @@ def server_lent_views(aup, pure, work, threads=1): def server_bursts(aup, pure, work, clients=8, edits=30, shards=1): """Concurrent edits share commits, and every acknowledged edit survives a kill -9 of the server.""" - import http.client import signal - import struct import threading import time - module = work / 'editor-bursts.pmod' - compile_module(aup, EDITOR, module) + module = work / 'docs-bursts.pmod' + (work / 'docs.aup').write_text(DOCS_SOURCE) + compile_module(aup, work / 'docs.aup', module) + store = work / f'bursts-{shards}' def load(host, rounds, acknowledged): def client(number): key, base = f'c{number}', acknowledged[number][1] for _ in range(rounds): try: - status, tag, _, _ = host.request('PUT', key, struct.pack('<II', 0, 0) + b'x', base) - except (OSError, http.client.HTTPException): # the server died mid-answer: not acknowledged + status, _, _ = docs_call(host.port, edit_request(key, base, 0, 0, 'x')) + except Exception: # the server died mid-answer: not acknowledged return if status != 200: return - base = int(tag.strip('"')) + base += 1 acknowledged[number] = (acknowledged[number][0] + 1, base) threads = [threading.Thread(target=client, args=(n,)) for n in range(clients)] start = time.perf_counter() @@ -377,22 +417,18 @@ def server_bursts(aup, pure, work, clients=8, edits=30, shards=1): thread.start() return threads, start - timings = {} - for name, args in [('program', [pure / 'prototypes/connections/build/tree/pure-connections', '--program', pure / 'prototypes/connections/examples/editor.pure']), - ('module', [pure / 'build/pure-serve', '--module', module, '--threads', str(shards)])]: - host = HttpHost(args + ['--store', work / f'bursts-{name}-{shards}']) - acknowledged = [(0, 0)] * clients - threads, start = load(host, edits, acknowledged) - for thread in threads: - thread.join() - timings[name] = time.perf_counter() - start - assert all(count == edits for count, _ in acknowledged), acknowledged - stats = host.stop() - if name == 'module': - bursts = stats[0]['bursts'] - assert stats[0]['burst_events'] == clients * edits and (clients == 1 or bursts < clients * edits), stats - # Kill the module server while clients edit; what it acknowledged is stored. - store = work / f'bursts-module-{shards}' + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', str(shards)]) + acknowledged = [(0, 0)] * clients + threads, start = load(host, edits, acknowledged) + for thread in threads: + thread.join() + seconds = time.perf_counter() - start + assert all(count == edits for count, _ in acknowledged), acknowledged + stats = host.stop() + owner = next(line for line in stats if 'bursts' in line) + bursts = owner['bursts'] + assert owner['burst_events'] == clients * edits and (clients == 1 or bursts < clients * edits), stats + # Kill the server while clients edit; what it acknowledged is stored. host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', str(shards)]) acknowledged = [(edits, edits)] * clients threads, _ = load(host, 1000, acknowledged) @@ -404,13 +440,13 @@ def server_bursts(aup, pure, work, clients=8, edits=30, shards=1): host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', str(shards)]) try: for number, (count, base) in enumerate(acknowledged): - status, tag, _, body = host.request('GET', f'c{number}') - stored = int(tag.strip('"')) - assert status == 200 and stored in (base, base + 1) and len(body) == stored, (number, count, base, tag) + status, _, body = docs_call(host.port, view_request('revision', f'c{number}')) + stored = int(body) + size = int(docs_call(host.port, view_request('size', f'c{number}'))[2]) + assert status == 200 and stored in (base, base + 1) and size == stored, (number, count, base, stored) finally: host.stop() - return {'writes': clients * edits, 'bursts': bursts, 'program_seconds': round(timings['program'], 3), - 'module_seconds': round(timings['module'], 3), + return {'writes': clients * edits, 'bursts': bursts, 'seconds': round(seconds, 3), 'acknowledged_before_kill': sum(count for count, _ in acknowledged) - clients * edits} @@ -1700,7 +1736,7 @@ def module_specs(aup, pure, work): import struct runs = {} program = work / 'sha256.pmod' - compile_module(aup, EDITOR.parent / 'sha256.aup', program) + compile_module(aup, EXAMPLES / 'sha256.aup', program) chooser = random.Random(140) messages = [b'abc', b'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq', b'abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu'] @@ -1715,7 +1751,7 @@ def module_specs(aup, pure, work): assert digests == [hashlib.sha256(m).hexdigest() for m in messages], 'sha256' runs['sha256'] = len(messages) program = work / 'json.pmod' - compile_module(aup, EDITOR.parent / 'json.aup', program) + compile_module(aup, EXAMPLES / 'json.aup', program) suite = pure / 'tests/specs/json' cases = {name: (suite / name).read_bytes() for name in sorted(os.listdir(suite)) if name.endswith('.json')} cases['n_structure_100000_opening_arrays.json'] = b'[' * 100000 @@ -1776,7 +1812,7 @@ def module_specs(aup, pure, work): cases[f'r_{k}.json'] = json.dumps(value(0), ensure_ascii=chooser.random() < 0.5, indent=chooser.choice([None, 1, 2])).encode() (work / f'r_{k}.json').write_bytes(cases[f'r_{k}.json']) program = work / 'json_tree.pmod' - compile_module(aup, EDITOR.parent / 'json_tree.aup', program) + compile_module(aup, EXAMPLES / 'json_tree.aup', program) given = ''.join(f'message check @{work / name}\n' for name in cases) outputs = {name: subprocess.run([str(pure / 'build/pure-run'), str(program)], input=given, capture_output=True, text=True, timeout=600, env=dict(os.environ, **env)).stdout @@ -4445,12 +4481,12 @@ def cpu_seconds(pid): def server_clocks(aup, pure, work): """The wall clock moved an hour while a node runs (build/clock-back.so moves what the node reads of it, from its start): its timers, the pauses - of a wire whose receiver is down, the owner's waits and a store's group - commit all count on the monotonic clock. Were a wait's deadline read - from the wall clock, set back it would end at once, again and again (a - core spun, the wire tried thousands of times); set forward it would last - an hour (the timers stopped, the wire tried once, and a lone commit - after a group waited that long for the others to join it).""" + of a wire whose receiver is down and the owner's waits all count on the + monotonic clock. Were a wait's deadline read from the wall clock, set + back it would end at once, again and again (a core spun, the wire tried + thousands of times); set forward it would last an hour (the timers + stopped, the wire tried once, and a lone edit after three at once waited + that long).""" import os import socket import struct @@ -4479,12 +4515,12 @@ def server_clocks(aup, pure, work): (shift, stats['ticks'], share, stats['delivery_attempts']) seen[f'shift_{shift}'] = {'ticks': stats['ticks'], 'cpu_share': round(share, 3), 'wire_attempts': stats['delivery_attempts']} - editor = work / 'clocks-editor.pmod' - compile_module(aup, EDITOR, editor) + editor = work / 'clocks-docs.pmod' + (work / 'clocks-docs.aup').write_text(DOCS_SOURCE) + compile_module(aup, work / 'clocks-docs.aup', editor) env = {**os.environ, 'LD_PRELOAD': str(pure / 'build/clock-back.so'), 'CLOCK_BACK_MS': '0', 'CLOCK_SHIFT_S': '3600'} host = HttpHost([pure / 'build/pure-serve', '--module', editor, '--store', work / 'clocks-docs', '--threads', 3], env=env) - new = struct.pack('<II', 0, 0) # an edit that makes the document: at 0, none removed, then its bytes statuses, alone = [], 0.0 try: for round_number in range(20): # three at once, grouped; then one alone @@ -4492,7 +4528,7 @@ def server_clocks(aup, pure, work): def put(k): together.wait() - statuses.append(host.request('PUT', f'd{round_number}-{k}', new + b'x' * 100000, 0)[0]) + statuses.append(docs_call(host.port, edit_request(f'd{round_number}-{k}', 0, 0, 0, 'x' * 100000))[0]) threads = [threading.Thread(target=put, args=(k,)) for k in range(3)] for thread in threads: @@ -4500,12 +4536,11 @@ def server_clocks(aup, pure, work): for thread in threads: thread.join() began = time.time() - statuses.append(host.request('PUT', f'alone-{round_number}', new + b'y', 0)[0]) + statuses.append(docs_call(host.port, edit_request(f'alone-{round_number}', 0, 0, 0, 'y'))[0]) alone = max(alone, time.time() - began) finally: - stats = host.stop()[-1] - assert statuses == [200] * 80 and alone < 1 and stats['grouped_commits'] > stats['commit_groups'], \ - (statuses, alone, stats) + host.stop() + assert statuses == [200] * 80 and alone < 1, (statuses, alone) seen['alone_after_groups_s'] = round(alone, 4) return seen @@ -5790,7 +5825,7 @@ def server_view_bodies(aup, pure, work): import random import threading module = work / 'routes-view.pmod' - compile_module(aup, EDITOR.parent / 'routes.aup', module) + compile_module(aup, EXAMPLES / 'routes.aup', module) rng = random.Random(18) n, edges = 300, set() while len(edges) < 1200: @@ -6268,8 +6303,9 @@ def server_tokens(aup, pure, work): for name, text in (('tokens', TOKENS_SOURCE), ('tokens-sender', TOKENS_SENDER)): (work / f'{name}.aup').write_text(text) compile_module(aup, work / f'{name}.aup', work / f'{name}.pmod') - editor = work / 'tokens-editor.pmod' - compile_module(aup, EDITOR, editor) + editor = work / 'tokens-docs.pmod' + (work / 'tokens-docs.aup').write_text(DOCS_SOURCE) + compile_module(aup, work / 'tokens-docs.aup', editor) serve = pure / 'build/pure-serve' names = {'read': 'r' * 40, 'write': 'w' * 40, 'input=add': 'i' * 40, 'read2': 'q' * 40} lengths = {n: bytes(65 + (k * 7) % 57 for k in range(n)).decode() for n in (1, 55, 56, 63, 64, 65, 119, 200)} @@ -6306,7 +6342,7 @@ def server_tokens(aup, pure, work): try: none = call('GET', '/t/views/total') assert none[:3] == (401, 'unauthorized', 'Bearer'), none - for method, path in (('POST', '/t/inputs/add?step=1'), ('GET', '/docs/objects/k'), ('PUT', '/t/queries/q'), + for method, path in (('POST', '/t/inputs/add?step=1'), ('GET', '/docs/views/body?id=k'), ('PUT', '/t/queries/q'), ('GET', '/t/queries/q')): assert call(method, path)[:2] == (401, 'unauthorized'), (method, path) assert call('GET', '/t/views/total', 'x' * 40)[:2] == (401, 'unauthorized') @@ -6314,11 +6350,11 @@ def server_tokens(aup, pure, work): assert call('GET', '/t/views/total', r)[0] == 200 assert call('GET', '/t/queries/q', r)[:2] == (404, 'no_value') # read, and there is none assert call('POST', '/t/inputs/add?step=1', r)[:2] == (403, 'forbidden') - assert call('PUT', '/docs/objects/k', r, b'x', {'If-None-Match': '*'})[:2] == (403, 'forbidden') + json_type = {'Content-Type': 'application/json'} + assert call('POST', '/docs/inputs/edit', r, edit_body('k', 0, 0, 0, 'x'), json_type)[:2] == (403, 'forbidden') assert call('POST', '/t/inputs/add?step=1', w)[0] == 200 - edit = struct.pack('<II', 0, 0) + b'hello' # the edit: start, remove, insert - assert call('PUT', '/docs/objects/k', w, edit, {'If-None-Match': '*'})[0] == 200 - assert call('GET', '/docs/objects/k', r)[::3] == (200, b'hello') + assert call('POST', '/docs/inputs/edit', w, edit_body('k', 0, 0, 0, 'hello'), json_type)[0] == 200 + assert call('GET', '/docs/views/body?id=k', r)[::3] == (200, b'hello') assert call('POST', '/t/inputs/add?step=2', i)[0] == 200 assert call('POST', '/t/inputs/other?step=1', i)[:2] == (403, 'forbidden') assert call('GET', '/t/views/total', i)[:2] == (403, 'forbidden') @@ -6417,14 +6453,14 @@ def server_tokens(aup, pure, work): def main(): - global EDITOR + global EXAMPLES parser = argparse.ArgumentParser() parser.add_argument('--aup', type=Path, required=True, help='the aup compiler') - parser.add_argument('--aup-root', type=Path, default=ROOT.parent / 'aup', help='its checkout, for examples/editor.aup') + parser.add_argument('--aup-root', type=Path, default=ROOT.parent / 'aup', help='its checkout, for its examples') parser.add_argument('--seeds', type=int, default=4) args = parser.parse_args() aup, pure = args.aup.resolve(), ROOT - EDITOR = args.aup_root.resolve() / 'examples' / 'editor.aup' + EXAMPLES = args.aup_root.resolve() / 'examples' report = {} with tempfile.TemporaryDirectory(prefix='pure-check-') as directory: work = Path(directory) @@ -6432,10 +6468,9 @@ def main(): report['module_assumptions'] = module_assumptions(aup, pure, work) report['module_specs'] = module_specs(aup, pure, work) report['module_tier'] = module_tier(aup, pure, work) - report['server_parity_requests'] = sum(server_parity(aup, pure, work, range(args.seeds), threads=threads) - for threads in (1, 3)) + report['server_documents_requests'] = sum(server_documents(aup, pure, work, range(args.seeds), threads=threads) + for threads in (1, 3)) report['server_pipelined_answers'] = server_pipelining(aup, pure, work) - report['server_lent_read_bytes'] = server_lent_reads(aup, pure, work) report['server_lent_budget_bytes'] = server_lent_budget(aup, pure, work) report['server_lent_views'] = [server_lent_views(aup, pure, work, threads) for threads in (1, 3)] report['server_bursts'] = server_bursts(aup, pure, work) |
