Skip to content

refactor(ray): drop obsolete indexer env vars, make Milvus timeout configurable#566

Merged
Ahmath-Gadji merged 9 commits into
refactor/hexagonalfrom
refactor/ray-config-cleanup
Jun 26, 2026
Merged

refactor(ray): drop obsolete indexer env vars, make Milvus timeout configurable#566
Ahmath-Gadji merged 9 commits into
refactor/hexagonalfrom
refactor/ray-config-cleanup

Conversation

@Ahmath-Gadji

@Ahmath-Gadji Ahmath-Gadji commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

The hexagonal indexer refactor left several Ray/indexer config knobs with no remaining consumers. This PR removes the dead vars, restores a real multi-actor indexer pool, and makes the Milvus client timeout configurable.

Base branch is refactor/hexagonal. These changes build on the hexagonal indexer (IndexerWorkerActor, plain MilvusVectorStore, SemaphoreConfig), so they don't apply to main.

Removed env vars

Each removed var's responsibility either moved elsewhere or never existed in this branch:

Env var Why it's safe to remove
RAY_NUM_GPUS Wired to no actor on either branch (dead). Marker/Docling reserve GPU via their own MARKER_NUM_GPUS / DOCLING_NUM_GPUS.
RAY_MAX_TASK_RETRIES Ray actor-task retries replaced by per-parser retry_with_backoff.
INDEXER_SERIALIZE_TIMEOUT Serialization became the parse stage, bounded by per-parser timeouts (MARKER_TIMEOUT, DOCLING_TIMEOUT, …).
VECTORDB_TIMEOUT Replaced by VDB_TIMEOUT (see below).
RAY_SEMAPHORE_CONCURRENCY Real LLM/VLM throttles are LLM_SEMAPHORE / VLM_SEMAPHORE (SemaphoreConfig).
INDEXER_DEFAULT_CONCURRENCY, INDEXER_UPDATE_CONCURRENCY, INDEXER_SEARCH_CONCURRENCY, INDEXER_DELETE_CONCURRENCY, INDEXER_SERIALIZE_CONCURRENCY, INDEXER_CHUNK_CONCURRENCY, INDEXER_INSERT_CONCURRENCY Ray concurrency_groups only partition concurrent calls across distinct actor methods. Post-refactor the indexer exposes a single method, so the groups are inert; contention is now handled by physical worker separation.

Added

  • VDB_TIMEOUT (vectordb.timeout, default 120s) — replaces the hardcoded 60s MilvusVectorStore client timeout, applied to both the sync and async Milvus clients.

Pool restored

RAY_POOL_SIZE now spawns pool_size IndexerWorkerActor instances with least-loaded dispatch (the ObjectRef is preserved, so ray.cancel and task-state tracking keep working). pool_size and max_tasks_per_worker moved under ray.indexer with ge=1 validation. Env var names are unchanged (RAY_POOL_SIZE, RAY_MAX_TASKS_PER_WORKER) — only the internal config path moved.

Also (minor)

Drops redundant *_API_KEY os.getenv double-reads in model-endpoint seeding. The config loader already maps API_KEY / EMBEDDER_API_KEY / VLM_API_KEY / RERANKER_API_KEY onto Settings, so seed_defaults reads each once via s.<type>.api_key. This also makes seeding deterministic when a local .env is loaded into the process.

Migration

Deployments that set any removed var should drop it. The only rename is VECTORDB_TIMEOUT (default 30s) → VDB_TIMEOUT (default 120s).

Testing

  • uv run python -m pytest tests/unit/1340 passed
  • uv run ruff check → clean
  • New coverage: tests/unit/core/config/test_ray_config.py, tests/unit/core/config/test_vectordb_config.py

Summary by CodeRabbit

  • New Features
    • Added/updated configurable Milvus per-request timeout (default now 120s).
    • Improved Ray indexing tuning by deriving total indexing concurrency from the indexer worker pool; default per-worker task limit increased to 50.
  • Bug Fixes
    • More reliable indexing dispatch and in-flight tracking/release under load.
  • Documentation
    • Updated Ray and environment-variable guidance to reflect the new timeout and indexing concurrency controls, and removed outdated GPU tuning references.
  • Tests
    • Added/updated unit tests for Milvus timeout and the revised indexing pool/dispatcher behavior.

