Skip to content

Commit d109cc6

Browse files
authored
Merge pull request #35 from beersoccer/feature/memory-lifecycle-cleanup
chore: update configuration defaults and timeout values for improved performance
2 parents f2eeeed + ad79cb1 commit d109cc6

28 files changed

Lines changed: 467 additions & 96 deletions
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
- 目标:将性能测试的 `DIFY_USER_ID` 更名为“用户数量”并由代码生成 `user1..userN`
2+
- 修改 `performance/.env`:把 `DIFY_USER_COUNT` 设置为整数示例值
3+
- 新增 `performance/user_ids.py`:解析 env 值并生成用户列表
4+
- 更新 `performance/locustfile.py`:改用 `DIFY_USER_COUNT` 并保持随机选择用户
5+
- 更新 `performance/README.md`:同步配置说明与示例
6+
- 新增单元测试:覆盖数值与列表两种输入场景
7+
- 影响文件:`performance/.env`, `performance/locustfile.py`, `performance/user_ids.py`, `performance/README.md`, `tests/unit/utils/test_performance_user_ids.py`
8+

CONFIG.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ First, select the operation mode in plugin credentials:
6262
- Recommended for production environments
6363
- Supports high concurrency
6464
- Write operations (Add/Update/Delete/Delete_All): non-blocking, return ACCEPT status immediately
65-
- Read operations (Search/Get/Get_All/History): wait for results with timeout protection (default: 30s)
65+
- Read operations (Search/Get/Get_All/History): wait for results with timeout protection (default: 5s)
6666

6767
- **Sync Mode** (`async_mode=false`)
6868
- Recommended for testing environments
@@ -116,14 +116,14 @@ After installation, click on the `mem0ai` plugin to configure it. You'll see cre
116116
You can configure the following performance parameters in plugin settings to optimize concurrency and database connections for production environments:
117117

118118
**Performance Parameters:**
119-
- `max_concurrent_memory_operations` - Maximum concurrent memory operations (default: 40)
119+
- `max_concurrent_memory_operations` - Maximum concurrent memory operations (default: 20)
120120
- Applies to all operations including search/add/get/get_all/update/delete/delete_all/history
121121
- Must be a positive integer (>= 1)
122-
- Invalid values (<= 0 or cannot be converted to integer) will use default value 40 with warning logs
122+
- Invalid values (<= 0 or cannot be converted to integer) will use default value 20 with warning logs
123123

124124
**Concurrency Configuration Logic:**
125125
- **`max_concurrent_memory_operations` configured**: Uses the configured value directly
126-
- **Not configured**: Uses default value (40)
126+
- **Not configured**: Uses default value (20)
127127
- **Invalid values** (cannot be converted to positive integers): Uses default values and logs a warning
128128
- **Unset or empty values**: Uses default values and logs a warning
129129

@@ -276,8 +276,8 @@ The plugin automatically creates a psycopg3 ConnectionPool when `connection_stri
276276
"connection_string": "postgresql://<user>:<password>@<host>:<port>/<db>?sslmode=disable&keepalives=1&keepalives_idle=30&keepalives_interval=10&keepalives_count=3&connect_timeout=5",
277277
"collection_name": "mem0",
278278
"embedding_model_dims": 1536,
279-
"minconn": 2,
280-
"maxconn": 4
279+
"minconn": 10,
280+
"maxconn": 40
281281
}
282282
}
283283
```
@@ -349,9 +349,9 @@ If you need fine-grained control over pool sizing and lifecycle, you can add the
349349
"collection_name": "mem0",
350350
"embedding_model_dims": 1536,
351351
"min_connections": 10,
352-
"max_connections": 40,
352+
"max_connections": 20,
353353
"pool_min_size": 10,
354-
"pool_max_size": 40,
354+
"pool_max_size": 20,
355355
"pool_max_lifetime": 3600,
356356
"pool_max_idle": 600,
357357
"pool_timeout": 30,
@@ -364,9 +364,9 @@ If you need fine-grained control over pool sizing and lifecycle, you can add the
364364

365365
**Connection Pool Parameters (Optional, with best practice defaults):**
366366
- `min_connections` (int, default: 10): Default minimum connections (used when `pool_min_size` not provided)
367-
- `max_connections` (int, default: 40): Default maximum connections (used when `pool_max_size` not provided)
367+
- `max_connections` (int, default: 20): Default maximum connections (used when `pool_max_size` not provided)
368368
- `pool_min_size` (int, default: uses `min_connections` or 10): Minimum number of connections in the pool
369-
- `pool_max_size` (int, default: uses `max_connections` or 40): Maximum number of connections in the pool
369+
- `pool_max_size` (int, default: uses `max_connections` or 20): Maximum number of connections in the pool
370370
- `pool_max_lifetime` (float, default: 3600.0): Connection maximum lifetime in seconds (1 hour)
371371
- `pool_max_idle` (float, default: 600.0): Connection maximum idle time in seconds (10 minutes)
372372
- `pool_timeout` (float, default: 30.0): Timeout in seconds to get a connection from the pool
@@ -389,7 +389,7 @@ If you have a pre-configured psycopg3 ConnectionPool object, you can pass it dir
389389
**Important Notes:**
390390
- If using individual parameters, `user` is required
391391
- Connection pool defaults (`min_connections`, `max_connections`) should be specified in the vector store config JSON
392-
- The plugin automatically sets `minconn` and `maxconn` based on `min_connections`/`max_connections` in config (or defaults: 10 and 40)
392+
- The plugin automatically sets `minconn` and `maxconn` based on `min_connections`/`max_connections` in config (or defaults: 10 and 20)
393393
- **Production recommendation**: Set `max_connections` to match `max_concurrent_memory_operations` for optimal performance
394394
- Parameter priority: `connection_pool` > `connection_string` > individual parameters
395395
- If you provide both `connection_string` and individual parameters, `connection_string` takes precedence
@@ -936,7 +936,7 @@ Use `get_user_checkpoint` to inspect the extraction checkpoint for a user, optio
936936

937937
- **Read Operations** (Search/Get/Get_All/History):
938938
- Wait for results and return actual data
939-
- **Timeout protection**: All async read operations have timeout mechanisms (default: 30s, configurable)
939+
- **Timeout protection**: All async read operations have timeout mechanisms (default: 5s, configurable)
940940
- On timeout or error: logs event, cancels background tasks, returns default/empty results
941941

942942
### Sync Mode (`async_mode=false`)
@@ -1147,7 +1147,7 @@ For detailed upgrade instructions and field mapping, see [README.md - Upgrade Gu
11471147
- **Cause**: Invalid or unset concurrency parameter values (cannot be converted to positive integers)
11481148
- **Solution**:
11491149
- Check logs for specific warning messages indicating which parameter has an invalid value
1150-
- Ensure concurrency parameters are positive integers (minimum: 1, default: 40)
1150+
- Ensure concurrency parameters are positive integers (minimum: 1, default: 20)
11511151
- Configure `max_concurrent_memory_operations` to control concurrency for all operations
11521152
- See [Performance Parameters](#step-3-configure-performance-parameters-optional-recommended-for-production) for detailed configuration logic
11531153

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ Note: `extract_long_term_memory` uses `conversations_limit` as the per-user tota
222222
- `local_graph_db_json_secret` (was `local_graph_db_json`, optional)
223223
- `local_reranker_json_secret` (was `local_reranker_json`, optional)
224224
- **Important**: If you previously used `pgvector_min_connections` and `pgvector_max_connections` credential fields, you must now configure them in the `local_vector_db_json_secret` JSON config:
225-
- Add `"minconn": 10` and `"maxconn": 40` to your pgvector config JSON (see [CONFIG.md](https://github.com/beersoccer/mem0_dify_plugin/blob/main/CONFIG.md#vector-store-configuration-local_vector_db_json_secret) for examples)
225+
- Add `"minconn": 10` and `"maxconn": 20` to your pgvector config JSON (or set `maxconn` to match your `max_concurrent_memory_operations`, default: 20). See [CONFIG.md](https://github.com/beersoccer/mem0_dify_plugin/blob/main/CONFIG.md#vector-store-configuration-local_vector_db_json_secret) for examples.
226226
- These fields are no longer available as separate credential fields
227227
- Use the same configuration values you backed up in step 1
228228
- Save the configuration
@@ -243,7 +243,7 @@ Note: `extract_long_term_memory` uses `conversations_limit` as the per-user tota
243243
**New Features:**
244244
- **Dynamic Log Level**: You can now change log level (INFO/DEBUG/WARNING/ERROR) in plugin credentials without redeployment
245245
- **Request Tracing**: All tools now support `run_id` parameter for better call chain tracking (recommended to use Dify's `workflow_run_id`)
246-
- **Timeout Optimization**: Read operation timeout reduced to 15 seconds for better responsiveness
246+
- **Timeout Optimization**: Read operation timeout is tuned for responsiveness (current default: 5s, configurable per tool)
247247

248248
### Upgrading from v0.1.3
249249

performance/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ locust -f performance/locustfile.py --host=http://localhost
2323
### Headless Mode
2424

2525
```bash
26-
# Run without web UI
27-
locust -f performance/locustfile.py \
26+
# Run without web UI (override user count)
27+
DIFY_USER_COUNT=10 locust -f performance/locustfile.py \
2828
--host=http://localhost \
2929
--users 10 \
3030
--spawn-rate 2 \
@@ -60,7 +60,7 @@ DIFY_API_KEY="<your-dify-api-key>"
6060
DIFY_BASE_URL="http://<your-dify-host>/v1" # Base URL for remote testing (overrides --host if set)
6161
DIFY_ENDPOINT="/chat-messages"
6262
DIFY_QUERY="<your-custom-query>"
63-
DIFY_USER_ID="<user_a>" # Single user, or comma-separated list: "<user_a>,<user_b>"
63+
DIFY_USER_COUNT=5 # 参与测试的用户数量,实际用户 ID 为 user1..userN(默认 5)
6464
DIFY_RESPONSE_MODE="streaming"
6565
DIFY_MIN_TURNS=3 # Minimum number of follow-up conversation turns (default: 3)
6666
DIFY_MAX_TURNS=5 # Maximum number of follow-up conversation turns (default: 5)
@@ -70,7 +70,7 @@ The script will automatically load variables from `performance/.env` if it exist
7070

7171
**Note**:
7272
- The conversation will have 3-5 follow-up turns (randomly selected) after the initial message. This simulates multi-turn conversations that can trigger long-term memory extraction.
73-
- If `DIFY_USER_ID` contains multiple users (comma-separated), each request will randomly select one user from the list. This allows testing with different user contexts.
73+
- If `DIFY_USER_COUNT` is an integer N, each request will randomly select one user from user1..userN. This allows testing with different user contexts.
7474
- Use a base URL ending with `/v1` and endpoints starting with `/chat-messages` and `/messages/...` to match the Dify API structure.
7575

7676
**Option 2: Set environment variables directly**
@@ -80,7 +80,7 @@ DIFY_API_KEY='<your-dify-api-key>' \
8080
DIFY_BASE_URL='http://<your-dify-host>/v1' \
8181
DIFY_ENDPOINT='/chat-messages' \
8282
DIFY_QUERY='<your-custom-query>' \
83-
DIFY_USER_ID='<user_a>' \
83+
DIFY_USER_COUNT=10 \
8484
DIFY_RESPONSE_MODE='streaming' \
8585
DIFY_MIN_TURNS=3 \
8686
DIFY_MAX_TURNS=5 \

performance/locustfile.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
DIFY_API_KEY='key' \
3636
DIFY_ENDPOINT='/chat-messages' \
3737
DIFY_QUERY='Your custom query' \
38-
DIFY_USER_ID='test_user' \
38+
DIFY_USER_COUNT='10' \
3939
DIFY_RESPONSE_MODE='streaming' \
4040
locust -f performance/locustfile.py --host=http://localhost
4141
"""
@@ -51,6 +51,7 @@
5151

5252
from dotenv import load_dotenv
5353
from locust import HttpUser, TaskSet, between, events, task
54+
from user_ids import build_user_ids
5455

5556
# Load environment variables from .env file in performance directory
5657
env_file = Path(__file__).parent / ".env"
@@ -91,11 +92,9 @@ def on_start(self) -> None:
9192
# Configurable payload template
9293
self.response_mode = os.getenv("DIFY_RESPONSE_MODE", "streaming")
9394

94-
# Parse user_id(s) - can be comma-separated list for random selection
95-
user_id_str = os.getenv("DIFY_USER_ID", "test_user")
96-
self.user_ids = [uid.strip() for uid in user_id_str.split(",") if uid.strip()]
97-
if not self.user_ids:
98-
self.user_ids = ["test_user"]
95+
# Parse user_id(s) - integer count generates user1..userN
96+
user_count_str = os.getenv("DIFY_USER_COUNT", "5")
97+
self.user_ids = build_user_ids(user_count_str, default_user="test_user")
9998

10099
# Multi-turn conversation settings
101100
# min_turns and max_turns define the range for follow-up conversation rounds

performance/user_ids.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from __future__ import annotations
2+
3+
4+
def build_user_ids(raw_value: str, default_user: str = "test_user") -> list[str]:
5+
"""Build a user id list from env value.
6+
7+
- If raw_value is an integer N, generate user1..userN.
8+
- Otherwise, treat raw_value as a comma-separated list.
9+
"""
10+
normalized = (raw_value or "").strip()
11+
if not normalized:
12+
return [default_user]
13+
14+
if normalized.isdigit():
15+
count = int(normalized)
16+
if count <= 0:
17+
return ["user1"]
18+
return [f"user{i}" for i in range(1, count + 1)]
19+
20+
user_ids = [uid.strip() for uid in normalized.split(",") if uid.strip()]
21+
return user_ids or [default_user]
22+

provider/mem0ai.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,10 @@ def _validate_credentials(self, credentials: dict[str, Any]) -> None:
7171

7272
logger.debug("Validating Mem0 provider credentials")
7373

74-
# Use a longer timeout for validation to allow for vector DB initialization
75-
# (e.g., Pinecone connection setup, index creation, etc.)
76-
validation_timeout = READ_OPERATION_TIMEOUT * 2 # 30 seconds
74+
# Use a longer timeout for validation to allow for vector DB initialization.
75+
# NOTE: This should be independent from runtime read timeouts (e.g. search timeout=5s),
76+
# otherwise first-time credential validation can become flaky in real networks.
77+
validation_timeout = max(30, READ_OPERATION_TIMEOUT * 2)
7778

7879
try:
7980
async_mode = is_async_mode(credentials)

provider/mem0ai.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,13 @@ credentials_for_provider:
9494
max_concurrent_memory_operations:
9595
type: text-input
9696
required: false
97-
default: "40"
97+
default: "20"
9898
label:
9999
en_US: Max Concurrent Memory Operations
100100
zh_Hans: 最大并发记忆操作数
101101
help:
102-
en_US: "Maximum concurrent memory operations (default: 40). Must be a positive integer (>= 1). Applies to all operations including search/add/get/get_all/update/delete/delete_all/history."
103-
zh_Hans: "记忆操作的最大并发数(默认值:40)。必须为正整数(>= 1),涵盖所有操作包括 search/add/get/get_all/update/delete/delete_all/history。"
102+
en_US: "Maximum concurrent memory operations (default: 20). Must be a positive integer (>= 1). Applies to all operations including search/add/get/get_all/update/delete/delete_all/history."
103+
zh_Hans: "记忆操作的最大并发数(默认值:20)。必须为正整数(>= 1),涵盖所有操作包括 search/add/get/get_all/update/delete/delete_all/history。"
104104
log_level:
105105
type: select
106106
required: false
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from __future__ import annotations
2+
3+
from unittest.mock import MagicMock
4+
5+
import pytest
6+
7+
from tools.add_memory import AddMemoryTool
8+
9+
10+
def test_add_memory_async_overload_skips_enqueue(
11+
monkeypatch: pytest.MonkeyPatch,
12+
) -> None:
13+
"""When overloaded, add_memory should not enqueue a background task."""
14+
import tools.add_memory as add_mod
15+
16+
class FakeClient:
17+
max_ops = 10
18+
19+
def get_pending_tasks_count(self) -> int:
20+
return 999
21+
22+
def ensure_bg_loop(self): # pragma: no cover
23+
raise AssertionError("ensure_bg_loop should not be called on overload")
24+
25+
def track_bg_task(self, *_a, **_kw): # pragma: no cover
26+
raise AssertionError("track_bg_task should not be called on overload")
27+
28+
def _boom(*_a, **_kw): # pragma: no cover
29+
raise AssertionError("run_coroutine_threadsafe should not be called on overload")
30+
31+
monkeypatch.setattr(add_mod, "get_async_client", lambda _c: FakeClient())
32+
monkeypatch.setattr(add_mod.asyncio, "run_coroutine_threadsafe", _boom)
33+
34+
mock_runtime = MagicMock()
35+
mock_runtime.credentials = {} # async_mode defaults to True
36+
tool = AddMemoryTool(runtime=mock_runtime, session=MagicMock())
37+
38+
# Make message objects easy to assert on
39+
tool.create_json_message = lambda d: d # type: ignore[method-assign]
40+
tool.create_text_message = lambda t: t # type: ignore[method-assign]
41+
42+
msgs = list(
43+
tool._invoke(
44+
{
45+
"user_id": "u1",
46+
"user": "hi",
47+
"assistant": "",
48+
"timeout": 1,
49+
}
50+
)
51+
)
52+
53+
assert len(msgs) == 2
54+
assert isinstance(msgs[0], dict)
55+
assert msgs[0]["status"] == "OVERLOAD"
56+
assert "results" in msgs[0]
57+
assert isinstance(msgs[1], str)
58+
59+
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from __future__ import annotations
2+
3+
import time
4+
from unittest.mock import MagicMock
5+
6+
import pytest
7+
8+
9+
def test_execute_async_read_operation_rejects_before_enqueue_when_overloaded(
10+
monkeypatch: pytest.MonkeyPatch,
11+
) -> None:
12+
"""Overload guard must run BEFORE scheduling a new background task."""
13+
import utils.memory_tool_helpers as helpers
14+
15+
class FakeClient:
16+
max_ops = 10
17+
18+
def get_pending_tasks_count(self) -> int:
19+
return 999
20+
21+
def ensure_bg_loop(self): # pragma: no cover
22+
raise AssertionError("ensure_bg_loop should not be called on overload")
23+
24+
def track_bg_task(self, *_a, **_kw): # pragma: no cover
25+
raise AssertionError("track_bg_task should not be called on overload")
26+
27+
monkeypatch.setattr(helpers, "get_async_client", lambda _c: FakeClient())
28+
29+
def _boom(*_a, **_kw): # pragma: no cover
30+
raise AssertionError("run_coroutine_threadsafe should not be called on overload")
31+
32+
monkeypatch.setattr(helpers.asyncio, "run_coroutine_threadsafe", _boom)
33+
34+
tool = MagicMock()
35+
tool.runtime.credentials = {}
36+
37+
result, error_type = helpers.execute_async_read_operation(
38+
tool_instance=tool,
39+
operation=MagicMock(),
40+
operation_args=(),
41+
operation_kwargs={},
42+
timeout=1.0,
43+
request_id="req1",
44+
mode_str="async",
45+
start_time=time.time(),
46+
operation_name="search_memory(user_id=u1)",
47+
)
48+
49+
assert result is None
50+
assert error_type == "OVERLOAD"
51+
52+

0 commit comments

Comments
 (0)