Tornado gives error Cannot write() after finish() - python

I am using the Tornado chat demo example from here: https://github.com/tornadoweb/tornado/tree/master/demos/chat
and just altering it very slightly.
The code change is just a small class called Connections and a bit in the MessageNewHandler(). All I am doing is just saving a reference to self and trying to write(message) to a previous client.
But when I go to save on this line conns.conns[0].write(message) I get this error message:
[E 220107 23:18:38 web:1789] Uncaught exception POST /a/message/new (::1)
HTTPServerRequest(protocol='http', host='localhost:8888', method='POST', uri='/a/message/new', version='HTTP/1.1', remote_ip='::1')
Traceback (most recent call last):
File "/home/joe/dev/tornado/lib/python3.8/site-packages/tornado/web.py", line 1702, in _execute
result = method(*self.path_args, **self.path_kwargs)
File "server.py", line 89, in post
MessageNewHandler.clients[0].write(message)
File "/home/joe/dev/tornado/lib/python3.8/site-packages/tornado/web.py", line 833, in write
raise RuntimeError("Cannot write() after finish()")
RuntimeError: Cannot write() after finish()
[E 220107 23:18:38 web:2239] 500 POST /a/message/new (::1) 5.98ms
Here is the code:
import asyncio
import tornado.escape
import tornado.ioloop
import tornado.locks
import tornado.web
import os.path
import uuid
from tornado.options import define, options, parse_command_line
define("port", default=8888, help="run on the given port", type=int)
define("debug", default=True, help="run in debug mode")
class Connections(object):
def __init__(self):
self.conns = []
def add_connection(self, conn_self):
self.conns.append(conn_self)
def conns(self):
return self.conns
conns = Connections()
class MessageBuffer(object):
def __init__(self):
# cond is notified whenever the message cache is updated
self.cond = tornado.locks.Condition()
self.cache = []
self.cache_size = 200
def get_messages_since(self, cursor):
"""Returns a list of messages newer than the given cursor.
``cursor`` should be the ``id`` of the last message received.
"""
results = []
for msg in reversed(self.cache):
if msg["id"] == cursor:
break
results.append(msg)
results.reverse()
return results
def add_message(self, message):
self.cache.append(message)
if len(self.cache) > self.cache_size:
self.cache = self.cache[-self.cache_size :]
self.cond.notify_all()
# Making this a non-singleton is left as an exercise for the reader.
global_message_buffer = MessageBuffer()
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render("index.html", messages=global_message_buffer.cache)
class MessageNewHandler(tornado.web.RequestHandler):
"""Post a new message to the chat room."""
def post(self):
message = {"id": str(uuid.uuid4()), "body": self.get_argument("body")}
# render_string() returns a byte string, which is not supported
# in json, so we must convert it to a character string.
message["html"] = tornado.escape.to_unicode(
self.render_string("message.html", message=message)
)
conns.add_connection(self)
if (len(conns.conns)>2):
conns.conns[0].write(message)
self.finish()
class MessageUpdatesHandler(tornado.web.RequestHandler):
"""Long-polling request for new messages.
Waits until new messages are available before returning anything.
"""
async def post(self):
cursor = self.get_argument("cursor", None)
messages = global_message_buffer.get_messages_since(cursor)
while not messages:
# Save the Future returned here so we can cancel it in
# on_connection_close.
self.wait_future = global_message_buffer.cond.wait()
try:
await self.wait_future
except asyncio.CancelledError:
return
messages = global_message_buffer.get_messages_since(cursor)
if self.request.connection.stream.closed():
return
self.write(dict(messages=messages))
def on_connection_close(self):
self.wait_future.cancel()
def main():
parse_command_line()
app = tornado.web.Application(
[
(r"/", MainHandler),
(r"/a/message/new", MessageNewHandler),
(r"/a/message/updates", MessageUpdatesHandler),
],
cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
template_path=os.path.join(os.path.dirname(__file__), "templates"),
static_path=os.path.join(os.path.dirname(__file__), "static"),
xsrf_cookies=True,
debug=options.debug,
)
app.listen(options.port)
tornado.ioloop.IOLoop.current().start()
if __name__ == "__main__":
main()

You're writing to an already closed connection, that's why you're seeing the error.
If you want to write to a previously connected client, you've keep that connection open.
However, this - conns.add_connection(self) - doesn't make sense to track regular http connections.
You should consider using websockets if you want to keep previous connections open and track them.
Update: Here's how you can keep a connection open. If I understand correctly, you want to send a message from current client to the previous client.
1. Using tornado.locks.Condition():
import tornado.locks
class MessageNewHandler(tornado.web.RequestHandler):
"""Post a new message to the chat room."""
clients = []
condition = tornado.locks.Condition()
async def post(self):
message = {"id": str(uuid.uuid4()), "body": self.get_argument("body")}
# render_string() returns a byte string, which is not supported
# in json, so we must convert it to a character string.
message["html"] = tornado.escape.to_unicode(
self.render_string("message.html", message=message)
)
MessageNewHandler.clients.append(self)
if len(MessageNewHandler.clients) < 2:
# less than 2 clients
# wait until notified
await MessageNewHandler.condition.wait()
else:
# at least 2 clients
# write to previous client's response
MessageNewHandler.clients[0].finish(message)
# notify the first waiting client
# so it can send the response
MessageNewHandler.condition.notify()
# Note: since you've finished previous client's response
# you should also remove it from clients list
# since you can't use that connection again
2. Using tornado.concurrent.Future():
import tornado.concurrent
class MessageNewHandler(tornado.web.RequestHandler):
"""Post a new message to the chat room."""
waiters = []
async def post(self):
message = {"id": str(uuid.uuid4()), "body": self.get_argument("body")}
# render_string() returns a byte string, which is not supported
# in json, so we must convert it to a character string.
message["html"] = tornado.escape.to_unicode(
self.render_string("message.html", message=message)
)
future = tornado.concurrent.Future() # create a future
# instead of a saving the reference to the client,
# save the future
MessageNewHandler.waiters.append(future)
if len(MessageNewHandler.waiters) < 2:
# less than 2 clients
# wait for next client's message
msg_from_next_client = await future
# the future will resolve when the next client
# sets a result on it
# then python will execute the following code
self.finish(msg_from_next_client)
# Note: since you've finished this connection
# you should remove this future from the waiters list
# since you can't reuse this connection again
else:
# at least 2 clients
# set the current client's message
# as a result on previous client's future
previous_client_future = MessageNewHandler.waiters[0]
if not previous_client_future.done():
# only set a result if you haven't set it already
# otherwise you'll get an error
previous_client_future.set_result(message)
3: A more practical example using tornado.concurrent.Future():
import tornado.concurrent
class Queue:
"""We'll keep the future related code in this class.
This will allow us to present a cleaner, more intuitive usage api.
"""
waiters = []
#classmethod
def get_message_from_next_client(cls):
future = tornado.concurrent.Future()
cls.waiters.append(future)
return future
#classmethod
def send_message_to_prev_client(cls, message):
previous_client_future = cls.waiters[0]
if not previous_client_future.done():
previous_client_future.set_result(message)
class MessageNewHandler(tornado.web.RequestHandler):
"""Post a new message to the chat room."""
async def post(self):
message = {"id": str(uuid.uuid4()), "body": self.get_argument("body")}
message["html"] = tornado.escape.to_unicode(
self.render_string("message.html", message=message)
)
if len(Queue.waiters) < 2:
msg_from_next_client = await Queue.get_message_from_next_client()
self.finish(msg_from_next_client)
else:
Queue.send_message_to_prev_client(message)

I had a look at the RequestHandler code on https://github.com/tornadoweb/tornado/blob/master/tornado/web.py
I got rid of the Connection class and changed the MessageNewHandler to this..
class MessageNewHandler(tornado.web.RequestHandler):
"""Post a new message to the chat room."""
clients =[]
def post(self):
self._auto_finish =False
message = {"id": str(uuid.uuid4()), "body": self.get_argument("body")}
# render_string() returns a byte string, which is not supported
# in json, so we must convert it to a character string.
message["html"] = tornado.escape.to_unicode(
self.render_string("message.html", message=message)
)
MessageNewHandler.clients.append(self)
if len(MessageNewHandler.clients)>1:
MessageNewHandler.clients[0].finish(message)
So the 2 key things that made it work were self._auto_finish =False
and MessageNewHandler.clients[0].finish(message)

Related

Why isn't the value updated when publishing to a Redis channel?

I'm trying to use Redis in my FastAPI -application and struggling to update a value when publishing to a channel.
This is my Redis client setup:
import logging
import redis as _redis
logger = logging.getLogger(__name__)
REDIS_URL = "redis://redis:6379"
class RedisClient:
__instance = None
client: _redis.Redis
def __new__(cls) -> "RedisClient":
if cls.__instance is None:
cls.__instance = object.__new__(cls)
try:
logger.info("Connecting to Redis")
client = _redis.Redis.from_url(REDIS_URL)
cls.client = client
except Exception:
logger.error("Unable to connect to Redis")
logger.info("Connected to Redis")
return cls.__instance
def disconnect(self):
logger.info("Closing Redis connection")
self.client.connection_pool.disconnect()
redis = RedisClient().client
redis_pubsub = redis.pubsub()
This is my POST route what I'm using to test updating the value:
#router.post("/", response_model=Any)
async def post(tracing_no: int) -> Any:
data = {"tracing_no": tracing_no, "result": 200, "client_id": "10"}
data = json.dumps(data)
result = redis.publish(TestChannel.TestTopic.value, data)
return redis.get(TestChannel.TestTopic.value)
This code returns always "{\"tracing_no\": 5, \"result\": 200, \"client_id\": \"10\"}", no matter what number I give as tracking_no when I send the request. I think 5 was the first number I used when testing and that is in my dump.rdb -file.
Any ideas why my tracking_no isn't updating?

return self.browse((self._ids[key],)) IndexError: tuple index out of range : When sending message from Discuss in Odoo V13

Below error occur, whenever sending a message on discuss module, which we configured to send notifications to users firebase_id.
File "/odoo/odoo-server/addons/mail/models/mail_channel.py", line 368, in message_post
message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(message_type=message_type, moderation_status=moderation_status, **kwargs)**
File "/odoo/custom/addons/elite_event_management_api/controllers/message.py", line 34, in message_post
registration_id = channel_partner_id.partner_id.user_ids[0].firebase_id
File "/odoo/odoo-server/odoo/models.py", line 5624, in _getitem_
return self.browse((self._ids[key],))
IndexError: tuple index out of range
HERE IS THE CODE -All users are unable to send messages on discuss Module.
import logging
from odoo import models, api
from odoo.exceptions import AccessDenied, UserError
logger = logging.getLogger(__name_)
class MailThred(models.AbstractModel):
_inherit = "mail.thread"
#api.returns('mail.message', lambda value: value.id)
def message_post(self, *,
body='', subject=None, message_type='notification',
email_from=None, author_id=None, parent_id=False,
subtype_id=False, subtype=None, partner_ids=None, channel_ids=None,
attachments=None, attachment_ids=None,
add_sign=True, record_name=False,
**kwargs):
res = super(MailThred, self).message_post(body=body, subject=subject, message_type=message_type,
email_from=email_from, author_id=author_id, parent_id=parent_id,
subtype_id=subtype_id, subtype=subtype, partner_ids=partner_ids, channel_ids=channel_ids,
attachments=attachments, attachment_ids=attachment_ids,
add_sign=add_sign, record_name=record_name,
**kwargs)
message_subtype_id = self.env['mail.message.subtype'].sudo().search([('name', 'ilike', 'Discussions')])
if res.message_type == 'comment' and res.subtype_id.id == message_subtype_id.id:
for each in res.channel_ids:
for channel_partner_id in each.channel_last_seen_partner_ids:
if channel_partner_id.partner_id.id != res.author_id.id:
from . import FCMManager as fcm
registration_id = channel_partner_id.partner_id.user_ids[0].firebase_id
if registration_id:
try:
tokens = [str(registration_id)]
message_title = "ControlE#ERP - CHAT"
message_body = res.body
fcm.sendPush(message_title, message_body, tokens)
_logger.info('ControlE#ERP Alert- NEW CHAT MESSAGE SENT')
except Exception as e:
_logger.info('not sent')
return res
You have no users in channel_partner_id.partner_id.user_ids, so you are trying to get the 0th element from empty list ,
So check your code and try again.

Web wocket server python with socket module

