mariadb-haskell-0.1.0.0: validation/cdc_fixture.py
#!/usr/bin/env python3
"""Run one CDC check on a disposable, socket-only semisynchronous MariaDB.
The managed server and its settings are untouched. The child has a deadline,
cleanup always waits for the owned server, and no network listener is exposed.
Only the temporary datadir is removed; existing server data is never opened.
"""
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import time
def main(arguments):
if not arguments:
raise ValueError('expected validation executable and arguments')
required = ('mariadbd', 'mariadb-install-db', 'mariadb')
programs = {name: shutil.which(name) for name in required}
if not all(programs.values()):
raise RuntimeError('installed MariaDB server binaries are required for the CDC fixture')
with tempfile.TemporaryDirectory(prefix='mb-cdc-', dir='/tmp') as name:
directory = Path(name)
datadir = directory/'data'
sock = directory/'server.sock'
with (directory/'server.log').open('w+') as log:
subprocess.run([programs['mariadb-install-db'], '--no-defaults', '--datadir='+str(datadir),
'--auth-root-authentication-method=normal', '--skip-test-db'],
stdout=log, stderr=subprocess.STDOUT, check=True, timeout=45)
command = [programs['mariadbd'], '--no-defaults', '--datadir='+str(datadir),
'--socket='+str(sock), '--pid-file='+str(directory/'server.pid'),
'--skip-networking', '--server-id=610129', '--log-bin='+str(directory/'bin'),
'--binlog-format=ROW', '--binlog-row-image=FULL', '--binlog-checksum=CRC32',
'--plugin-load-add=semisync_master', '--rpl-semi-sync-master-timeout=5000',
'--default-time-zone=+00:00']
if os.geteuid() == 0:
command.append('--user=root')
server = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT)
child = None
try:
def sql(statement):
return subprocess.run([programs['mariadb'], '--no-defaults', '--protocol=socket',
'--socket='+str(sock), '--user=root', '--batch', '--skip-column-names'],
input=statement, text=True, capture_output=True, timeout=8)
deadline = time.monotonic()+30
while time.monotonic() < deadline:
ready = sql('SELECT VERSION()')
if ready.returncode == 0:
break
if server.poll() is not None:
raise RuntimeError('disposable CDC server stopped before readiness')
time.sleep(0.1)
else:
raise RuntimeError('disposable CDC server readiness deadline')
setup = sql('CREATE DATABASE cdc_validation; '
'CREATE TABLE cdc_validation.changes(id INT PRIMARY KEY); '
'SET GLOBAL rpl_semi_sync_master_enabled=1;')
if setup.returncode:
raise RuntimeError('semisync fixture setup failed: '+setup.stderr)
print('Disposable CDC MariaDB:', ready.stdout.strip(), flush=True)
child = subprocess.Popen(arguments, env={**os.environ, 'MB_CDC_SOCKET': str(sock),
'MB_CDC_CONTROL': str(directory)})
child_deadline = time.monotonic()+40
while child.poll() is None:
if time.monotonic() >= child_deadline:
raise RuntimeError('CDC fixture child deadline exceeded')
request = directory/'restart.request'
if request.exists():
request.unlink()
server.terminate()
server.wait(timeout=10)
server = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT)
ready_deadline = time.monotonic()+10
while time.monotonic() < ready_deadline:
if sql('SELECT 1').returncode == 0:
break
if server.poll() is not None:
raise RuntimeError('CDC fixture failed to restart')
time.sleep(0.05)
else:
raise RuntimeError('CDC restart readiness deadline')
(directory/'restart.done').touch()
time.sleep(0.02)
return child.returncode
except BaseException:
log.flush()
log.seek(0)
print(log.read()[-4000:], file=sys.stderr)
raise
finally:
if child is not None and child.poll() is None:
child.terminate()
try:
child.wait(timeout=5)
except subprocess.TimeoutExpired:
child.kill()
child.wait(timeout=5)
server.terminate()
try:
server.wait(timeout=10)
except subprocess.TimeoutExpired:
server.kill()
server.wait(timeout=10)
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))