…nfigurable

The hexagonal indexer refactor left several Ray/indexer config knobs with no
remaining consumers. Remove them, restore a real multi-actor indexer pool, and
expose the previously-hardcoded Milvus client timeout as config.

Removed env vars (concern re-homed or dead):
- RAY_NUM_GPUS              — wired to no actor on either branch
- RAY_MAX_TASK_RETRIES      — retries now per-parser (retry_with_backoff)
- INDEXER_SERIALIZE_TIMEOUT — superseded by per-parser MARKER/DOCLING timeouts
- VECTORDB_TIMEOUT          — replaced by VDB_TIMEOUT
- RAY_SEMAPHORE_CONCURRENCY — real throttles are LLM_SEMAPHORE / VLM_SEMAPHORE
- INDEXER_{DEFAULT,UPDATE,SEARCH,DELETE,SERIALIZE,CHUNK,INSERT}_CONCURRENCY
  — concurrency groups are inert now that the indexer exposes a single method

Added:
- VDB_TIMEOUT (vectordb.timeout, default 120s) replaces the hardcoded
  MilvusVectorStore client timeout (was 60s), on both sync and async clients

Pool:
- RAY_POOL_SIZE now spawns pool_size IndexerWorkerActor instances with
  least-loaded dispatch (ObjectRef preserved for cancellation + task state);
  pool_size and max_tasks_per_worker move under ray.indexer with ge=1
  validation. Env var names are unchanged.

Also drops redundant *_API_KEY os.getenv double-reads in model-endpoint
seeding — the loader already maps those vars into Settings.
@Ahmath-Gadji Ahmath-Gadji added the breaking-change Change of behavior after upgrade label Jun 25, 2026
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 82d6af7c-c300-409b-8ee2-9819001a6807

📥 Commits

Reviewing files that changed from the base of the PR and between 2eae96d and 6e078e5.

📒 Files selected for processing (1)
  • tests/unit/services/workers/test_indexer_pool.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/services/workers/test_indexer_pool.py

📝 Walkthrough

Walkthrough

The PR adds VDB_TIMEOUT, moves Ray indexing settings under ray.indexer, updates docs and deployment defaults, switches Milvus and seed setup to config-backed values, and changes indexing dispatch to use detached worker actors.

Changes

Configuration, docs, and runtime wiring