Problem:
I am trying to make a backend web framework as a challenge and I'm trying to implement web
sockets but whenever I make the handshake my browser says that it received no response and the status is 'finished'.
Things I have tried:
I have tried making the request to my own server using the requests module and same Sec Websocket Key as on another website and the key was the same as that other websites response.
I have also tried running it on a node.js server with the same code for the client and the whole thing worked as expected so it's a serverside problem.
Changing browsers and updating browser versions.
Changing from localhost to my private IP and my local machine name.
code
# Ignore missing stuff, some stuff is missing to reduce the length and shouldn't be needed to debug
#app.route('/')
def ws(request: dict):
if request.get('Connection') == 'Upgrade' and request.get('Upgrade') == 'websocket':
# Get returns None if key doesn't exist [] raises keyerror
print(f"Sec-Websocket-Key: {request['Sec-WebSocket-Key']}")
# Prints correct key (:
request['Sec-WebSocket-Key'] += '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
SecWebSocketAccept = hashlib.sha1(request['Sec-WebSocket-Key'].encode())
SecWebSocketAccept = SecWebSocketAccept.digest()
SecWebSocketAccept = base64.b64encode(SecWebSocketAccept).decode()
return HttpResponse(status_code=101,
message='Switching Protocols',
headers= {
'Upgrade': 'websocket',
'Connection': 'Upgrade',
'Sec-WebSocket-Accept': SecWebSocketAccept,
'Sec-WebSocket-Protocol': 'chat'
}, AutoGenerateExtraHeaders=False)
else:
return RenderTemplate('example.html', minimize=False)
# warning: remove minimize only for testing
# HttpResponse class
class HttpResponse:
def __init__(
self,
data: str = '',
status_code: int = 200,
message: str = 'OK',
headers: dict = {},
AutoGenerateExtraHeaders=True
)-> None:
""" todo: add docs on class and methods"""
self.protocol = 'HTTP/1.1'
self.status = f'{status_code} {message}'
self.data = data
self.headers = headers
if AutoGenerateExtraHeaders:
contentmd5 = hashlib.md5(data.encode()).digest()
self.headers['Content-MD5'] = base64.b64encode(contentmd5)
self.headers['Content-Length'] = len(data.encode())
# todo: add more headers such as date,
self.headers = str(headers)[2:].replace('{', '').replace('}', '').replace(', \'', '\r\n\'').replace('\'', '')
# String version of headers
def encode(self) -> bytes:
return str(self).encode()
# Handle Connection Code
def handleConnection(self, connection: socket.socket, address) -> None:
"""handles incoming requests
Args:
connection (socket.socket): client socket connection
address: address of incoming connection
"""
request = connection.recv(1024).decode()
# print(request)
if not request: return
request: dict = self.dictFromRequest(request)
route = self.app.getRoute(request['route']) # returns route object which is made up of a function and a URL defined by app.route(route: str) decor
method: str = request['method']
protocol: str = request['protocol']
# 404 Page not found
# Call event
event = self.app.callEvent(RequestEvent(request))
if event.timeOut: return
# Handle
if protocol == 'HTTP':
# todo: cors and csrf
if method == 'GET':
response = route.call(request)
print(f'encoded: {response.encode()}, \n\ndecoded: {response}')
# ouupt:
# encoded: b'HTTP/1.1 101 Switching Protocols\r\nUpgrade:
# websocket\r\nConnection:
# Upgrade\r\nSec-WebSocket-Accept: TwmkkaET4SyBJad/5OzZNHxaZ/o=\r\nSec
# -WebSocket-Protocol:
# chat\r\n\r\n',
#
# decoded: HTTP/1.1 101 Switching Protocols
# Upgrade: websocket
# Connection: Upgrade
# Sec-WebSocket-Accept: TwmkkaET4SyBJad/5OzZNHxaZ/o=
# Sec-WebSocket-Protocol: chat
#
#
connection.send(response.encode())
return connection.close()
def dictFromRequest(self, headers: str) -> dict:
# todo: just lol
dict = {}
dict['method'] = headers.split('\r\n\r\n')[0].split('\r\n')[0].split(' ')[0]
dict['protocol'] = headers.split('\r\n')[0].split(' ')[-1].split('/')[0]
dict['route'] = headers.split('\r\n\r\n')[0].split('\r\n')[0].split(' ')[1].split('?')[0]
dict['query'] = ''.join(headers.split('\n\n')[0].split('\r\n')[0].split(' ')[1].split('?')[1:])
dict['data'] = headers.split('\r\n\r\n')[1]
for header in headers.split('\r\n\r\n')[0].split('\r\n')[1:]:
dict[header.split(': ')[0]] = header.split(': ')[1]
return dict
P.S. pls don't judge I'm only 13 lol

iot edge direct method handler in python

I have created a module for a Bacnet scan and it will respond with a list of devices and its address as a result. But I'm having trouble implementing a direct method handler in python. When i first tried implementing it myself i got this error. Which could mean I didn't successfully register the direct method callback. I have some references but it was from C# and azure docs is not helping me figure out the right method to register the callback. for IoTHubModuleClient there's a on_method_request_received and a receive_method_request. appreciate any help!
def iothub_client_scan_run():
try:
iot_client = iothub_client_init()
bacnet_scan_listener_thread = threading.Thread(target=device_method_listener, args=(iot_client,))
bacnet_scan_listener_thread.daemon = True
bacnet_scan_listener_thread.start()
while True:
time.sleep(1000)
def device_method_listener(iot_client):
while True:
# Receive the direct method request
method_request = iot_client.receive_method_request()
print (
"\nMethod callback called with:\nmethodName = {method_name}\npayload = {payload}".format(
method_name=method_request.name,
payload=method_request.payload
)
)
if method_request.name == "runBacnetScan":
response = bacnet_scan_device(method_request)
else:
response_payload = {"Response": "Direct method {} not defined".format(method_request.name)}
response_status = 404
# Send a method response indicating the method request was resolved
print('Sending method response')
iot_client.send_method_response(response)
print('Message sent!')
Edit:
Here is my route config
I was able to resolve my issue or at least find the root cause and it was my network configuration under the createOptions. It seems like there's an issue when I'm trying to do NetworkMode: host and connects to the IotModuleClient.connect_from_edge_environment via connect with connection string. I'm still trying to tweak the connection configuration but at least i know its not on the code.
async def method_request_handler(module_client):
while True:
method_request = await module_client.receive_method_request()
print (
"\nMethod callback called with:\nmethodName = {method_name}\npayload = {payload}".format(
method_name=method_request.name,
payload=method_request.payload
)
)
if method_request.name == "method1":
payload = {"result": True, "data": "some data"} # set response payload
status = 200 # set return status code
print("executed method1")
elif method_request.name == "method2":
payload = {"result": True, "data": 1234} # set response payload
status = 200 # set return status code
print("executed method2")
else:
payload = {"result": False, "data": "unknown method"} # set response payload
status = 400 # set return status code
print("executed unknown method: " + method_request.name)
# Send the response
method_response = MethodResponse.create_from_method_request(method_request, status, payload)
await module_client.send_method_response(method_response)
print('Message sent!')
def stdin_listener():
while True:
try:
selection = input("Press Q to quit\n")
if selection == "Q" or selection == "q":
print("Quitting...")
break
except:
time.sleep(10)
# Schedule task for C2D Listener
listeners = asyncio.gather(input1_listener(module_client), twin_patch_listener(module_client), method_request_handler(module_client))

Tornado testing async requests

I need an advice regards testing tornado app. For now I just playing with demo chat application, but it looks like real-life problem.
In the handler I have:
class MessageUpdatesHandler(BaseHandler):
#tornado.web.authenticated
#tornado.web.asynchronous
def post(self):
cursor = self.get_argument("cursor", None)
global_message_buffer.wait_for_messages(self.on_new_messages,
cursor=cursor)
def on_new_messages(self, messages):
# Closed client connection
if self.request.connection.stream.closed():
return
self.finish(dict(messages=messages))
class MessageBuffer(object):
def __init__(self):
....
def wait_for_messages(self, callback, cursor=None):
if cursor:
new_count = 0
for msg in reversed(self.cache):
if msg["id"] == cursor:
break
new_count += 1
if new_count:
callback(self.cache[-new_count:])
return
self.waiters.add(callback)
def cancel_wait(self, callback):
.....
def new_messages(self, messages):
logging.info("Sending new message to %r listeners", len(self.waiters))
for callback in self.waiters:
try:
callback(messages)
except:
logging.error("Error in waiter callback", exc_info=True)
self.waiters = set()
self.cache.extend(messages)
if len(self.cache) > self.cache_size:
self.cache = self.cache[-self.cache_size:]
As I metioned full source code is in torndado demos
In my test I have:
#wsgi_safe
class MessageUpdatesHandlerTest(LoginedUserHanldersTest):
Handler = MessageUpdatesHandler
def test_add_message(self):
from chatdemo import global_message_buffer
kwargs = dict(
method="POST",
body='',
)
future = self.http_client.fetch(self.get_url('/'), callback=self.stop, **kwargs)
message = {
"id": '123',
"from": "first_name",
"body": "hello",
"html": "html"
}
global_message_buffer.new_messages([message])
response = self.wait()
self.assertEqual(response.code, 200)
self.mox.VerifyAll()
What happens:
It creates a future object
It sends a hello message, in this moment no waiter is registered
in MessageBuffer so callback is not called
In wait starts IoLoop and makes, a post fetch and waiter becomes
registered in MessageBuffer
Callback is never called and my response remains empty, so
everything fails with
AssertionError: Async operation timed out
after 5 seconds
What I want it to do:
On post register itself as a waiter
Receive some messages
Return to me a 200 response
Thank you for your help

Categories