def update_poll_messages(session, bot, poll): """Logic for handling updates.""" now = datetime.now() # Check whether we have a new window current_update = session.query(Update) \ .filter(Update.poll == poll) \ .one_or_none() # Don't handle it in here, it's already handled in the job if current_update is not None: return try: # Try to send updates send_updates(session, bot, poll) except RetryAfter as e: # Schedule an update after the RetryAfter timeout + 1 second buffer try: update = Update(poll, now + timedelta(seconds=int(e.retry_after) + 1)) session.add(update) session.commit() except (UniqueViolation, IntegrityError): session.rollback() except Exception: # Something happened # Schedule an update after two secondsj try: update = Update(poll, now + timedelta(seconds=3)) session.add(update) session.commit() except (UniqueViolation, IntegrityError): # The update has already been added session.rollback()
def try_update_reference(session, bot, poll, reference, first_try=False): try: update_reference(session, bot, poll, reference, first_try) except RetryAfter as e: session.rollback() # Handle a flood control exception on initial reference update. retry_after_seconds = int(e.retry_after) + 1 retry_after = datetime.now() + timedelta(seconds=retry_after_seconds) update = Update(poll, retry_after) try: session.add(update) session.commit() except IntegrityError: # There's already a scheduled update for this poll. session.rollback() except IntegrityError: # There's already a scheduled update for this poll. session.rollback()
def update_poll_messages(session, bot, poll): """Logic for handling updates.""" now = datetime.now() # Check whether we have a new window current_update = session.query(Update) \ .filter(Update.poll == poll) \ .one_or_none() # Don't handle it in here, it's already handled in the job if current_update is not None: return try: # Try to send updates send_updates(session, bot, poll) except (TimedOut, RetryAfter) as e: # Schedule an update after the RetryAfter timeout + 1 second buffer if isinstance(e, RetryAfter): retry_after = int(e.retry_after) + 1 else: retry_after = 2 try: update = Update(poll, now + timedelta(seconds=retry_after)) session.add(update) session.commit() except (UniqueViolation, IntegrityError): session.rollback() except Exception as e: # We encountered an unknown error # Since we don't want to continuously tro to send this update, and spam sentry, delete the update if current_update is not None: session.delete(current_update) session.commit() raise e
def update_poll_messages(session, bot, poll, message_id=None, user=None, inline_message_id=None): """Logic for handling updates. The message the original call has been made from will be updated instantly. The updates on all other messages will be scheduled in the background. """ now = datetime.now() reference = None if message_id is not None: reference = (session.query(Reference).filter( Reference.message_id == message_id).filter( Reference.user == user).filter( Reference.poll == poll).one_or_none()) elif inline_message_id is not None: reference = (session.query(Reference).filter( Reference.bot_inline_message_id == inline_message_id).filter( Reference.poll == poll).one_or_none()) # Check whether there already is a scheduled update new_update = False retry_after = None update = session.query(Update).filter(Update.poll == poll).one_or_none() if reference is not None: try: update_reference(session, bot, poll, reference) except RetryAfter as e: retry_after = int(e.retry_after) + 1 retry_after = datetime.now() + timedelta(seconds=retry_after) pass # If there's no update yet, create a new one if update is None: try: update = Update(poll, now) session.add(update) if retry_after is not None: update.next_update = retry_after session.commit() new_update = True except (UniqueViolation, IntegrityError): # Some other function already created the update. Try again session.rollback() update_poll_messages(session, bot, poll) if not new_update: # In case there already is an update increase the counter and set the next_update date # This will result in a new update in the background job and ensures # currently (right now) running updates will be scheduled again. if retry_after is not None: next_update = retry_after else: next_update = datetime.now() try: session.query(Update).filter(Update.poll == poll).update({ "count": Update.count + 1, "next_update": next_update }) except ObjectDeletedError: # This is a hard edge-case # This occurs, if the update we got a few microseconds ago # just got deleted by the background job. It happens maybe # once every 10000 requests and fixes itself as soon as somebody # votes on the poll once more # # The result of this MAY be, that polls in other chats have a # desync of a single vote. But it may also be the case, that # everything is already in sync. session.rollback() # Anyway just try again update_poll_messages(session, bot, poll) pass
def update_reference( session, bot, poll, reference, show_warning=False, first_try=False ): try: # Admin poll management interface if reference.type == ReferenceType.admin.name and not poll.in_settings: text, keyboard = get_poll_text_and_vote_keyboard( session, poll, user=poll.user, show_warning=show_warning, show_back=True ) if poll.user.expected_input != ExpectedInput.votes.name: keyboard = get_management_keyboard(poll) bot.edit_message_text( text, chat_id=reference.user.id, message_id=reference.message_id, reply_markup=keyboard, parse_mode="markdown", disable_web_page_preview=True, ) # User that votes in private chat (priority vote) elif reference.type == ReferenceType.private_vote.name: text, keyboard = get_poll_text_and_vote_keyboard( session, poll, user=reference.user, show_warning=show_warning, ) bot.edit_message_text( text, chat_id=reference.user.id, message_id=reference.message_id, reply_markup=keyboard, parse_mode="markdown", disable_web_page_preview=True, ) # Edit message created via inline query elif reference.type == ReferenceType.inline.name: # Create text and keyboard text, keyboard = get_poll_text_and_vote_keyboard( session, poll, show_warning=show_warning ) bot.edit_message_text( text, inline_message_id=reference.bot_inline_message_id, reply_markup=keyboard, parse_mode="markdown", disable_web_page_preview=True, ) except BadRequest as e: if ( e.message.startswith("Message_id_invalid") or e.message.startswith("Message can't be edited") or e.message.startswith("Message to edit not found") or e.message.startswith("Chat not found") or e.message.startswith("Can't access the chat") ): # This is just a hunch # It feels like we're too fast and the message isn't synced between Telegram's servers yet. # If this happens, allow the first try to fail and schedule an update # If it happens again, we'll fail on the second try if first_try: update = Update(poll, datetime.now() + timedelta(seconds=2)) session.add(update) sentry.capture_exception( extra={ "context": "update_reference", }, ) return session.delete(reference) session.commit() elif e.message.startswith("Message is not modified"): pass else: raise e except Unauthorized: session.delete(reference) session.commit() except TimedOut: # Ignore timeouts during updates for now pass
def update_poll_messages(session, bot, poll): """Logic for handling updates.""" # Round the current time to the nearest time window now = datetime.now() time_window = now - timedelta(seconds=now.second % window_size, microseconds=now.microsecond) one_minute_ago = time_window - timedelta(minutes=1) # Check whether we have a new window current_update = session.query(Update) \ .filter(Update.poll == poll) \ .filter(Update.time_window == time_window) \ .one_or_none() updates_in_last_minute = session.query(func.sum(Update.count)) \ .filter(Update.poll == poll) \ .filter(Update.time_window >= one_minute_ago) \ .one()[0] if updates_in_last_minute is None: updates_in_last_minute = 0 # No window yet, we need to create it if current_update is None: try: # Create and commit update. # This automatically schedules the update and might result in a double # update, if the job runs at practically the same time. # The worst case scenario is a Message is not modified exception. current_update = Update(poll, time_window) session.add(current_update) session.commit() # We are below the flood_limit, just update it if updates_in_last_minute <= flood_threshold: # Try to send updates send_updates(session, bot, poll) # If that succeeded, set updated to true and increase count # Update inside of mysql to avoid race conditions between threads session.query(Update) \ .filter(Update.id == current_update.id) \ .update({ 'count': Update.count + 1, 'updated': True, }) except (IntegrityError, UniqueViolation): # The update has been already created in another thread # Get the update and work with this instance session.rollback() current_update = session.query(Update) \ .filter(Update.poll == poll) \ .filter(Update.time_window == time_window) \ .one() # The update should be updated again elif current_update and current_update.updated: try: # We are still below the flood_threshold, update directrly if updates_in_last_minute <= flood_threshold: if updates_in_last_minute == flood_threshold: send_updates(session, bot, poll, show_warning=True) else: send_updates(session, bot, poll) # Update inside of mysql to avoid race conditions between threads session.query(Update) \ .filter(Update.id == current_update.id) \ .update({'count': Update.count + 1}) # Reschedule the update, the job will increment the count else: current_update.updated = False except Exception as e: # Some error occurred during updating of the message. # Set the updated flag to False to reschedule the update! current_update.updated = False # Commit here for now and raise e. Just for temporary debugging and monitoring purposes session.commit() raise e # The next update is already scheduled elif current_update and not current_update.updated: pass session.commit()
def update_poll_messages(session, bot, poll): """Logic for handling updates.""" # Round the current time to the nearest time window now = datetime.now() time_window = now - timedelta(seconds=now.second % window_size, microseconds=now.microsecond) one_minute_ago = time_window - timedelta(minutes=1) # Check whether we have a new window current_update = session.query(Update) \ .filter(Update.poll == poll) \ .filter(Update.time_window == time_window) \ .one_or_none() updates_in_last_minute = session.query(func.sum(Update.count)) \ .filter(Update.poll == poll) \ .filter(Update.time_window >= one_minute_ago) \ .one()[0] if updates_in_last_minute is None: updates_in_last_minute = 0 # No window yet, we need to create it if current_update is None: try: update = Update(poll, time_window) session.add(update) # We are below the flood_limit, just update it if updates_in_last_minute <= flood_threshold: update.count = 1 update.updated = True # Set the count and updated to true to avoid racing conditions with other threads session.commit() send_updates(session, bot, poll) # We are above the flood limit. Simply commit and thereby schedule the update. else: session.commit() except (IntegrityError, UniqueViolation): # The update has been already created in another thread # Get the update and work with this instance session.rollback() current_update = session.query(Update) \ .filter(Update.time_window == time_window) \ .one() # The update should be updated again elif current_update and current_update.updated: # We are still below the flood_threshold, update directrly if updates_in_last_minute <= flood_threshold: if updates_in_last_minute == flood_threshold: send_updates(session, bot, poll, show_warning=True) else: send_updates(session, bot, poll) # Update inside of mysql to avoid race conditions between threads session.query(Update) \ .filter(Update.id == current_update.id) \ .update({'count': Update.count + 1}) # Reschedule the update, the job will increment the count else: current_update.updated = False # The next update is already scheduled elif current_update and not current_update.updated: pass