Layer / File(s) Summary
Config schema and overrides
openrag/core/config/infrastructure.py, openrag/core/config/loader.py, conf/config.yaml, infra/charts/openrag-stack/values.yaml, infra/compose/.env.ollama, tests/unit/core/config/*
VectorDBConfig adds timeout, Ray indexer settings move under ray.indexer, environment overrides and default configs follow the new paths, and config tests cover the new defaults and validation.
Docs and env vars
docs/content/docs/documentation/deploy_ray_cluster.md, docs/content/docs/documentation/env_vars.md
The Ray deployment guide and env-var reference now describe the nested indexer concurrency settings, the VDB timeout, and the removal of GPU-oriented Ray entries.
Milvus and seed config use
openrag/services/storage/milvus_store.py, openrag/services/orchestrators/model_endpoint_service.py
MilvusVectorStore now uses config.timeout, and seed construction reads API keys from Settings values instead of direct environment lookups.
Indexer pool actor split
openrag/services/workers/indexer_pool.py, tests/unit/services/workers/test_indexer_pool.py
IndexerWorkerActor is introduced, IndexerPool becomes a detached dispatcher over named workers, and the worker tests cover the new pool behavior and actor wiring.
Dispatcher and task-state wiring
openrag/services/workers/dispatcher.py, openrag/services/workers/task_state.py, tests/unit/services/workers/test_dispatcher.py
dispatch_indexing now calls submit, task-state capacity reads the nested Ray config, and dispatcher tests assert the updated remote-call shape.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • linagora/openrag#511: Updates the same openrag/services/workers/indexer_pool.py construction path and detached Ray worker setup.
  • linagora/openrag#419: Shares the TaskStateManager / Ray pool capacity wiring updated in openrag/services/workers/task_state.py.
  • linagora/openrag#393: Touches MilvusVectorStore in the same module where the new timeout config is applied.

Suggested labels

refactor, fix

Suggested reviewers

  • hedhoud

Poem

A bunny hopped through config land,
With timeout tea in paw and hand.
The pool split left and split split right,
Then raced the tasks in bounded flight.
🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.87% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the env var cleanup and Milvus timeout configurability, which are major parts of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/ray-config-cleanup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hedhoud hedhoud left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found two things I’d like to address before merging.

First, the new pool balancing looks local to each API process. That means with multiple API_NUM_WORKERS, each process keeps its own view of in-flight work. During a burst, several API workers can all think the same indexer actor is the least loaded and send work there, while the rest of the pool stays underused. Since the PR’s main goal is to restore real multi-actor indexing capacity, I think the dispatch state should either live in a shared Ray-side component, or use a distribution strategy that does not depend on per-process counters.

Second, the env var documentation still looks inconsistent after the cleanup. RAY_MAX_TASKS_PER_WORKER appears with two different defaults, and RAY_SEMAPHORE_CONCURRENCY is still documented even though this PR removes it from config loading. Because this PR is intentionally removing obsolete knobs, keeping stale docs will make migration harder for deployments.

The targeted tests pass locally and CI is green, so this looks close. I’d just like these two points handled or explicitly clarified before merge.

@Ahmath-Gadji Ahmath-Gadji marked this pull request as ready for review June 25, 2026 07:43
@Ahmath-Gadji Ahmath-Gadji marked this pull request as draft June 25, 2026 07:43
@coderabbitai coderabbitai Bot added fix Fix issue refactor labels Jun 25, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/content/docs/documentation/env_vars.md (1)

379-400: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the Ray defaults consistent across both tables.

RAY_MAX_TASKS_PER_WORKER is documented as 8 in the General Ray Settings table and 50 in the Indexer Configuration table. Please pick one source of truth or remove the duplicate row, otherwise readers get conflicting defaults for the same env var.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/content/docs/documentation/env_vars.md` around lines 379 - 400, The
documentation has conflicting defaults for the same env var, specifically
RAY_MAX_TASKS_PER_WORKER in the Ray settings table and the Indexer Configuration
table. Make the value consistent by choosing one source of truth and updating
the duplicate row in env_vars.md, or remove the redundant entry so the README
only documents a single default for RAY_MAX_TASKS_PER_WORKER.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/content/docs/documentation/deploy_ray_cluster.md`:
- Around line 56-57: Update the sizing guidance in the deployment docs so it
consistently refers to indexing concurrency via RAY_POOL_SIZE and
RAY_MAX_TASKS_PER_WORKER, and remove or reword the leftover GPU memory caution
so it no longer suggests subtracting GPU memory from the pool size. Keep the
guidance aligned with the Ray cluster sizing section by adjusting the
surrounding prose in the deploy_ray_cluster document rather than introducing
mixed CPU/GPU budgeting rules.

In `@openrag/core/config/infrastructure.py`:
- Around line 23-24: The VectorDBConfig timeout field currently allows zero or
negative values, so tighten validation at the model level by adding a
positive-only Field constraint to timeout in VectorDBConfig. Update the timeout
declaration in infrastructure.py so config loading fails immediately for
non-positive values instead of deferring the error until Milvus client usage.

In `@openrag/services/workers/indexer_pool.py`:
- Around line 252-255: The worker selection logic in IndexerPool currently
increments _inflight before calling process_file.remote, but if that remote
submission fails the counter is never rolled back. Update the submission path in
the method that builds ref/task to wrap
self._workers[idx].process_file.remote(**kwargs) in a try/except, and on failure
decrement self._inflight[idx] before re-raising so load balancing stays
accurate.
- Around line 275-282: The IndexerWorkerActor creation path is silently reusing
detached actors via get_if_exists=True, so updated max_concurrency values are
ignored for existing actors. In the worker pool setup that builds the workers
list, change the IndexerWorkerActor.options/.remote flow to detect when an actor
already exists with a stale concurrency setting and explicitly shut it down
before recreating it, or otherwise ensure a fresh actor is created when
max_concurrency changes. Keep the fix centered around the IndexerWorkerActor and
the worker initialization logic in the pool.

---

Outside diff comments:
In `@docs/content/docs/documentation/env_vars.md`:
- Around line 379-400: The documentation has conflicting defaults for the same
env var, specifically RAY_MAX_TASKS_PER_WORKER in the Ray settings table and the
Indexer Configuration table. Make the value consistent by choosing one source of
truth and updating the duplicate row in env_vars.md, or remove the redundant
entry so the README only documents a single default for
RAY_MAX_TASKS_PER_WORKER.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ef98b9ea-a967-40b9-a0dc-394ec1e0e092

📥 Commits

Reviewing files that changed from the base of the PR and between 8826408 and 9ad1766.

📒 Files selected for processing (16)
  • conf/config.yaml
  • docs/content/docs/documentation/deploy_ray_cluster.md
  • docs/content/docs/documentation/env_vars.md
  • infra/charts/openrag-stack/values.yaml
  • infra/compose/.env.ollama
  • openrag/core/config/infrastructure.py
  • openrag/core/config/loader.py
  • openrag/services/orchestrators/model_endpoint_service.py
  • openrag/services/storage/milvus_store.py
  • openrag/services/workers/dispatcher.py
  • openrag/services/workers/indexer_pool.py
  • openrag/services/workers/task_state.py
  • tests/unit/core/config/test_ray_config.py
  • tests/unit/core/config/test_vectordb_config.py
  • tests/unit/services/workers/test_dispatcher.py
  • tests/unit/services/workers/test_indexer_pool.py
💤 Files with no reviewable changes (2)
  • infra/compose/.env.ollama
  • infra/charts/openrag-stack/values.yaml

Comment thread docs/content/docs/documentation/deploy_ray_cluster.md
Comment thread openrag/core/config/infrastructure.py Outdated
Comment thread openrag/services/workers/indexer_pool.py
Comment thread openrag/services/workers/indexer_pool.py Outdated
@hedhoud

hedhoud commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Small clarification on my earlier pool-balancing comment: the concern is not specifically API_NUM_WORKERS.

I agree that API_NUM_WORKERS itself is a footgun and should be removed/ignored like we did on main in #501. The broader issue is where the new pool balancing state lives.

Right now the _inflight counters live inside the API-side IndexerPool object. That means each API process or Ray Serve replica has its own local view of worker load.

Example:

  • RAY_POOL_SIZE=3, so we have IndexerWorker-0, IndexerWorker-1, and IndexerWorker-2.
  • RAY_SERVE_NUM_REPLICAS=4, so we have four API replicas.
  • Each API replica starts with its own local counters saying all three workers are free.

If several uploads arrive at the same time, each API replica can independently decide that IndexerWorker-0 is the least loaded worker, because locally it sees the pool as empty. Globally, that can put several tasks on worker 0 while workers 1 and 2 are still idle.

So the concern is about local dispatch state across multiple API replicas. Removing API_NUM_WORKERS fixes the Uvicorn multi-worker case, but we should still clarify or handle the Ray Serve replica case if this pool is meant to provide balanced multi-actor indexing capacity.

The VectorDBConfig.timeout field accepted 0/negative values, deferring
the failure to Milvus client usage. Constrain it with gt=0 so config
loading fails immediately.
IndexerPool.submit incremented _inflight before calling
process_file.remote. If that submission raised (unserializable args,
dead actor), the count stayed elevated and skewed least-loaded
dispatch. Decrement and re-raise on failure.
RAY_MAX_TASKS_PER_WORKER was documented with two defaults (8 and 50);
the code default is 50, so correct the General Ray Settings row and
drop the duplicate Indexer Configuration table. Also remove the stale
RAY_SEMAPHORE_CONCURRENCY section — this var no longer exists after the
cleanup.
The GPU-memory caution still told readers to subtract GPU memory from
the pool size, but indexer workers no longer reserve GPU after the
RAY_NUM_GPUS removal. Reword it to route GPU budgeting through the
parser (MARKER_NUM_GPUS) and model-server settings, keeping pool sizing
about indexing throughput.
The client-side IndexerPool kept per-replica in-flight counters, so
multiple API processes / Ray Serve replicas each balanced load against
their own view of the shared workers and could pile onto one worker
during bursts. Make IndexerPool a single detached named actor (shared
cluster-wide via get_if_exists) that owns the worker fleet and the
in-flight counts, so least-loaded dispatch is global.

submit returns the worker's ObjectRef wrapped in a one-element list so
ray.cancel still targets the real task (a bare returned ref can be
auto-dereferenced and block the dispatch).
@Ahmath-Gadji

Ahmath-Gadji commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks @hedhoud — both points are addressed:

1. Pool balancing state was local per replica. You're right. I moved the dispatch state off the API side into a single detached, named IndexerPool actor (get_if_exists=True), so it's one shared instance cluster-wide — every uvicorn process and every Ray Serve replica now dispatches through the same actor and the same _inflight view, instead of each replica balancing against its own local counters. The worker fleet (IndexerWorker-*) was already a shared singleton via the same named+detached pattern; only the dispatcher was per-replica, which is exactly what diverged. Commit c06cfac.

On API_NUM_WORKERS: agreed it should be removed/ignored like in #501 — I deliberately left it out of this PR because the #501 port is already heading to refactor/hexagonal via forward-port/main-to-hexagonal, and duplicating it here would conflict. So the uvicorn multi-worker case is covered by that forward-port, and the Serve-replica case by the shared dispatcher above.

2. Env-var docs. Fixed in 75b639a: RAY_MAX_TASKS_PER_WORKER now has a single source of truth (default 50, matching the config), the duplicate Indexer table is removed, and the stale RAY_SEMAPHORE_CONCURRENCY section is gone.

Resolved conflicts:
- indexer_pool.py: keep the new dispatcher-actor structure (IndexerWorkerActor
  + shared IndexerPool dispatcher) and fold in hexagonal's named embedder/VLM
  endpoint support (vlm_factory, dict-based required-model-name resolution,
  _global_embedder/_global_vlm fallbacks).
- model_endpoint_service.py: keep hexagonal's batch_size/timeout seed fields
  together with the single-read api_key cleanup (no os.getenv double-read).
@Ahmath-Gadji Ahmath-Gadji marked this pull request as ready for review June 25, 2026 13:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/services/workers/test_indexer_pool.py`:
- Around line 831-839: The async test leaves `_release` tasks pending because
`pool.submit(...)` schedules work that awaits the fake futures, so the event
loop can be torn down with unfinished tasks. In the test around `pool.submit`
and `ref0`, make sure the fake futures are completed and then await the release
tasks before the test exits, so all scheduled cleanup finishes cleanly. Apply
the same cleanup pattern in the other affected submit assertions as well, using
the existing `workers`, `pool.submit`, and future references already captured in
the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 422e639a-b1f7-4548-bc8f-991a260fa7b9

📥 Commits

Reviewing files that changed from the base of the PR and between 9ad1766 and e18224a.

📒 Files selected for processing (10)
  • docs/content/docs/documentation/deploy_ray_cluster.md
  • docs/content/docs/documentation/env_vars.md
  • infra/charts/openrag-stack/values.yaml
  • openrag/core/config/infrastructure.py
  • openrag/services/orchestrators/model_endpoint_service.py
  • openrag/services/workers/dispatcher.py
  • openrag/services/workers/indexer_pool.py
  • tests/unit/core/config/test_vectordb_config.py
  • tests/unit/services/workers/test_dispatcher.py
  • tests/unit/services/workers/test_indexer_pool.py
✅ Files skipped from review due to trivial changes (1)
  • tests/unit/services/workers/test_dispatcher.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • infra/charts/openrag-stack/values.yaml
  • openrag/core/config/infrastructure.py
  • openrag/services/orchestrators/model_endpoint_service.py
  • docs/content/docs/documentation/deploy_ray_cluster.md

Comment thread tests/unit/services/workers/test_indexer_pool.py
@hedhoud

hedhoud commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

One live-upgrade issue here is the same kind of Ray detached-actor compatibility concern we avoided in PR #568.

In #568, the detached IndexerPool name still matches the old actor shape. In this PR, IndexerPool becomes a dispatcher and callers now use submit, but the detached actor name is still reused with get_if_exists=true. During a rolling upgrade, Ray can return the old detached actor from the namespace, and that actor does not have submit, so indexing can fail until the actor or cluster is cleared.

This should use a new detached actor name for the new dispatcher shape, or add a small compatibility/migration guard before reusing the existing actor.

@Ahmath-Gadji Ahmath-Gadji merged commit bfdc829 into refactor/hexagonal Jun 26, 2026
6 checks passed
@Ahmath-Gadji Ahmath-Gadji deleted the refactor/ray-config-cleanup branch June 26, 2026 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change Change of behavior after upgrade fix Fix issue refactor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants