Skip to content

userlist missing names after a critical connection error #217

@dbohdan

Description

@dbohdan

I have noticed names missing in bot.channels after a connection error like this:

CRITICAL irc3.|foobot connection lost (136908348632496): TimeoutError('SSL shutdown timed out')

However, bot.channels isn't empty. The bot itself recovers when the Internet connection comes back, and it keeps working. It looks as if irc3.plugins.userlist stops updating bot.channels. Maybe it is only missing the names that joined during the downtime, but it doesn't seem that way.

Unfortunately, I am can't give you the steps to reproduce the problem because it is intermittent. Simply cutting the Internet connection doesn't reliably cause it.

Note I don't have irc3.plugins.userlist in includes in my config file, only a plugin that requires it. Could that prevent userlist from recovering?

As for solutions, irc3.plugins.userlist could issue a NAMES command to update its data when it is probably out of date. I have prototyped a NAMES-based irc3 plugin. It must still have bugs, and it only updates manually when you call update. I release this code under irc3's license.

names plugin source
import asyncio
import time
from dataclasses import dataclass, replace
from typing import ClassVar

import irc3  # type: ignore

MAX_OUTDATED = 10


@dataclass(frozen=True)
class ChannelUsers:
    flag: asyncio.Event
    names: frozenset[str]
    updated_at: float


@irc3.plugin
class NamesPlugin:
    requires: ClassVar[list[str]] = [
        "irc3.plugins.core",
    ]

    def __init__(self, bot):
        self.bot = bot
        self.bot.names_plugin = self

        self._curr_channel = ""
        self._flag = None
        self.channels: dict[str, ChannelUsers] = {}

    async def update(self, channel: str):
        if self._flag is not None:
            await self._flag.wait()

        curr_channel_users = self.channels.get(channel)
        if (
            curr_channel_users is not None
            and time.time() - curr_channel_users.updated_at <= MAX_OUTDATED
        ):
            return

        new_channel_users = ChannelUsers(asyncio.Event(), frozenset(), 0)

        self.channels[channel] = new_channel_users
        self._curr_channel = channel
        self._flag = new_channel_users.flag

        self.bot.send_line(f"NAMES {channel}")
        await self._flag.wait()

    @irc3.event(irc3.rfc.RPL_NAMREPLY)
    async def on_namreply(self, _mask=None, data=None, **_kw):
        if self._flag is None:
            return

        if not isinstance(data, str):
            self.bot.log.error("data is not 'str'")
            return

        channel = self._curr_channel
        channel_users = self.channels[channel]

        self.channels[channel] = replace(
            channel_users,
            names=channel_users.names | frozenset(data.split()),
        )

    @irc3.event(irc3.rfc.RPL_ENDOFNAMES)
    async def on_endofnames(self, _mask=None, _data=None, **_kw):
        if self._flag is None:
            return

        channel = self._curr_channel
        channel_users = self.channels[channel]

        channel_users.flag.set()
        self.channels[channel] = replace(channel_users, updated_at=time.time())

        self._curr_channel = ""
        self._flag = None

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions