Skip to content

[Bug]: SyncOpenViking.search(session=sess) deadlocks (hangs indefinitely) #226

@ponsde

Description

@ponsde

Title: [Bug]: SyncOpenViking.search(session=sess) deadlocks (hangs indefinitely)


Bug Description

SyncOpenViking.search() hangs forever when a session parameter is provided. Normal search() (without session) and find() work fine.

Root Cause

Traced through source code:

  1. SyncOpenViking.search()run_async(async_client.search(...)) — schedules coroutine on the shared background loop thread
  2. async_client.search()local.py search()session.get_context_for_search(query) (sync method)
  3. Session.get_context_for_search() internally calls run_async(self._viking_fs.ls(...)) and run_async(self._viking_fs.read_file(...))
  4. These nested run_async() calls submit new coroutines to the same shared loop via run_coroutine_threadsafe() and block with .result()
  5. But the shared loop thread is already occupied executing step 1's coroutine → deadlock: loop thread waiting on itself
Main Thread                    Shared Loop Thread
    │                               │
    ├─ run_async(search())          │
    │   └─ future.result() ────────►│─ executing search() coroutine
    │       (blocking main)         │   ├─ session.get_context_for_search()
    │                               │   │   └─ run_async(viking_fs.ls())
    │                               │   │       └─ future.result() ← BLOCKED
    │                               │   │           (waiting for loop thread,
    │                               │   │            but loop thread is here!)
    │                               │   │
    │                               │   └─ 💀 DEADLOCK

Steps to Reproduce

Minimal script (no external dependencies beyond openviking):

import asyncio, threading

from openviking_cli.utils.async_utils import _get_loop, run_async

# Simulates the exact call pattern: async function calls sync method that calls run_async
async def outer():
    print(f"outer() on thread: {threading.get_ident()}")
    
    def sync_inner():  # Like session.get_context_for_search()
        print(f"sync_inner() on thread: {threading.get_ident()}")
        async def dummy():
            await asyncio.sleep(0.01)
            return "ok"
        return run_async(dummy())  # Submits to same loop → deadlock
    
    return sync_inner()

# This will hang forever
result = run_async(outer())

Full OpenViking reproduction:

import os
os.environ['OPENVIKING_CONFIG_FILE'] = './ov.conf'
import openviking as ov
from openviking.message.part import TextPart

client = ov.SyncOpenViking(path='./data')
client.initialize()

# This works fine:
r1 = client.search("test query", limit=3)
print(f"search() without session: OK, {len(r1.resources)} results")

# This hangs forever:
sess_info = client.create_session()
sess = client.session(sess_info['session_id'])
sess.add_message('user', [TextPart("test query")])
r2 = client.search("test query", session=sess, limit=3)  # ← never returns

Workaround

Use AsyncOpenViking with asyncio.run() instead. The nested run_async() calls go to the shared background loop, while the outer coroutine runs on a separate temporary loop — no deadlock:

import asyncio
import openviking as ov
from openviking.message.part import TextPart

async def search_with_session():
    client = ov.AsyncOpenViking(path='./data')
    await client.initialize()
    sess_info = await client.create_session()
    sess = client.session(sess_info['session_id'])
    sess.add_message('user', [TextPart("test query")])
    result = await client.search("test query", session=sess, limit=3)
    return result

result = asyncio.run(search_with_session())  # Works!

Suggested Fix

Make Session.get_context_for_search() async, or at minimum avoid calling run_async() from within a coroutine already running on the shared loop. The sync Session methods (load(), commit(), get_context_for_search()) all use run_async() internally, which is safe when called from the main thread but deadlocks when called from within the shared loop's own coroutines.

Environment

  • openviking: 0.1.17
  • Python: 3.12
  • OS: Ubuntu 24.04
  • Reproduced in both production and clean (empty data directory) environments

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions