Trying to make asyncio working with telnetlib - python-3.x

I'm having a hard time trying to make asyncio working with telnetlib for interrogate some hardware.
I think I clearly don't understand the way asyncio is working and I'm a completely lost in all of this. It's really unclear.
My basic version (which is synchrone) is working well but interrogating the complete list of equipments takes 6 hours actually and a large part of the equipements are not responding because they are unreachable.
Since Asyncio make us able to parallelize the connections without waiting for each timeout to trigger I would like to transform my code in proper asynchrone code, without success.
Here is what I tried :
import telnetlib
import time
import datetime
import asyncio
from env.fonctions import *
from env.variables import *
first_cmds = ['term length 0', \
'show run', \
'exit']
#create lists to iterate through
hosts = ['router-1', 'router-2', 'router-3', 'router-4', 'router-5']
async def main(hosts, user_rw_hw ,password_rw_hw, first_cmds):
class ContinueI(Exception):
pass
continue_i = ContinueI()
for host in hosts:
print(f'{host} | Trying to connect...')
try:
tn = await async_establish_telnet_connexion(user_rw_hw ,password_rw_hw, host, 23, 0.5, True)
except:
continue
print(f'{host} | Checking if equipment is not Nexus')
tn.write('show version'.encode('ascii') + b"\n")
sh_ver = await async_read_telnet_output(tn)
if 'Nexus' in sh_ver or 'NX-OS' in sh_ver or 'nexus' in sh_ver:
print(f'{host} | Equipment is Nexus, closing connection...')
tn.write('exit'.encode('ascii') + b"\n")
continue
tn.write(''.encode('ascii') + b"\n")
try:
for cmd in first_cmds:
tn.write(cmd.encode('ascii') + b"\n")
if not 'exit' in cmd:
response = await async_read_telnet_output(tn)
if '\r\n% Invalid' in response:
print(f'{host} | Commande "{cmd}" pas reconnue')
raise continue_i
else:
print(f'{host} | Commands are accepted')
except ContinueI:
tn.write(b"exit\n")
tn.write(b"exit\n")
print(f'{host} | Logout for command not recognized')
continue
if __name__ == "__main__":
try:
loop = asyncio.get_event_loop()
loop.set_debug(1)
loop.run_until_complete(main(hosts, user_rw_hw ,password_rw_hw, first_cmds))
except Exception as e:
pass
finally:
loop.close()
and functions :
async def async_read_telnet_output(tn, timeout=2, timestep=0.1):
timer = 0
data = b''
while timer <= timeout:
new_datas = tn.read_very_eager()
if len(new_datas) != 0:
timer = 0
data += new_datas
await asyncio.wait(timestep)
timer += timestep
return data.decode('utf-8')
async def async_establish_telnet_connexion(user_rw_hw, password_rw_hw, host, port=23, timeout=1, debug=False):
try:
tn = telnetlib.Telnet(host, port) # Here I don't know how to make it awaitable, if I put await before the IDE said that this method is not an awaitable, btw even if I put an awaitable like "asyncio.sleep" the behavior is still the same so it's not the only point bad
except:
if debug == True:
print(f"{host} | Telnet not responding.")
raise Exception
if debug == True:
print(f"{host} | Telnet is responding.")
response = loop.create_task(async_read_telnet_output(tn, 15))
if not 'Username:' in response and not 'login' in response:
if debug == True:
print(f"{host} | Don't see Username asked by equipment.")
raise Exception
else:
tn.write(user_rw_hw.encode('ascii') + b"\n")
if debug == True:
print(f"{host} | Username entered.")
try:
await tn.read_until(b"Password: ", timeout)
except:
if debug == True:
print(f"{host} | Don't see Password asked by equipment.")
raise Exception
finally:
tn.write(password_rw_hw.encode('ascii') + b"\n")
response = await async_read_telnet_output(tn, 10)
if '% Authentication failed' in response or 'Rejected' in response:
if debug == True:
print(f"{host} | Connection failed bad credentials.")
raise Exception
if debug == True:
print(f"{host} | Connection succeed waiting for commands.")
return tn
If some people know where I fail I would be grateful i'm stuck since one week on it... Reading some books and youtube tutos but nothing help me..
Thank you by advance !

For the ones landing here... I found
https://pypi.org/project/asynctelnet/
quick exmple:
client:
import anyio, asynctelnet
async def shell(tcp):
async with asynctelnet.TelnetClient(tcp, client=True) as stream:
while True:
# read stream until '?' mark is found
outp = await stream.receive(1024)
if not outp:
# End of File
break
elif '?' in outp:
# reply all questions with 'y'.
await stream.send('y')
# display all server output
print(outp, flush=True)
# EOF
print()
async def main():
async with await connect_tcp('localhost', 56023) as client:
await shell(client)
anyio.run(main)
Server:
import anyio, asynctelnet
async def shell(tcp):
async with asynctelnet.TelnetServer(tcp) as stream:
# this will fail if no charset has been negotiated
await stream.send('\r\nWould you like to play a game? ')
inp = await reader.receive(1)
if inp:
await stream.echo(inp)
await stream.send('\r\nThey say the only way to win '
'is to not play at all.\r\n')
async def main():
listener = await anyio.create_tcp_listener(local_port=56023)
await listener.serve(shell)
anyio.run(main)
Still this lib has some bugs. so the current state is "be prepared to code some bug workarounds if you want to use it".

Related

Why is asyncio session is closed error occurring with my Pymyq script?

I have written a script which uses the Python Pymyq library to interact with Chamberlin smart home devices, in this case a garage door. This script ran successfully for months and then stopped working. When the script is executed, it will run as desired accepting a command to open, close, or provide door status. On the second run and thereafter I am getting a Session is closed error as the response back. I have read everything I can find online regarding aiohttp and asyncio loops. I see no reason why the session or loop should be closed. Anyone know why I am getting this error? I am running the script with Python 3.9 and have tried it on Ubuntu and Windows machines with the same results.
Here is the full script I am working from:
import os
import sys
import datetime
import requests
import json
import time
import re
import asyncio
import pymyq
from aiohttp import ClientSession
import aiohttp
from pymyq import login
from pymyq.errors import MyQError, RequestError
Input = command
Input = Input.lower()
print('Input = '+Input)
#codebox = set_ip()
#creating a place holder global variable for the garage door status
door_status = ''
async def door_position() -> None:
async with aiohttp.ClientSession() as websession:
try:
# Create an API object:
api = await login(EMAIL, PASSWORD, websession)
device_id = '###############'
device = api.devices[device_id]
#global door_status
door_status = device.state
return(door_status)
except MyQError as err:
return('error')
async def door_open() -> None:
async with aiohttp.ClientSession() as websession:
try:
# Create an API object:
api = await login(EMAIL, PASSWORD, websession)
device_id = '################'
device = api.devices[device_id]
await device.open()
except MyQError as err:
return('error')
async def door_close() -> None:
async with aiohttp.ClientSession() as websession:
try:
# Create an API object:
api = await login(EMAIL, PASSWORD, websession)
device_id = '##############'
device = api.devices[device_id]
await device.close()
except MyQError as err:
return('error')
if 'garage door status' in Input:
try:
#gets the status of the myQ garage door
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
future = asyncio.ensure_future(door_position())
door_status = asyncio.get_event_loop().run_until_complete(future)
gen_msg = ('The garage door is '+door_status)
except Exception as e:
print(e)
gen_msg = ('*******API ERROR*****'+str(e))
elif 'open the garage door' in Input:
#opens the myQ garage door
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
future = asyncio.ensure_future(door_open())
asyncio.get_event_loop().run_until_complete(future)
gen_msg = ('The garage door is now opening')
except Exception as e:
print(e)
gen_msg = ('*******MYQ ERROR: DOOR NOT RESPONDING*****'+str(e))
elif 'close the garage door' in Input:
#closes the myQ garage door
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
future = asyncio.ensure_future(door_close())
asyncio.get_event_loop().run_until_complete(future)
gen_msg = ('The garage door is now closing')
except Exception as e:
print(e)
gen_msg = ('*******MYQ ERROR: DOOR NOT RESPONDING*****'+str(e))
else:
gen_msg = ("""command unknown.)""")
return(gen_msg)
I'm having the same issue, but only when it's used inside Bottle as a web app. Almost the same code works fine from the command line every time. What I have noticed is that the door state gets stuck at either closing or opening after the first task is completed. Thereafter, in the same web service session, subsequent requests fail with "Session is closed". What's interesting is that the command line reports closed or open correctly.
I think this may be a bug in pymyq. Please check https://github.com/arraylabs/pymyq/issues/143.
This was a bug, and I fixed it.

Running web.run_app() along with another async program : Python

Currently I am working on a project which involves usage of Asynchronous functions, due to the usage of certain set of libraries. My code runs fine as long as I don't integrate a web-socket server implementing functionality in my code.
But, I wish to stream the output 'Result' continuously in a websocket stream. So, I tried integrating websocket from socketio library as an AsyncServer.
Firstly, in my code, I want to gather all my inputs, and keep displaying the possible Result in a terminal. Once my inputs are finalized, I wish my result to be streamed over Websocket.
Initially, I just tried using web.run_app() in an asynchronous task in the main thread. Refer code below with #Type-1 comments. (Make sure that the lines with comment #Type-2 should be commented out). But I get the following exception "This event loop is already running".
I thought maybe if I run web.run_app() in a separate thread, then this issue might not come up. So, I changed my implementation slightly. Refer code below with #Type-2 comments. (Make sure that the lines with comment #Type-1 should be commented out). Now, I get another issue "set_wakeup_fd only works in main thread of the main interpreter".
Can someone please help me solve this issue, and let me know how must I use web.run_app()?
Here is the code:
import os, sys
import asyncio
import platform
import threading
import socketio
import json
from aioconsole import ainput
from aiohttp import web
from array import *
Result = -1
Inputs_Required = True
Input_arr = array('i')
sio = socketio.AsyncServer()
app = web.Application()
sio.attach(app)
Host = "192.168.0.7"
Port = 8050
async def IOBlock():
global Input_arr
global Inputs_Required
while(True):
response = input("Enter new input? (y/n): ")
if('y' == response or 'Y' == response):
Input = input("Enter number to be computed: ")
Input_arr.append(int(Input))
break
elif('n' == response or 'N' == response):
Inputs_Required = False
break
else:
print("Invalid response.")
async def main():
global Results
global Inputs_Required
global Input_arr
WebSocketStarted = False
#WebSocketThread = threading.Thread(target = WebStreaming, daemon = True) #Type-2
try:
while True:
if(Inputs_Required == True):
Task_AddInput = asyncio.create_task(IOBlock())
await Task_AddInput
elif (WebSocketStarted == False):
WebSocketStarted = True
#WebSocketThread.start() #Type-2
WebTask = asyncio.create_task(WebStreaming()) #Type-1
await WebTask #Type-1
if(len(Input_arr) > 0):
Task_PrintResult = asyncio.create_task(EvaluateResult())
await Task_PrintResult
except Exception as x:
print(x)
finally:
await Cleanup()
async def WebStreaming(): #Type-1
#def WebStreaming(): #Type-2
print("Starting web-socket streaming of sensor data..")
Web_loop = asyncio.new_event_loop #Type-1 or 2
asyncio.set_event_loop(Web_loop) #Type-1 or 2
web.run_app(app, host=Host, port=Port)
async def EvaluateResult():
global Input_arr
global Result
Result = 0
for i in range (0, len(Input_arr)):
Result += Input_arr[i]
print(f"The sum of inputs fed so far = {Result}.")
await asyncio.sleep(5)
async def Cleanup():
global Input_arr
global Inputs_Required
global Result
print("Terminating program....")
Result = -1
Inputs_Required = True
for i in reversed(range(len(Input_arr))):
del Input_arr[i]
#sio.event
async def connect(sid, environ):
print("connect ", sid)
#sio.event
async def OnClientMessageReceive(sid, data):
global Result
print("Client_message : ", data)
while True:
msg = json.dumps(Result)
print(msg)
await sio.send('OnServerMessageReceive', msg)
#sio.event
def disconnect(sid):
print('disconnect ', sid)
if __name__ == "__main__":
asyncio.run(main())

Python multiprocessing with async functions

I built a websocket server, a simplified version of it is shown below:
import websockets, subprocess, asyncio, json, re, os, sys
from multiprocessing import Process
def docker_command(command_words):
return subprocess.Popen(
["docker"] + command_words,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
async def check_submission(websocket:object, submission:dict):
exercise=submission["exercise"]
with docker_command(["exec", "-w", "badkan", "grade_exercise", exercise]) as proc:
for line in proc.stdout:
print("> " + line)
await websocket.send(line)
async def run(websocket, path):
submission_json = await websocket.recv() # returns a string
submission = json.loads(submission_json) # converts the string to a python dict
####
await check_submission(websocket, submission)
websocketserver = websockets.server.serve(run, '0.0.0.0', 8888, origins=None)
asyncio.get_event_loop().run_until_complete(websocketserver)
asyncio.get_event_loop().run_forever()
It works fine when there is only a single user at a time. But, when several users try to use the server, the server processes them serially so later users have to wait a long time.
I tried to convert it to a multiprocessing server by replacing the line marked with "####" ("await check_submission...") with:
p = Process(target=check_submission, args=(websocket, submission,))
p.start()
But, it did not work - I got a Runtime Warning: "coroutine: 'check_submission' was never awaited", and I did not see any output coming through the websocket.
I also tried to replace these lines with:
loop = asyncio.get_event_loop()
loop.set_default_executor(ProcessPoolExecutor())
await loop.run_in_executor(None, check_submission, websocket, submission)
but got a different error: "can't pickle asyncio.Future objects".
How can I build this multi-processing websocket server?
this is my example, asyncio.run() worked for me, use multi process start an async function
class FlowConsumer(Base):
def __init__(self):
pass
async def run(self):
self.logger("start consumer process")
while True:
# get flow from queue
flow = {}
# call flow executor get result
executor = FlowExecutor(flow)
rtn = FlowResult()
try:
rtn = await executor.run()
except Exception as e:
self.logger("flow run except:{}".format(traceback.format_exc()))
rtn.status = FLOW_EXCEPT
rtn.msg = str(e)
self.logger("consumer flow finish,result:{}".format(rtn.dict()))
time.sleep(1)
def process(self):
asyncio.run(self.run())
processes = []
consumer_proc_count = 3
# start multi consumer processes
for _ in range(consumer_proc_count):
# old version
# p = Process(target=FlowConsumer().run)
p = Process(target=FlowConsumer().process)
p.start()
processes.append(p)
for p in processes:
p.join()
The problem is that subprocess.Popen is not async, so check_submission blocks the event loop while waiting for the next line of docker output.
You don't need to use multiprocessing at all; since you are blocking while waiting on a subprocess, you just need to switch from subprocess to asyncio.subprocess:
async def docker_command(command_words):
return await asyncio.subprocess.create_subprocess_exec(
*["docker"] + command_words,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT)
async def check_submission(websocket:object, submission:dict):
exercise = submission["exercise"]
proc = await docker_command(["exec", "-w", "badkan", "grade_exercise", exercise])
async for line in proc.stdout:
print(b"> " + line)
await websocket.send(line)
await proc.wait()

RabbitMQ Pika connection reset , (-1, ConnectionResetError(104, 'Connection reset by peer'))

searched through stackoverflow and posting this question because no solution worked for me and my question might be different from other question.
I am writing a script which gets an article from rabbitMQ queue and process the article to count words and extract key words from it and dump it in db. my script is working fine but after some time of execution i get this exception
(-1, "ConnectionResetError(104, 'Connection reset by peer')")
I have no idea why am I getting this. I have tried a lot of solutions available on stackover flow none is working for me. I havr written my script and tried it in two different ways. both work fine but after some time same exception occurs.
here is my first code:
def app_main():
global channel, results, speedvars
Logger.log_message('Starting app main')
# Edit 4
def pika_connect():
connection = pika.BlockingConnection(pika.ConnectionParameters(
host=Config.AMQ_DAEMONS['base']['amq-host']))
channel = connection.channel()
print ("In pika connect")
Logger.log_message('Setting up input queue consumer')
channel.queue_declare(Config.AMQ_DAEMONS['consumer']['input'], durable=True)
channel.basic_consume(on_message, queue=Config.AMQ_DAEMONS['consumer']['input'], no_ack=True)
Logger.log_message('Starting loop')
channel.start_consuming()
#########
speedvars = SpeedVars()
speedtracker = SpeedTracker(speedvars)
speedtracker.start()
sender = ResultsSender(results, speedvars)
sender.start()
# Edit 5 starting 10 threads to listen to pika
for th in range(qthreads):
Logger.log_message('Starting thread: '+str(th))
try:
t = Thread(target=pika_connect, args=())
t.start()
except Exception as e:
Logger.error_message("Exception in starting threads " + str(e))
try:
app_main()
except Exception as e:
Logger.error_message("Exception in APP MAIN " + str(e))
here is my second code:
def app_main():
global channel, results, speedvars
Logger.log_message('Starting app main')
speedvars = SpeedVars()
speedtracker = SpeedTracker(speedvars)
speedtracker.start()
sender = ResultsSender(results, speedvars)
sender.start()
connection = pika.BlockingConnection(pika.ConnectionParameters(
host=Config.AMQ_DAEMONS['base']['amq-host']))
channel = connection.channel()
print ("In app main")
Logger.log_message('Setting up input queue consumer')
channel.queue_declare(Config.AMQ_DAEMONS['consumer']['input'], durable=True)
channel.basic_consume(on_message, queue=Config.AMQ_DAEMONS['consumer']['input'], no_ack=True)
Logger.log_message('Starting loop')
try:
channel.start_consuming()
except Exception as e:
Logger.error_message("Exception in start_consuming in main " + str(e))
raise e
try:
app_main()
except Exception as e:
Logger.error_message("Exception in APP MAIN " + str(e))
in my first code i used threading because i want to speed up the process of processing articles.
this is my call back fuction
def on_message(ch, method, properties, message):
Logger.log_message("Starting parsing new msg ")
handle_message(message)
EDIT: Full Code
import os
abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
os.chdir(dname)
from Modules import Logger
import pika
import Config
import json
import pickle
import Pipeline
import sys
import time
import datetime
import threading
import queue
import functools
from pid.decorator import pidfile
Logger.log_init(Config.AMQ_DAEMONS['consumer']['log-ident'])
#qthreads = Config.AMQ_DAEMONS['consumer']['threads']
results = queue.Queue()
channel = None
speedvars = None
SPD_RECEIVED = 'received'
SPD_DISCARDED = 'discarded'
SPD_SENT = 'sent'
class SpeedVars(object):
vars = {}
lock = None
def __init__(self):
self.lock = threading.Lock()
def inc(self, var):
self.lock.acquire()
try:
if var in self.vars:
self.vars[var] += 1
else:
self.vars[var] = 1
finally:
self.lock.release()
def dec(self, var):
self.lock.acquire()
try:
if var in self.vars:
self.vars[var] -= 1
else:
Logger.error_message('Cannot decrement ' + var + ', not tracked')
finally:
self.lock.release()
def get(self, var):
out = None
self.lock.acquire()
try:
if var in self.vars:
out = self.vars[var]
else:
Logger.error_message('Cannot get ' + var + ', not tracked')
finally:
self.lock.release()
return out
def get_all(self):
out = None
self.lock.acquire()
try:
out = self.vars.copy()
finally:
self.lock.release()
return out
class SpeedTracker(threading.Thread):
speedvars = None
start_ts = None
last_vars = {}
def __init__(self, speedvars):
super(SpeedTracker, self).__init__()
self.start_ts = time.time()
self.speedvars = speedvars
Logger.log_message('Setting up speed tracker')
def run(self):
while True:
time.sleep(Config.AMQ_DAEMONS['consumer']['speed-tracking-interval'])
prev = self.last_vars
cur = self.speedvars.get_all()
now = time.time()
if len(prev) > 0:
q = {}
for key in cur:
qty = cur[key] - prev[key]
avg = qty / Config.AMQ_DAEMONS['consumer']['speed-tracking-interval']
overall_avg = cur[key] / (now - self.start_ts)
Logger.log_message('Speed-tracking (' + key + '): total ' + str(cur[key])
+ ', delta ' + str(qty) + ', speed ' + '%0.2f' % avg + '/sec, '
+ ', overall speed ' + '%0.2f' % overall_avg + '/sec')
pending = cur[SPD_RECEIVED] - cur[SPD_DISCARDED] - cur[SPD_SENT]
pending_avg = pending / (now - self.start_ts)
Logger.log_message('Speed-tracking (pending): total ' + str(pending)
+ ', overall speed ' + '%0.2f' % pending_avg + '/sec')
self.last_vars = cur
class ResultsSender(threading.Thread):
channel = None
results = None
speedvars = None
def __init__(self, results, speedvars):
super(ResultsSender, self).__init__()
connection = pika.BlockingConnection(pika.ConnectionParameters(
host=Config.AMQ_DAEMONS['base']['amq-host']))
self.channel = connection.channel()
Logger.log_message('Setting up output exchange')
self.channel.exchange_declare(exchange=Config.AMQ_DAEMONS['consumer']['output'], exchange_type='direct')
self.results = results
self.speedvars = speedvars
def run(self):
while True:
item = self.results.get()
self.channel.basic_publish(
exchange=Config.AMQ_DAEMONS['consumer']['output'],
routing_key='',
body=item)
self.speedvars.inc(SPD_SENT)
def parse_message(message):
try:
bodytxt = message.decode('UTF-8')
body = json.loads(bodytxt)
return body
except Exception as e:
Logger.error_message("Cannot parse message - " + str(e))
raise e
def get_body_elements(body):
try:
artid = str(body.get('article_id'))
article_dt = datetime.datetime.fromtimestamp(body.get('pubTime'))
date = article_dt.strftime(Config.DATE_FORMAT)
article = "\n".join([body.get('title', ''), body.get('subheading', ''), body.get('content', '')])
return (artid, date, article)
except Exception as e:
Logger.error_message("Cannot retrieve article attributes " + str(e))
raise e
def process_article(id, date, text):
global results, speedvars
try:
Logger.log_message('Processing article ' + id)
keywords = Pipeline.extract_keywords(text)
send_data = {"id": id, "date": date, "keywords": keywords}
results.put(pickle.dumps(send_data))
# print('Queue Size:',results.qsize())
except Exception as e:
Logger.error_message("Problem processing article " + str(e))
raise e
def ack_message(ch, delivery_tag):
"""Note that `channel` must be the same pika channel instance via which
the message being ACKed was retrieved (AMQP protocol constraint).
"""
if channel.is_open:
channel.basic_ack(delivery_tag)
else:
Logger.error_message("Channel is already closed, so we can't ACK this message" + str(e))
# Channel is already closed, so we can't ACK this message;
# log and/or do something that makes sense for your app in this case.
#pass
def handle_message(connection, ch, delivery_tag, message):
global speedvars
start = time.time()
thread_id = threading.get_ident()
try:
speedvars.inc(SPD_RECEIVED)
body = parse_message(message)
(id, date, text) = get_body_elements(body)
words = len(text.split())
if words <= Config.AMQ_DAEMONS['consumer']['word-count-limit']:
process_article(id, date, text)
else:
Logger.log_message('Ignoring article, over word count limit')
speedvars.inc(SPD_DISCARDED)
except Exception as e:
Logger.error_message("Could not process message - " + str(e))
cb = functools.partial(ack_message, ch, delivery_tag)
connection.add_callback_threadsafe(cb)
Logger.log_message("Thread id: "+str(thread_id)+" Delivery tag: "+str(delivery_tag))
Logger.log_message("TOtal time taken to handle message : "+ str(time.time()-start))
# CALL BACK
## def on_message(ch, method, properties, message):
## global executor
## executor.submit(handle_message, message)
def on_message(ch, method, header_frame, message, args):
(connection, threads) = args
delivery_tag = method.delivery_tag
t = threading.Thread(target=handle_message, args=(connection, ch, delivery_tag, message))
t.start()
threads.append(t)
####################################################
#pidfile(piddir=Config.AMQ_DAEMONS['base']['pid-dir'], pidname=Config.AMQ_DAEMONS['consumer']['pid-file'])
def app_main():
global channel, results, speedvars
speedvars = SpeedVars()
speedtracker = SpeedTracker(speedvars)
speedtracker.start()
sender = ResultsSender(results, speedvars)
sender.start()
# Pika Connection
connection = pika.BlockingConnection(pika.ConnectionParameters(
host=Config.AMQ_DAEMONS['base']['amq-host']))
channel = connection.channel()
Logger.log_message('Setting up input queue consumer')
channel.queue_declare(Config.AMQ_DAEMONS['consumer']['input'], durable=True)
#channel.basic_consume(on_message, queue=Config.AMQ_DAEMONS['consumer']['input'], no_ack=True)
channel.basic_qos(prefetch_count=1)
threads = []
on_message_callback = functools.partial(on_message, args=(connection, threads))
channel.basic_consume(on_message_callback, Config.AMQ_DAEMONS['consumer']['input'])
Logger.log_message('Starting loop')
## channel.start_consuming()
try:
channel.start_consuming()
except KeyboardInterrupt:
channel.stop_consuming()
Wait for all to complete
for thread in threads:
thread.join()
connection.close()
app_main()
pika is not taking a lot of time to process message still i am facing connection reset issue.
**TOtal time taken to handle message : 0.0005991458892822266
**
Your handle_message method is blocking heartbeats because all of your code, including the Pika I/O loop, is running on the same thread. Check out this example of how to run your work (handle_message) on a separate thread from Pikas I/O loop and then acknowledge messages correctly.
NOTE: the RabbitMQ team monitors the rabbitmq-users mailing list and only sometimes answers questions on StackOverflow.
I was getting the same issue . Increasing the duration of heart-beat & connection timeouts configuration didn't work out for me. I finally figured out that, if you have
already created a channel and you are not publishing anything on it for
several minutes(20 mins in my case) ,in that case we get this error.
The Solutions which worked for me:
Create channel immediately just before publishing any message. OR
Use try-except and if you get an exception , create another channel and republish. ie.
try:
channel.basic_publish(exchange='', routing_key='abcd', body=data)
except Exception as e1:
connection=pika.BlockingConnection(pika.ConnectionParameters(host='1.128.0.3',credentials=credentials))
channel = connection.channel()
channel.basic_publish(exchange='', routing_key='abcd', body=data)
This will atleast keep the things running and prevent from losing any data. I'm not an expert in this, but hope this helps someone!
I also faced the same issue and resolved by increasing the duration for heart-beat & connection timeouts configuration.
Many thanks to #LukeBakken who has actually identified the root cause.
Here is how you can configure the timeouts:
import pika
def main():
# NOTE: These parameters work with all Pika connection types
params = pika.ConnectionParameters(heartbeat=600, blocked_connection_timeout=300)
conn = pika.BlockingConnection(params)
chan = conn.channel()
chan.basic_publish('', 'my-alphabet-queue', "abc")
# If publish causes the connection to become blocked, then this conn.close()
# would hang until the connection is unblocked, if ever. However, the
# blocked_connection_timeout connection parameter would interrupt the wait,
# resulting in ConnectionClosed exception from BlockingConnection (or the
# on_connection_closed callback call in an asynchronous adapter)
conn.close()
if __name__ == '__main__':
main()
Reference: https://pika.readthedocs.io/en/stable/examples/heartbeat_and_blocked_timeouts.html

How to ensure all commands (and errors) handled in order given

TLDR; How do I make a "single-file" asyncio.Queue() and feed it my adb commands, have them executed in the order they're received (one-by-one), handle errors that may occur (disconnect/reconnect) during one of the tasks, and continue processing the rest of the queue after handling the error?
I'm working on a module that leverages the existing python-adb module to ultimately control my android tablet as a media device and incorporate it into my home automation setup.
Problem:
My module is built entirely around async, while the python-adb module is not. The python-adb module also doesn't manage/throttle requests. And I very quickly found out that if multiple adb commands are requested too quickly the adb connection is overloaded, causing an error & requiring a reconnect whenever the disconnect occurred.
A friend of mine managed to implement a workaround/hack-y solution. Note: self._adb_lock & self._adb_error are initially set in the AndroidDevice class's __init__ function.
def adb_wrapper(func):
"""Wait if previous ADB commands haven't finished."""
#functools.wraps(func)
async def _adb_wrapper(self, *args, **kwargs):
attempts = 0
while self._adb_lock and attempts < 5:
attempts += 1
await asyncio.sleep(1)
if (attempts == 4 and self._adb_lock) or self._adb_error:
try:
await self.connect()
self._adb_error = False
except self._exceptions:
logging.error('Failed to re-establish the ADB connection; '
'will re-attempt in the next update.')
self._adb = None
self._adb_lock = False
self._adb_error = True
return
self._adb_lock = True
try:
returns = await func(self, *args, **kwargs)
except self._exceptions:
returns = None
logging.error('Failed to execute an ADB command; will attempt to '
're-establish the ADB connection in the next update')
self._adb = None
self._adb_error = True
finally:
self._adb_lock = False
return returns
return _adb_wrapper
With this workaround I placed the #adb_wrapper decorator above all functions that make adb calls. However, this is terribly inefficient & on higher-end devices doesn't prevent overloading of the adb connection.
Enter asyncio
Let me start my stating I have very little experience working with asyncio at this point; therefore, it's been touch to pick out which questions that were already posted would help me. So, my apologies if the answer is already present elsewhere. Also, in order to give people an idea of how my library is operating the codeblock will be a bit lengthy, but I only included a part of the file (a few functions to show how I'm ultimately interacting) and I tried to only include functions that connect to show the chain of commands.
My idea of a solution:
My goal is to be able to use asyncio to queue all commands and have them sent one at a time and if at any point the command fails (which would cause adb to disconnect) I want to re-establish the adb connection and continue with the queue of commands.
Current Code Structure:
class AndroidTV:
""" Represents an Android TV device. """
def __init__(self, host, adbkey=''):
""" Initialize AndroidTV object.
:param host: Host in format <address>:port.
:param adbkey: The path to the "adbkey" file
"""
self.host = host
self.adbkey = adbkey
self._adb = None
self.state = STATE_UNKNOWN
self.muted = False
self.device = 'hdmi'
self.volume = 0.
self.app_id = None
self.package_launcher = None
self.package_settings = None
self._adb_error = False
self._adb_lock = False
self._exceptions = (TypeError, ValueError, AttributeError,
InvalidCommandError, InvalidResponseError,
InvalidChecksumError, BrokenPipeError)
#adb_wrapper
async def connect(self):
""" Connect to an Android TV device.
Will attempt to establish ADB connection to the given host.
Failure sets state to UNKNOWN and disables sending actions.
"""
try:
if self.adbkey:
signer = Signer(self.adbkey)
# Connect to the device
self._adb = adb_commands.AdbCommands().ConnectDevice(serial=self.host, rsa_keys=[signer])
else:
self._adb = adb_commands.AdbCommands().ConnectDevice(serial=self.host)
if not self.package_settings:
self._adb.Shell("am start -a android.settings.SETTINGS")
await asyncio.sleep(1)
logging.info("Getting Settings App Package")
self.package_settings = await self.current_app
if not self.package_launcher:
await self.home()
await asyncio.sleep(1)
logging.info("Getting Launcher App Package")
self.package_launcher = await self.current_app
except socket_error as serr:
logging.warning("Couldn't connect to host: %s, error: %s", self.host, serr.strerror)
#adb_wrapper
async def update(self):
""" Update the device status. """
# Check if device is disconnected.
if not self._adb:
self.state = STATE_UNKNOWN
self.app_id = None
# Check if device is off.
elif not await self._screen_on:
self.state = STATE_OFF
self.app_id = None
else:
self.app_id = await self.current_app
if await self._wake_lock:
self.state = STATE_PLAYING
elif self.app_id not in (self.package_launcher, self.package_settings):
# Check if state was playing on last update
if self.state == STATE_PLAYING:
self.state = STATE_PAUSED
elif self.state != STATE_PAUSED:
self.state = STATE_IDLE
else:
# We're on either the launcher or in settings
self.state = STATE_ON
# Get information from the audio status.
audio_output = await self._dump('audio')
stream_block = re.findall(BLOCK_REGEX, audio_output,
re.DOTALL | re.MULTILINE)[0]
self.muted = re.findall(MUTED_REGEX, stream_block,
re.DOTALL | re.MULTILINE)[0] == 'true'
#property
async def current_app(self):
filtered_dump = await self._dump("window windows", "mCurrentFocus")
current_focus = filtered_dump.replace("\r", "")
matches = WINDOW_REGEX.search(current_focus)
if matches:
(pkg, activity) = matches.group('package', 'activity')
return pkg
else:
logging.warning("Couldn't get current app, reply was %s", current_focus)
return None
#property
async def _screen_on(self):
return await self._dump_has('power', 'Display Power', 'state=ON')
#property
async def _awake(self):
return await self._dump_has('power', 'mWakefulness', 'Awake')
#property
async def _wake_lock(self):
return not await self._dump_has('power', 'Locks', 'size=0')
#adb_wrapper
async def _input(self, cmd):
if not self._adb:
return
self._adb.Shell('input {0}'.format(cmd))
#adb_wrapper
async def _dump(self, service, grep=None):
if not self._adb:
return
if grep:
return self._adb.Shell('dumpsys {0} | grep "{1}"'.format(service, grep))
return self._adb.Shell('dumpsys {0}'.format(service))
async def _dump_has(self, service, grep, search):
dump_result = await self._dump(service, grep=grep)
return dump_result.strip().find(search) > -1
As I've stated before, the above method partially works, but is basically a band-aid.
The only commands that directly make adb.Shell calls are
1. async def connect(self)
2. async def update(self)
3. async def _input(self, cmd)
4. async def _dump(self, service, grep=None)
5. async def _key(self, key)
The connect & update functions result in multiple adb.Shell calls themselves, so this might be where my problem ultimately lies.
My (3-Part) Question:
1. How can I queue up all commands as they're received?
2. Execute them in the order they're received?
3. Handle errors at any point, reconnect, then continue executing the rest of the queue of commmands?
Here's my failed half-attempt at accomplishing this.
import asyncio
async def produce_output(queue, commands):
for command in commands:
#execute the adb command
if 'keypress' in command:
#command contains 'input keypress ENTER'
adb.Shell(command)
#mark the task done because there's nothing to process
queue.task_done()
else:
#command contains 'dumpsys audio'
output = adb.Shell(command)
#put result in queue
await queue.put(output)
async def process_adb(queue):
while True:
output = await queue.get()
#return output (somehow?)
queue.task_done()
async def update():
adb_queue = asyncio.Queue()
asyncio.create_task(produce_output(adb_queue,
[self._screen_on,
self.current_app,
self._wake_lock,
self._dump('audio')]))
#Not sure how to proceed
if not self._adb:
self.state = STATE_UNKNOWN
self.app_id = None
# Check if device is off.
# Fetching result of first item in the queue - self._screen_on
elif not await adb_queue.get():
self.state = STATE_OFF
self.app_id = None
else:
# Fetching result of second item in the queue - self.current_app
self.app_id = await adb_queue.get()
# Fetching result of third item in the queue - self._wake_lock
if await adb_queue.get():
self.state = STATE_PLAYING
elif self.app_id not in (self.package_launcher, self.package_settings):
# Check if state was playing on last update
if self.state == STATE_PLAYING:
self.state = STATE_PAUSED
elif self.state != STATE_PAUSED:
self.state = STATE_IDLE
else:
# We're on either the launcher or in settings
self.state = STATE_ON
# Get information from the audio status.
# Fetching result of fourth item in the queue - self._dump('audio')
audio_output = await adb_queue.get()
stream_block = re.findall(BLOCK_REGEX, audio_output,
re.DOTALL | re.MULTILINE)[0]
self.muted = re.findall(MUTED_REGEX, stream_block,
re.DOTALL | re.MULTILINE)[0] == 'true'
You need to ensure that only a single task is using the adb connection to execute a command at any given time. This means you need to either use synchronisation primitives to coordinate access, or use a queue to feed a single worker task commands to execute.
Next, because an adb connection is entirely synchronous and, as with all I/O, relatively slow, I'd use a thread pool executor to run operations on a adb connection off the asyncio loop, so that asyncio is free to run some other tasks that are not currently blocked on I/O. Otherwise, there is no point to putting .Shell() commands in a async def coroutine, you are not actually cooperating and making room for other tasks to be run.
Last but not least, if even with serialised access to the connection object you find that it can't take too many commands per time period, you would want to use some kind of rate limiting technique. I've created an asyncio leaky bucket algorithm implementation before that can take care of this, if so required.
Both a queue or a lock would ensure that commands are executed in first-come-first-serve order, but a queue would require some kind of deferred response mechanism to return command results. A queue would let you queue up related commands (you can add multiple entries using queue.put_nowait() without yielding or you can allow grouped commands), without having to wait for a lock first.
Because you want to retry connections, I'd encapsulate the connection object in a asynchronous context manager, that can then also handle locking and executing commands with an executor:
import asyncio
import collections
from concurrent.futures import ThreadPoolExecutor
from functools import partial
try: # Python 3.7
base = contextlib.AbstractAsyncContextManager
except AttributeError:
base = object # type: ignore
_retry_exceptions = (...,) # define exceptions on which to retry commands?
class asyncnullcontext(base):
def __init__(self, enter_result=None):
self.enter_result = enter_result
async def __aenter__(self):
return self.enter_result
async def __aexit__(self, *excinfo):
pass
class AsyncADBConnection(base):
def __init__(
self,
host,
adbkey=None,
rate_limit=None,
max_retry=None,
loop=None
):
self._lock = asyncio.Lock(loop=loop)
self._max_retry = max_retry
self._loop = None
self._connection = None
self._executor = ThreadPoolExecutor()
self._connect_kwargs = {
"serial": host,
"rsa_keys": [Signer(adbkey)] if adbkey else []
}
if rate_limit is not None:
# max commands per second
self._limiter = AsyncLeakyBucket(rate_limit, 1, loop=loop)
else:
self._limiter = asyncnullcontext()
async def __aenter__(self):
await self._lock.acquire()
await self._ensure_connection()
return self
async def __aexit__(self):
self._lock.release()
async def _ensure_connection(self):
if self._connection is not None:
return
loop = self._loop or asyncio.get_running_loop()
connector = partial(
adb_commands.AdbCommands().ConnectDevice,
**self._connect_kwargs
)
fut = loop.run_in_executor(pool, connector)
self._connection = await fut
async def shell(self, command):
loop = self._loop or asyncio.get_running_loop()
max_attempts = self._max_retry or 1
attempts = 0
while True:
with self._limiter:
try:
fut = loop.run_in_executor(
self._executor,
self._connection.Shell,
command
)
return await fut
except _retry_exceptions as e:
attempts += 1
if attempts >= max_attempts:
raise
# re-connect on retry
self._connection = None
await self._ensure_connection()
If you then use a queue, use Future() instances to communicate results.
Pushing a job into the queue then becomes:
fut = asyncio.Future()
await queue.put((command, fut))
result = await fut
You could wrap that into a utility function or object. The await fut line only returns once the future has received a result. For commands where you don't care about a result, you only need to await if you want to make sure that the command completed.
The consumer in the worker task that manages the connection would use:
while True:
command, fut = await self.queue.get():
async with self.connection as conn:
response = await conn.shell(command)
fut.set_result(response)
self.queue.task_done() # optional, only needed when joining the queue
where self.connection is an AsyncADBConnection instance.

Resources