Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/release-workflow-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ name: Validate release workflow
on:
push:
branches: ['feature/**', 'master-1.12', 'master-1.12.2']
paths: ['.github/workflows/**', '.github/scripts/**']
pull_request:
branches: ['master-1.12']
paths: ['.github/workflows/**', '.github/scripts/**']
permissions:
contents: read
jobs:
Expand Down
15 changes: 13 additions & 2 deletions tools/fixture_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,24 @@ def completed_client(home, role):
if any(line==marker or line.endswith(': '+marker) for line in lines):return True
return False

def planned_disconnect(home, role, phase):
expected={'8':'A','10':'B'}
if expected.get(phase)!=role:return False
log=home/('client'+role+'.log')
if not log.is_file():return False
marker='VISIBILITY_CLIENT_PHASE '+role+' '+phase
return any(line==marker or line.endswith(': '+marker)
for line in log.read_text(encoding='utf-8',errors='replace').splitlines())

class LoginMonitor:
def __init__(self, home):
self.home=Path(home);self.seen=0;self.counts={'A':0,'B':0}
self.home=Path(home);self.seen=0;self.counts={'A':0,'B':0};self.visibility_phase=None
def poll(self):
text=(self.home/'server.log').read_text(encoding='utf-8',errors='replace')
lines=text[:text.rfind('\n')+1].splitlines()
for line in lines[self.seen:]:
phase=re.search(r'(?:^|: )VISIBILITY_PHASE ([0-9]+)$',line)
if phase:self.visibility_phase=phase[1]
joined=JOIN.fullmatch(line)
if joined and joined[1]=='A' and joined[2]=='joined':
(self.home/'sync/client-A-joined').touch()
Expand All @@ -44,7 +55,7 @@ def poll(self):
(self.home/'sync'/('login-retry-'+role)).write_text(str(self.counts[role]))
elif 'lost connection: Internal Exception:' in line:
reset=RESET.fullmatch(line)
if not reset or not completed_client(self.home,reset[1]):
if not reset or not (completed_client(self.home,reset[1]) or planned_disconnect(self.home,reset[1],self.visibility_phase)):
raise RuntimeError('Unrecognised connection failure: '+line)
self.seen=len(lines)

Expand Down
21 changes: 21 additions & 0 deletions tools/test_fixture_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,27 @@ def setUp(self):
self.root=Path(self.temp.name);(self.root/'sync').mkdir()
self.log=self.root/'server.log';self.log.write_text('')
self.monitor=LoginMonitor(self.root)
def test_planned_owner_and_observer_disconnects_do_not_abort_reconnect(self):
for phase,role in [('8','A'),('10','B')]:
with self.subTest(phase=phase,role=role):
reset=FAILURE.replace("io.netty.handler.codec.EncoderException: java.io.IOException: Can't serialize unregistered packet",'io.netty.channel.unix.Errors$NativeIoException: syscall:read(..) failed: Connection reset by peer').replace('ContainerA','Container'+role)
(self.root/('client'+role+'.log')).write_text('VISIBILITY_CLIENT_PHASE '+role+' '+phase+'\n')
self.log.write_text('VISIBILITY_PHASE '+phase+'\n'+reset+'VISIBILITY_PHASE '+str(int(phase)+1)+'\n')
LoginMonitor(self.root).poll()
self.assertFalse((self.root/('sync/login-retry-'+role)).exists())
def test_planned_disconnect_rejects_wrong_player_or_missing_client_phase(self):
reset=FAILURE.replace("io.netty.handler.codec.EncoderException: java.io.IOException: Can't serialize unregistered packet",'io.netty.channel.unix.Errors$NativeIoException: syscall:read(..) failed: Connection reset by peer')
self.log.write_text('VISIBILITY_PHASE 8\n'+reset)
with self.assertRaises(RuntimeError):LoginMonitor(self.root).poll()
(self.root/'clientA.log').write_text('VISIBILITY_CLIENT_PHASE A 8\n')
self.log.write_text('VISIBILITY_PHASE 10\n'+reset)
with self.assertRaises(RuntimeError):LoginMonitor(self.root).poll()
def test_old_disconnect_phase_cannot_explain_a_later_active_reset(self):
reset=FAILURE.replace("io.netty.handler.codec.EncoderException: java.io.IOException: Can't serialize unregistered packet",'io.netty.channel.unix.Errors$NativeIoException: syscall:read(..) failed: Connection reset by peer')
(self.root/'clientA.log').write_text('VISIBILITY_CLIENT_PHASE A 8\n')
self.log.write_text('VISIBILITY_PHASE 8\nVISIBILITY_PHASE 12\n'+reset)
with self.assertRaises(RuntimeError):LoginMonitor(self.root).poll()

def test_completed_respawn_client_may_close_its_transport(self):
reset=FAILURE.replace("io.netty.handler.codec.EncoderException: java.io.IOException: Can't serialize unregistered packet",'io.netty.channel.unix.Errors$NativeIoException: syscall:read(..) failed: Connection reset by peer')
self.log.write_text(reset)
Expand Down
Loading