stop/start client connections with loseConnection in ReconnectingClientFactory
"Chris Satterthwaite" <[email protected]> Fri, 22 Mar 2019 12:08:19 -0500
| Newsgroups | gmane.comp.python.twisted |
|---|---|
| Message-ID | <[email protected]> |
Hello community,
First of all - thanks for an awesome platform! I'm brand new to this
community, but have been using Twisted a couple years.
Reason for posting:
I've hit a condition with ReconnectingClientFactory that I'm not sure is per
design. I have a work around right now, but need your perspective. Seems
like there should be a better/right way to do this.
Attempted design:
I'd like to have long running TCP clients (forever until stopped), with a
long running TCP server. When a long running client hits a problem with a
dependency (database is down, kafka bus unavailable, external API not
responding, etc), I want the client to go offline for a while and then come
back online. an automated, self-recovery type action. Since it's not ok to
start/stop/restart the Twisted Reactor, I am letting the client finish
whatever it can do, disconnect from the service, destruct the dependencies,
wait for a period of time, and then attempt a clean re-initialization of
those dependencies along with reconnecting to the Twisted Server.
Problem case:
I'm using the ReconnectingClientFactory in my client. When the client hits
a problem, it calls transport.loseConnection(). But whenever the client
calls this, after the disconnect - it does not reconnect; stopFactory is
called and everything exits.
Work around:
I noticed some Twisted source code that works off factory.numPorts. If
numPorts is 1 and the client loses the connection, it goes to 0 and calls
the cleanup. So I conditionally increase this number right before
intentionally disconnecting, and then reset that after reconnecting. This
solves the problem, but it's a hack.
I'll attach the test scripts to this post (if attachments are allowed), but
the main code is with these functions in the factory:
def clientConnectionLost(self, connector, reason):
print(' factory clientConnectionLost:
reason: {}'.format(reason))
# if self.disconnectedOnPurpose:
# ## Hack to keep reactor alive
# print(' factory
clientConnectionLost: increasing numPorts')
# self.numPorts += 1
# self.numPortsChanged = True
# self.disconnectedOnPurpose =
False
print(' ... simulate client going idle
before attempting restart...')
time.sleep(5)
ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
print(' factory clientConnectionLost:
end.\n')
def clientConnectionMade(self):
print(' factory clientConnectionMade:
starting numPorts: {}'.format(self.numPorts))
# if self.numPortsChanged :
# ## Resetting from hacked value
# print(' factory
clientConnectionMade: decreasing numPorts')
# self.numPorts -= 1
# self.numPortsChanged = False
print(' factory clientConnectionMade:
finished numPorts: {}'.format(self.numPorts))
def cleanup(self):
print('factory cleanup: calling
loseConnection')
if self.connectedClient is not None:
self.connectedClient.transport.loseConnection()
self.disconnectedOnPurpose =
True
With the above lines commented out, once the cleanup call does
transport.loseConnection(), the factory stops at the end of
clientConnectionLost.
Sample scripts/logs:
I've tried to create short test scripts and corresponding logs (with the
client failing, and then with it restarting when I use the workaround).
I've cut out several thousand lines to get down to something simple for the
example test scripts, but I know the client is still a little long. Again,
I'm not sure if attachments work on the mailing list, but I'll attempt to
attach the client/server scripts with the corresponding pass/fail logs.
Thanks!
-Chris
_______________________________________________
Twisted-Python mailing list
[email protected]
https://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-python
testClient.py
(text/plain, 4.4 KB)
import os, sys, traceback
import json, time, datetime, psutil
from twisted.internet.protocol import ReconnectingClientFactory
from twisted.protocols.basic import LineReceiver
from twisted.internet import reactor, task, defer, threads
from contextlib import suppress
class CustomLineReceiverProtocol(LineReceiver):
delimiter = b':==:'
class ServiceClientProtocol(CustomLineReceiverProtocol):
def connectionMade(self):
print(' protocol connectionMade')
self.factory.connectedClient = self
self.factory.clientConnectionMade()
def lineReceived(self, line):
dataDict = json.loads(line)
if dataDict.get('action') == 'healthRequest':
self.factory.enterSimulateJob()
def connectionLost(self, reason):
print(' protocol connectionLost')
self.factory.connectedClient = None
def constructAndSendData(self, action, content):
message = {}
message['action'] = action
message['content'] = content
jsonMessage = json.dumps(message)
msg = jsonMessage.encode('utf-8')
print(' protocol constructAndSendData: {}'.format(msg))
self.sendLine(msg)
class ServiceClientFactory(ReconnectingClientFactory):
continueTrying = True
def __init__(self):
print('factory constructor')
self.connectedClient = None
self.health = {}
self.loopingSystemHealth = task.LoopingCall(self.enterSystemHealthCheck)
self.loopingSystemHealth.start(10)
self.numPortsChanged = False
self.disconnectedOnPurpose = False
super().__init__()
def buildProtocol(self, addr):
print(' factory buildProtocol')
self.resetDelay()
protocol = ServiceClientProtocol()
protocol.factory = self
return protocol
def clientConnectionLost(self, connector, reason):
print(' factory clientConnectionLost: reason: {}'.format(reason))
# if self.disconnectedOnPurpose:
# ## Hack to keep reactor alive
# print(' factory clientConnectionLost: increasing numPorts')
# self.numPorts += 1
# self.numPortsChanged = True
# self.disconnectedOnPurpose = False
print(' ... simulate client going idle before attempting restart...')
time.sleep(5)
ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
print(' factory clientConnectionLost: end.\n')
def clientConnectionMade(self):
print(' factory clientConnectionMade: starting numPorts: {}'.format(self.numPorts))
# if self.numPortsChanged :
# ## Resetting from hacked value
# print(' factory clientConnectionMade: decreasing numPorts')
# self.numPorts -= 1
# self.numPortsChanged = False
print(' factory clientConnectionMade: finished numPorts: {}'.format(self.numPorts))
print(' ..... pausing for <CTRL><C> test')
time.sleep(3)
def cleanup(self):
print('factory cleanup: calling loseConnection')
if self.connectedClient is not None:
self.connectedClient.transport.loseConnection()
self.disconnectedOnPurpose = True
def stopFactory(self):
print('stopFactory')
self.stopTrying()
with suppress(Exception):
self.loopingSystemHealth.stop()
print('stopFactory end.')
def enterSimulateJob(self):
print(' factory enterSimulateJob')
threadHandle = threads.deferToThread(self.simulateJob)
return threadHandle
def simulateJob(self):
print(' factory simulateJob: starting job')
time.sleep(2)
self.connectedClient.constructAndSendData('jobResponse', self.health)
print(' factory simulateJob: finished job... time to reset the client (diconnect/re-initialize)...')
self.cleanup()
def enterSystemHealthCheck(self):
print(' factory enterSystemHealthCheck')
threadHandle = threads.deferToThread(self.getSystemHealth)
return threadHandle
def getSystemHealth(self):
print(' factory getSystemHealth')
try:
currentTime = time.time()
process = psutil.Process(os.getpid())
startTime = process.create_time()
self.health = {
'processCpuPercent': process.cpu_percent(),
'processMemory': process.memory_full_info().uss,
'processRunTime': int(currentTime-startTime)
}
print(' factory getSystemHealth: system health: {}'.format(self.health))
except:
exception = traceback.format_exception(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2])
print(' factory getSystemHealth: exception: {}'.format(exception))
if __name__ == '__main__':
try:
connector = reactor.connectTCP('127.0.0.1', 51841, ServiceClientFactory(), timeout=300)
reactor.run()
except:
stacktrace = traceback.format_exception(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2])
print('clientWrapper exception: {}'.format(stacktrace))
print('exiting')
sys.exit(0)
testServer.py
(text/plain, 2 KB)
import sys, traceback
import json
from twisted.internet import reactor, task, defer, threads
from twisted.internet.protocol import ServerFactory
from twisted.protocols.basic import LineReceiver
class CustomLineReceiverProtocol(LineReceiver):
delimiter = b':==:'
class ServiceListener(CustomLineReceiverProtocol):
def connectionMade(self):
print(' protocol connectionMade')
self.factory.activeClients.append(self)
def connectionLost(self, reason):
print(' protocol connectionLost')
self.factory.removeClient(self)
def lineReceived(self, line):
print(' protocol lineReceived: {}'.format(line))
def constructAndSendData(self, action):
message = {'action': action}
jsonMessage = json.dumps(message)
msg = jsonMessage.encode('utf-8')
print(' protocol constructAndSendData: {}'.format(msg))
self.sendLine(msg)
class ServiceFactory(ServerFactory):
protocol = ServiceListener
def __init__(self):
print('factory constructor')
super().__init__()
self.activeClients = []
self.loopingHealthUpdates = task.LoopingCall(self.enterSystemHealthCheck)
self.loopingHealthUpdates.start(15)
def removeClient(self, client):
print(' factory removeClient')
self.activeClients.remove(client)
def enterSystemHealthCheck(self):
print(' factory enterSystemHealthCheck')
threadHandle = threads.deferToThread(self.sendHealthRequest)
return threadHandle
def sendHealthRequest(self):
if len(self.activeClients) <= 0:
print(' factory sendHealthRequest: no active clients to talk to')
else:
for client in self.activeClients:
print(' factory sendHealthRequest: requesting from client...')
client.constructAndSendData('healthRequest')
if __name__ == '__main__':
try:
reactor.listenTCP(51841, ServiceFactory(), interface='127.0.0.1')
reactor.run()
except:
stacktrace = traceback.format_exception(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2])
print('clientWrapper exception: {}'.format(stacktrace))
print('exiting')
sys.exit(0)
client_not_working.log
(application/octet-stream, 1.3 KB)
> python .\testClient.py
factory constructor
factory enterSystemHealthCheck
factory getSystemHealth
factory getSystemHealth: system health: {'processCpuPercent': 0.0, 'processMemory': 24510464, 'processRunTime': 1}
factory buildProtocol
protocol connectionMade
factory clientConnectionMade: starting numPorts: 1
factory clientConnectionMade: finished numPorts: 1
..... pausing for <CTRL><C> test
factory enterSimulateJob
factory simulateJob: starting job
factory enterSystemHealthCheck
factory getSystemHealth
factory getSystemHealth: system health: {'processCpuPercent': 0.0, 'processMemory': 24616960, 'processRunTime': 11}
protocol constructAndSendData: b'{"action": "jobResponse", "content": {"processCpuPercent": 0.0, "processMemory": 24616960, "processRunTime": 11}}'
factory simulateJob: finished job... time to reset the client (diconnect/re-initialize)...
factory cleanup: calling loseConnection
protocol connectionLost
factory clientConnectionLost: reason: [Failure instance: Traceback (failure with no frames): <class 'twisted.internet.error.ConnectionDone'>: Connection was closed cleanly.
]
... simulate client going idle before attempting restart...
factory clientConnectionLost: end.
stopFactory
stopFactory end.
exiting
client_working_with_hack.log
(application/octet-stream, 2.1 KB)
> python .\testClient.py
factory constructor
factory enterSystemHealthCheck
factory getSystemHealth
factory getSystemHealth: system health: {'processCpuPercent': 0.0, 'processMemory': 24518656, 'processRunTime': 0}
factory buildProtocol
protocol connectionMade
factory clientConnectionMade: starting numPorts: 1
factory clientConnectionMade: finished numPorts: 1
..... pausing for <CTRL><C> test
factory enterSystemHealthCheck
factory getSystemHealth
factory getSystemHealth: system health: {'processCpuPercent': 0.0, 'processMemory': 24604672, 'processRunTime': 10}
factory enterSimulateJob
factory simulateJob: starting job
protocol constructAndSendData: b'{"action": "jobResponse", "content": {"processCpuPercent": 0.0, "processMemory": 24604672, "processRunTime": 10}}'
factory simulateJob: finished job... time to reset the client (diconnect/re-initialize)...
factory cleanup: calling loseConnection
protocol connectionLost
factory clientConnectionLost: reason: [Failure instance: Traceback (failure with no frames): <class 'twisted.internet.error.ConnectionDone'>: Connection was closed cleanly.
]
factory clientConnectionLost: increasing numPorts
... simulate client going idle before attempting restart...
factory clientConnectionLost: end.
factory enterSystemHealthCheck
factory getSystemHealth
factory getSystemHealth: system health: {'processCpuPercent': 0.0, 'processMemory': 24604672, 'processRunTime': 20}
factory buildProtocol
protocol connectionMade
factory clientConnectionMade: starting numPorts: 2
factory clientConnectionMade: decreasing numPorts
factory clientConnectionMade: finished numPorts: 1
..... pausing for <CTRL><C> test
[note: hit <CTRL><C> here]
protocol connectionLost
factory clientConnectionLost: reason: [Failure instance: Traceback (failure with no frames): <class 'twisted.internet.error.ConnectionLost'>: Connection to the other side was lost in a non-clean fashion: Connection lost.
]
... simulate client going idle before attempting restart...
factory clientConnectionLost: end.
stopFactory
stopFactory end.
exiting
server.log
(application/octet-stream, 682 B)
> python .\testServer.py
factory constructor
factory enterSystemHealthCheck
factory sendHealthRequest: no active clients to talk to
protocol connectionMade
factory enterSystemHealthCheck
factory sendHealthRequest: requesting from client...
protocol constructAndSendData: b'{"action": "healthRequest"}'
protocol lineReceived: b'{"action": "jobResponse", "content": {"processCpuPercent": 0.0, "processMemory": 24604672, "processRunTime": 10}}'
protocol connectionLost
factory removeClient
protocol connectionMade
protocol connectionLost
factory removeClient
factory enterSystemHealthCheck
factory sendHealthRequest: no active clients to talk to