# Buggy code
async def _find_unreferenced_groups(self, state_groups: Set[int]) -> Set[int]:
    """Used when purging history to figure out which state groups can be
    deleted.

    Args:
        state_groups: Set of state groups referenced by events
            that are going to be deleted.

    Returns:
        The set of state groups that can be deleted.
    """
    # Graph of state group -> previous group
    graph = {}

    # Set of events that we have found to be referenced by events
    referenced_groups = set()

    # Set of state groups we've already seen
    state_groups_seen = set(state_groups)

    # Set of state groups to handle next.
    next_to_search = set(state_groups)
    while next_to_search:
        # We bound size of groups we're looking up at once, to stop the
        # SQL query getting too big
        if len(next_to_search) < 100:
            current_search = next_to_search
            next_to_search = set()
        else:
            current_search = set(itertools.islice(next_to_search, 100))
            next_to_search -= current_search

        referenced = await self.stores.main.get_referenced_state_groups(current_search)
        referenced_groups |= referenced

        # We don't continue iterating up the state group graphs for state
        # groups that are referenced.
        current_search -= referenced

        edges = await self.stores.state.get_previous_state_groups(current_search)

        prevs = set(edges.values())
        # We don't bother re-handling groups we've already seen
        prevs -= state_groups_seen
        next_to_search |= prevs
        state_groups_seen |= prevs

        graph.update(edges)

    to_delete = state_groups_seen - referenced_groups

    return to_delete


# Traceback (twisted.internet.defer.FirstError)
synapse.http.server: [POST-10040] Failed handle request via 'JoinRoomAliasServlet': <XForwardedForRequest at 0x7f57646fa970 method='POST' uri='/_matrix/client/r0/join/%23synapse%3Amatrix.org' clientproto='HTTP/1.1' site='8008'>
Traceback (most recent call last):
File "/usr/lib/python3.9/site-packages/synapse/http/server.py", line 252, in _async_render_wrapper
callback_return = await self._async_render(request)
File "/usr/lib/python3.9/site-packages/synapse/http/server.py", line 430, in _async_render
callback_return = await raw_callback_return
File "/usr/lib/python3.9/site-packages/synapse/rest/client/v1/room.py", line 301, in on_POST
await self.room_member_handler.update_membership(
File "/usr/lib/python3.9/site-packages/synapse/handlers/room_member.py", line 333, in update_membership
result = await self.update_membership_locked(
File "/usr/lib/python3.9/site-packages/synapse/handlers/room_member.py", line 549, in update_membership_locked
remote_join_response = await self._remote_join(
File "/usr/lib/python3.9/site-packages/synapse/handlers/room_member.py", line 1091, in _remote_join
event_id, stream_id = await self.federation_handler.do_invite_join(
File "/usr/lib/python3.9/site-packages/synapse/handlers/federation.py", line 1400, in do_invite_join
max_stream_id = await self._persist_auth_tree(
File "/usr/lib/python3.9/site-packages/synapse/handlers/federation.py", line 2050, in _persist_auth_tree
await self.persist_events_and_notify(
File "/usr/lib/python3.9/site-packages/synapse/handlers/federation.py", line 2925, in persist_events_and_notify
events, max_stream_token = await self.storage.persistence.persist_events(
File "/usr/lib/python3.9/site-packages/synapse/storage/persist_events.py", line 262, in persist_events
ret_vals = await make_deferred_yieldable(
twisted.internet.defer.FirstError: FirstError[#0, [Failure instance: Traceback: <class 'psycopg2.errors.UniqueViolation'>: duplicate key value violates unique constraint "event_auth_chains_pkey"
DETAIL:  Key (event_id)=($e9U026auDHIgaZPAqlblvPupACjl7jcZDblP970dJPs) already exists.

/usr/lib/python3.9/site-packages/synapse/metrics/background_process_metrics.py:208:run
--- <exception caught here> ---
/usr/lib/python3.9/site-packages/synapse/storage/persist_events.py:172:handle_queue_loop
/usr/lib/python3.9/site-packages/synapse/storage/persist_events.py:324:persisting_queue
/usr/lib/python3.9/site-packages/synapse/storage/persist_events.py:532:_persist_events
/usr/lib/python3.9/site-packages/synapse/storage/databases/main/events.py:171:_persist_events_and_state_updates
/usr/lib/python3.9/site-packages/synapse/storage/database.py:661:runInteraction
/usr/lib/python3.9/site-packages/synapse/storage/database.py:744:runWithConnection
/usr/lib64/python3.9/site-packages/twisted/python/threadpool.py:250:inContext
/usr/lib64/python3.9/site-packages/twisted/python/threadpool.py:266:<lambda>
/usr/lib64/python3.9/site-packages/twisted/python/context.py:122:callWithContext
/usr/lib64/python3.9/site-packages/twisted/python/context.py:85:callWithContext
/usr/lib64/python3.9/site-packages/twisted/enterprise/adbapi.py:306:_runWithConnection
/usr/lib64/python3.9/site-packages/twisted/python/compat.py:464:reraise
/usr/lib64/python3.9/site-packages/twisted/enterprise/adbapi.py:297:_runWithConnection
/usr/lib/python3.9/site-packages/synapse/storage/database.py:739:inner_func
/usr/lib/python3.9/site-packages/synapse/storage/database.py:539:new_transaction
/usr/lib/python3.9/site-packages/synapse/logging/utils.py:71:wrapped
/usr/lib/python3.9/site-packages/synapse/storage/databases/main/events.py:379:_persist_events_txn
/usr/lib/python3.9/site-packages/synapse/storage/databases/main/events.py:472:_persist_event_auth_chain_txn
/usr/lib/python3.9/site-packages/synapse/storage/databases/main/events.py:630:_add_chain_cover_index
/usr/lib/python3.9/site-packages/synapse/storage/database.py:896:simple_insert_many_txn
/usr/lib/python3.9/site-packages/synapse/storage/database.py:274:execute_batch
/usr/lib/python3.9/site-packages/synapse/storage/database.py:319:_do_execute
/usr/lib/python3.9/site-packages/synapse/storage/database.py:274:<lambda>
/usr/lib64/python3.9/site-packages/psycopg2/extras.py:1209:execute_batch
]]

# Fixed code
async def _find_unreferenced_groups(self, state_groups: Set[int]) -> Set[int]:
    """Used when purging history to figure out which state groups can be
    deleted.

    Args:
        state_groups: Set of state groups referenced by events
            that are going to be deleted.

    Returns:
        The set of state groups that can be deleted.
    """
    # Set of events that we have found to be referenced by events
    referenced_groups = set()

    # Set of state groups we've already seen
    state_groups_seen = set(state_groups)

    # Set of state groups to handle next.
    next_to_search = set(state_groups)
    while next_to_search:
        # We bound size of groups we're looking up at once, to stop the
        # SQL query getting too big
        if len(next_to_search) < 100:
            current_search = next_to_search
            next_to_search = set()
        else:
            current_search = set(itertools.islice(next_to_search, 100))
            next_to_search -= current_search

        referenced = await self.stores.main.get_referenced_state_groups(current_search)
        referenced_groups |= referenced

        # We don't continue iterating up the state group graphs for state
        # groups that are referenced.
        current_search -= referenced

        edges = await self.stores.state.get_previous_state_groups(current_search)

        prevs = set(edges.values())
        # We don't bother re-handling groups we've already seen
        prevs -= state_groups_seen
        next_to_search |= prevs
        state_groups_seen |= prevs

    to_delete = state_groups_seen - referenced_groups

    return to_delete