Merge remote-tracking branch 'origin/master'
# Conflicts: # .beads/issues.jsonl # .beads/traces/bf-5xqk/metadata.json # .beads/traces/bf-5xqk/stdout.txt # .beads/traces/miroir-9dj/metadata.json # .beads/traces/miroir-9dj/stdout.txt # .beads/traces/miroir-cdo/metadata.json # .beads/traces/miroir-cdo/stdout.txt # .beads/traces/miroir-mkk/metadata.json # .beads/traces/miroir-mkk/stdout.txt # .beads/traces/miroir-r3j/metadata.json # .beads/traces/miroir-r3j/stdout.txt # .beads/traces/miroir-uhj/metadata.json # .beads/traces/miroir-uhj/stdout.txt # .beads/traces/miroir-zc2.6/metadata.json # .beads/traces/miroir-zc2.6/stdout.txt # .needle-predispatch-sha # Cargo.lock # charts/miroir/Chart.yaml # charts/miroir/templates/NOTES.txt # charts/miroir/templates/_helpers.tpl # charts/miroir/templates/redis-deployment.yaml # charts/miroir/templates/serviceaccount.yaml # charts/miroir/tests/README.md # charts/miroir/values.schema.json # charts/miroir/values.yaml # crates/miroir-core/Cargo.toml # crates/miroir-core/src/config.rs # crates/miroir-core/src/hedging.rs # crates/miroir-core/src/lib.rs # crates/miroir-core/src/merger.rs # crates/miroir-core/src/query_planner.rs # crates/miroir-core/src/raft_proto/mod.rs # crates/miroir-core/src/replica_selection.rs # crates/miroir-core/src/router.rs # crates/miroir-core/src/scatter.rs # crates/miroir-core/src/task_store/mod.rs # crates/miroir-core/src/task_store/redis.rs # crates/miroir-core/src/task_store/sqlite.rs # crates/miroir-core/src/topology.rs # crates/miroir-ctl/src/credentials.rs # crates/miroir-proxy/Cargo.toml # crates/miroir-proxy/src/auth.rs # crates/miroir-proxy/src/client.rs # crates/miroir-proxy/src/lib.rs # crates/miroir-proxy/src/main.rs # crates/miroir-proxy/src/middleware.rs # crates/miroir-proxy/src/routes/admin.rs # crates/miroir-proxy/src/routes/documents.rs # crates/miroir-proxy/src/routes/indexes.rs # crates/miroir-proxy/src/routes/search.rs # crates/miroir-proxy/src/routes/settings.rs # crates/miroir-proxy/src/routes/tasks.rs # docs/research/score-normalization-at-scale.md # notes/miroir-cdo.md # notes/miroir-r3j-final-verification.md # notes/miroir-r3j-verification.md # notes/miroir-r3j.1.md # notes/miroir-r3j.md # notes/miroir-zc2.1.md # notes/miroir-zc2.3.md # notes/miroir-zc2.4.md # notes/miroir-zc2.5.md
This commit is contained in:
commit
1f686c646b
216 changed files with 66821 additions and 9785 deletions
16
.beads/traces/bf-3gfw/metadata.json
Normal file
16
.beads/traces/bf-3gfw/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "bf-3gfw",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 1,
|
||||
"outcome": "failure",
|
||||
"duration_ms": 136129,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-08T19:26:08.544156983Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
0
.beads/traces/bf-3gfw/stderr.txt
Normal file
0
.beads/traces/bf-3gfw/stderr.txt
Normal file
2658
.beads/traces/bf-3gfw/stdout.txt
Normal file
2658
.beads/traces/bf-3gfw/stdout.txt
Normal file
File diff suppressed because one or more lines are too long
16
.beads/traces/bf-3lad/metadata.json
Normal file
16
.beads/traces/bf-3lad/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "bf-3lad",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 1,
|
||||
"outcome": "failure",
|
||||
"duration_ms": 179981,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-10T11:06:39.907809756Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
2
.beads/traces/bf-3lad/stderr.txt
Normal file
2
.beads/traces/bf-3lad/stderr.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
SessionEnd hook [/home/coding/.ccdash/hooks/session-end.sh] failed: /bin/sh: line 1: /home/coding/.ccdash/hooks/session-end.sh: cannot execute: required file not found
|
||||
|
||||
16
.beads/traces/bf-3lad/stdout.txt
Normal file
16
.beads/traces/bf-3lad/stdout.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{"type":"system","subtype":"hook_started","hook_id":"dbd28456-ae67-4d0e-ba6e-b266ec7f3554","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"294724a5-ddba-4153-b31a-63c3e6dd7165","session_id":"9201791e-959e-468c-9f35-3f363274e896"}
|
||||
{"type":"system","subtype":"hook_response","hook_id":"dbd28456-ae67-4d0e-ba6e-b266ec7f3554","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"/bin/sh: line 1: /home/coding/.ccdash/hooks/session-start.sh: cannot execute: required file not found\n","stdout":"","stderr":"/bin/sh: line 1: /home/coding/.ccdash/hooks/session-start.sh: cannot execute: required file not found\n","exit_code":127,"outcome":"error","uuid":"1d83198e-a1da-4c42-9927-bf6a32a933a1","session_id":"9201791e-959e-468c-9f35-3f363274e896"}
|
||||
{"type":"system","subtype":"init","cwd":"/home/coding/miroir","session_id":"9201791e-959e-468c-9f35-3f363274e896","tools":["Task","AskUserQuestion","Bash","CronCreate","CronDelete","CronList","Edit","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Glob","Grep","NotebookEdit","Read","ScheduleWakeup","Skill","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch","Write","mcp__claude_ai_Alphavantage__TOOL_CALL","mcp__claude_ai_Alphavantage__TOOL_GET","mcp__claude_ai_Alphavantage__TOOL_LIST","mcp__claude_ai_Gmail__authenticate","mcp__claude_ai_Gmail__complete_authentication","mcp__claude_ai_Google_Calendar__authenticate","mcp__claude_ai_Google_Calendar__complete_authentication","mcp__claude_ai_Google_Drive__authenticate","mcp__claude_ai_Google_Drive__complete_authentication"],"mcp_servers":[{"name":"claude.ai Alphavantage","status":"connected"},{"name":"claude.ai Google Calendar","status":"needs-auth"},{"name":"claude.ai Gmail","status":"needs-auth"},{"name":"claude.ai Google Drive","status":"needs-auth"}],"model":"glm-4.7","permissionMode":"bypassPermissions","slash_commands":["update-config","debug","simplify","batch","fewer-permission-prompts","loop","claude-api","clear","compact","context","heapdump","init","review","security-review","usage","insights","team-onboarding"],"apiKeySource":"none","claude_code_version":"2.1.138","output_style":"default","agents":["Explore","general-purpose","Plan","statusline-setup"],"skills":["update-config","debug","simplify","batch","fewer-permission-prompts","loop","claude-api"],"plugins":[],"analytics_disabled":true,"uuid":"bfbb2d9f-0887-4905-b028-428b1d67bbf5","memory_paths":{"auto":"/home/coding/.claude/projects/-home-coding-miroir/memory/"},"fast_mode_state":"off"}
|
||||
{"type":"system","subtype":"status","status":"requesting","uuid":"0b91a995-2e43-47c2-b2b1-945e1f011223","session_id":"9201791e-959e-468c-9f35-3f363274e896"}
|
||||
{"type":"system","subtype":"api_retry","attempt":1,"max_retries":10,"retry_delay_ms":594.126935420076,"error_status":null,"error":"unknown","session_id":"9201791e-959e-468c-9f35-3f363274e896","uuid":"fed130e2-257c-417a-9d75-251ee41c3777"}
|
||||
{"type":"system","subtype":"api_retry","attempt":2,"max_retries":10,"retry_delay_ms":1062.0535439076032,"error_status":null,"error":"unknown","session_id":"9201791e-959e-468c-9f35-3f363274e896","uuid":"2f18d2e7-ba94-4f58-9232-f92c3779369c"}
|
||||
{"type":"system","subtype":"api_retry","attempt":3,"max_retries":10,"retry_delay_ms":2132.401783390468,"error_status":null,"error":"unknown","session_id":"9201791e-959e-468c-9f35-3f363274e896","uuid":"7aa17d8b-7096-474b-bd69-3e68c9bc5ce6"}
|
||||
{"type":"system","subtype":"api_retry","attempt":4,"max_retries":10,"retry_delay_ms":4599.899047965057,"error_status":null,"error":"unknown","session_id":"9201791e-959e-468c-9f35-3f363274e896","uuid":"52844996-cbbe-4f91-baa8-6ea1a4d1f2d5"}
|
||||
{"type":"system","subtype":"api_retry","attempt":5,"max_retries":10,"retry_delay_ms":8296.40748059694,"error_status":null,"error":"unknown","session_id":"9201791e-959e-468c-9f35-3f363274e896","uuid":"48603337-d945-4ac7-9908-716cde4409e0"}
|
||||
{"type":"system","subtype":"api_retry","attempt":6,"max_retries":10,"retry_delay_ms":16239.273788222305,"error_status":null,"error":"unknown","session_id":"9201791e-959e-468c-9f35-3f363274e896","uuid":"6774c915-9d6c-4e10-b1d1-50ed6dab8f6f"}
|
||||
{"type":"system","subtype":"api_retry","attempt":7,"max_retries":10,"retry_delay_ms":33102.89590756301,"error_status":null,"error":"unknown","session_id":"9201791e-959e-468c-9f35-3f363274e896","uuid":"18848581-52df-4195-a49a-f781939e459d"}
|
||||
{"type":"system","subtype":"api_retry","attempt":8,"max_retries":10,"retry_delay_ms":36914.271258730005,"error_status":null,"error":"unknown","session_id":"9201791e-959e-468c-9f35-3f363274e896","uuid":"0cdb2c68-07cb-4f9f-86b4-158ba01043be"}
|
||||
{"type":"system","subtype":"api_retry","attempt":9,"max_retries":10,"retry_delay_ms":39458.44410609604,"error_status":null,"error":"unknown","session_id":"9201791e-959e-468c-9f35-3f363274e896","uuid":"0eea1ce9-4387-4d7d-945d-9796dd4e0de5"}
|
||||
{"type":"system","subtype":"api_retry","attempt":10,"max_retries":10,"retry_delay_ms":35524.58115524215,"error_status":null,"error":"unknown","session_id":"9201791e-959e-468c-9f35-3f363274e896","uuid":"5f35a325-f987-4128-9740-b3e7d7f794b4"}
|
||||
{"type":"assistant","message":{"id":"80acd61b-b207-4c22-bd26-5adbd06b6265","container":null,"model":"<synthetic>","role":"assistant","stop_details":null,"stop_reason":"stop_sequence","stop_sequence":"","type":"message","usage":{"input_tokens":0,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":null,"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":null,"iterations":null,"speed":null},"content":[{"type":"text","text":"API Error: Unable to connect to API (ConnectionRefused)"}],"context_management":null},"parent_tool_use_id":null,"session_id":"9201791e-959e-468c-9f35-3f363274e896","uuid":"e087ccb5-c868-47f0-b3b1-c696f75835a7","error":"unknown"}
|
||||
{"type":"result","subtype":"success","is_error":true,"api_error_status":null,"duration_ms":178916,"duration_api_ms":0,"num_turns":1,"result":"API Error: Unable to connect to API (ConnectionRefused)","stop_reason":"stop_sequence","session_id":"9201791e-959e-468c-9f35-3f363274e896","total_cost_usd":0,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"},"modelUsage":{},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"3da61640-0410-4b86-875f-de2f79054ff1"}
|
||||
16
.beads/traces/bf-4d9a/metadata.json
Normal file
16
.beads/traces/bf-4d9a/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "bf-4d9a",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 0,
|
||||
"outcome": "success",
|
||||
"duration_ms": 379153,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-08T19:29:59.500038883Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
0
.beads/traces/bf-4d9a/stderr.txt
Normal file
0
.beads/traces/bf-4d9a/stderr.txt
Normal file
3301
.beads/traces/bf-4d9a/stdout.txt
Normal file
3301
.beads/traces/bf-4d9a/stdout.txt
Normal file
File diff suppressed because one or more lines are too long
16
.beads/traces/bf-4w08/metadata.json
Normal file
16
.beads/traces/bf-4w08/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "bf-4w08",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 1,
|
||||
"outcome": "failure",
|
||||
"duration_ms": 174434,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-10T11:03:38.699815594Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
2
.beads/traces/bf-4w08/stderr.txt
Normal file
2
.beads/traces/bf-4w08/stderr.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
SessionEnd hook [/home/coding/.ccdash/hooks/session-end.sh] failed: /bin/sh: line 1: /home/coding/.ccdash/hooks/session-end.sh: cannot execute: required file not found
|
||||
|
||||
16
.beads/traces/bf-4w08/stdout.txt
Normal file
16
.beads/traces/bf-4w08/stdout.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{"type":"system","subtype":"hook_started","hook_id":"228dd5de-7520-4719-b873-270d6be8b095","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"5abf2738-6684-4a85-a917-1bf4de839cef","session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d"}
|
||||
{"type":"system","subtype":"hook_response","hook_id":"228dd5de-7520-4719-b873-270d6be8b095","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"/bin/sh: line 1: /home/coding/.ccdash/hooks/session-start.sh: cannot execute: required file not found\n","stdout":"","stderr":"/bin/sh: line 1: /home/coding/.ccdash/hooks/session-start.sh: cannot execute: required file not found\n","exit_code":127,"outcome":"error","uuid":"2dca8d9c-3e57-4e5b-8814-122a65f31ba5","session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d"}
|
||||
{"type":"system","subtype":"init","cwd":"/home/coding/miroir","session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d","tools":["Task","AskUserQuestion","Bash","CronCreate","CronDelete","CronList","Edit","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Glob","Grep","NotebookEdit","Read","ScheduleWakeup","Skill","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch","Write","mcp__claude_ai_Alphavantage__TOOL_CALL","mcp__claude_ai_Alphavantage__TOOL_GET","mcp__claude_ai_Alphavantage__TOOL_LIST","mcp__claude_ai_Gmail__authenticate","mcp__claude_ai_Gmail__complete_authentication","mcp__claude_ai_Google_Calendar__authenticate","mcp__claude_ai_Google_Calendar__complete_authentication","mcp__claude_ai_Google_Drive__authenticate","mcp__claude_ai_Google_Drive__complete_authentication"],"mcp_servers":[{"name":"claude.ai Alphavantage","status":"connected"},{"name":"claude.ai Google Calendar","status":"needs-auth"},{"name":"claude.ai Gmail","status":"needs-auth"},{"name":"claude.ai Google Drive","status":"needs-auth"}],"model":"glm-4.7","permissionMode":"bypassPermissions","slash_commands":["update-config","debug","simplify","batch","fewer-permission-prompts","loop","claude-api","clear","compact","context","heapdump","init","review","security-review","usage","insights","team-onboarding"],"apiKeySource":"none","claude_code_version":"2.1.138","output_style":"default","agents":["Explore","general-purpose","Plan","statusline-setup"],"skills":["update-config","debug","simplify","batch","fewer-permission-prompts","loop","claude-api"],"plugins":[],"analytics_disabled":true,"uuid":"2d0b42a2-63b3-4acb-8a37-72c57262e5df","memory_paths":{"auto":"/home/coding/.claude/projects/-home-coding-miroir/memory/"},"fast_mode_state":"off"}
|
||||
{"type":"system","subtype":"status","status":"requesting","uuid":"ec8b206b-0d1f-4f0c-8f3a-ed709d9ef5bd","session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d"}
|
||||
{"type":"system","subtype":"api_retry","attempt":1,"max_retries":10,"retry_delay_ms":585.065406262977,"error_status":null,"error":"unknown","session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d","uuid":"fe366dff-9dc9-44cc-8a59-2229fd12cc53"}
|
||||
{"type":"system","subtype":"api_retry","attempt":2,"max_retries":10,"retry_delay_ms":1231.1220782947023,"error_status":null,"error":"unknown","session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d","uuid":"82a7f0dd-1f08-4242-80c3-47a187702c77"}
|
||||
{"type":"system","subtype":"api_retry","attempt":3,"max_retries":10,"retry_delay_ms":2291.0281267998225,"error_status":null,"error":"unknown","session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d","uuid":"e4c372c0-6b87-422e-8a47-f41484e64765"}
|
||||
{"type":"system","subtype":"api_retry","attempt":4,"max_retries":10,"retry_delay_ms":4821.756914640311,"error_status":null,"error":"unknown","session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d","uuid":"230ba3a2-6846-429d-b51c-a50e6d8330b9"}
|
||||
{"type":"system","subtype":"api_retry","attempt":5,"max_retries":10,"retry_delay_ms":9915.794402170704,"error_status":null,"error":"unknown","session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d","uuid":"f1189243-1005-4ee8-91f8-36c035f1fdab"}
|
||||
{"type":"system","subtype":"api_retry","attempt":6,"max_retries":10,"retry_delay_ms":19728.754838667675,"error_status":null,"error":"unknown","session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d","uuid":"a8c97b8a-2d9c-429f-9c19-6c2feecdb337"}
|
||||
{"type":"system","subtype":"api_retry","attempt":7,"max_retries":10,"retry_delay_ms":32056.507439193858,"error_status":null,"error":"unknown","session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d","uuid":"9136a235-a9c5-42a3-b1de-e58cca544533"}
|
||||
{"type":"system","subtype":"api_retry","attempt":8,"max_retries":10,"retry_delay_ms":32813.34022624039,"error_status":null,"error":"unknown","session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d","uuid":"7b28c2ae-3344-4e7c-b4a9-aa14c1b3db5d"}
|
||||
{"type":"system","subtype":"api_retry","attempt":9,"max_retries":10,"retry_delay_ms":33948.93778320631,"error_status":null,"error":"unknown","session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d","uuid":"99928feb-6c01-41cd-9fa4-43d3d287c07c"}
|
||||
{"type":"system","subtype":"api_retry","attempt":10,"max_retries":10,"retry_delay_ms":34912.970971536546,"error_status":null,"error":"unknown","session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d","uuid":"8c9dc41b-3083-4fe1-a314-81f9939e2314"}
|
||||
{"type":"assistant","message":{"id":"338ba0ae-2ab4-4de3-8738-5108f330b6ae","container":null,"model":"<synthetic>","role":"assistant","stop_details":null,"stop_reason":"stop_sequence","stop_sequence":"","type":"message","usage":{"input_tokens":0,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":null,"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":null,"iterations":null,"speed":null},"content":[{"type":"text","text":"API Error: Unable to connect to API (ConnectionRefused)"}],"context_management":null},"parent_tool_use_id":null,"session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d","uuid":"9898b089-6288-4643-92e1-8804d4564f4a","error":"unknown"}
|
||||
{"type":"result","subtype":"success","is_error":true,"api_error_status":null,"duration_ms":173380,"duration_api_ms":0,"num_turns":1,"result":"API Error: Unable to connect to API (ConnectionRefused)","stop_reason":"stop_sequence","session_id":"6ab1d9c1-4fe7-4b8a-a764-9589e5d21e7d","total_cost_usd":0,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"},"modelUsage":{},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"2a9e3f72-dc99-40ef-a9e8-a4f9042d9ccd"}
|
||||
16
.beads/traces/bf-5gej/metadata.json
Normal file
16
.beads/traces/bf-5gej/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "bf-5gej",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 1,
|
||||
"outcome": "failure",
|
||||
"duration_ms": 8808,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-08T19:23:58.154835111Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
0
.beads/traces/bf-5gej/stderr.txt
Normal file
0
.beads/traces/bf-5gej/stderr.txt
Normal file
4
.beads/traces/bf-5gej/stdout.txt
Normal file
4
.beads/traces/bf-5gej/stdout.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{"type":"system","subtype":"init","cwd":"/home/coding/miroir","session_id":"97bb21d6-ffa1-4fbc-9edf-4bd9141c63d8","tools":["Task","AskUserQuestion","Bash","CronCreate","CronDelete","CronList","Edit","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Glob","Grep","NotebookEdit","Read","ScheduleWakeup","Skill","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch","Write"],"mcp_servers":[],"model":"glm-4.7","permissionMode":"bypassPermissions","slash_commands":["update-config","debug","simplify","batch","fewer-permission-prompts","loop","claude-api","clear","compact","context","heapdump","init","review","security-review","usage","insights","team-onboarding"],"apiKeySource":"none","claude_code_version":"2.1.133","output_style":"default","agents":["Explore","general-purpose","Plan","statusline-setup"],"skills":["update-config","debug","simplify","batch","fewer-permission-prompts","loop","claude-api"],"plugins":[],"analytics_disabled":true,"uuid":"96fb59e9-4d4f-43c4-8d23-e275b9ef2ab4","memory_paths":{"auto":"/home/coding/.claude/projects/-home-coding-miroir/memory/"},"fast_mode_state":"off"}
|
||||
{"type":"system","subtype":"status","status":"requesting","uuid":"4c2cdaca-3bbd-44d4-8dc3-283b82cb5acc","session_id":"97bb21d6-ffa1-4fbc-9edf-4bd9141c63d8"}
|
||||
{"type":"assistant","message":{"id":"29d19128-e199-4f9b-a8d2-443ababf2fc8","container":null,"model":"<synthetic>","role":"assistant","stop_reason":"stop_sequence","stop_sequence":"","type":"message","usage":{"input_tokens":0,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":null,"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":null,"iterations":null,"speed":null},"content":[{"type":"text","text":"API Error: API returned an empty or malformed response (HTTP 200) — check for a proxy or gateway intercepting the request"}],"context_management":null},"parent_tool_use_id":null,"session_id":"97bb21d6-ffa1-4fbc-9edf-4bd9141c63d8","uuid":"f4d6edd6-e4b3-42b1-9577-b1e90d5504aa","error":"unknown"}
|
||||
{"type":"result","subtype":"success","is_error":true,"api_error_status":null,"duration_ms":8547,"duration_api_ms":1508,"num_turns":1,"result":"API Error: API returned an empty or malformed response (HTTP 200) — check for a proxy or gateway intercepting the request","stop_reason":"stop_sequence","session_id":"97bb21d6-ffa1-4fbc-9edf-4bd9141c63d8","total_cost_usd":0.003705,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"},"modelUsage":{"glm-4.7":{"inputTokens":641,"outputTokens":20,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.003705,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"f4536a1e-22a1-4333-9e29-49bbb230eac6"}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
SessionEnd hook [/home/coding/.ccdash/hooks/session-end.sh] failed: /bin/sh: line 1: /home/coding/.ccdash/hooks/session-end.sh: cannot execute: required file not found
|
||||
|
||||
16
.beads/traces/bf-5xs1/metadata.json
Normal file
16
.beads/traces/bf-5xs1/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "bf-5xs1",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 0,
|
||||
"outcome": "success",
|
||||
"duration_ms": 180918,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-08T19:26:44.263084349Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
0
.beads/traces/bf-5xs1/stderr.txt
Normal file
0
.beads/traces/bf-5xs1/stderr.txt
Normal file
2610
.beads/traces/bf-5xs1/stdout.txt
Normal file
2610
.beads/traces/bf-5xs1/stdout.txt
Normal file
File diff suppressed because one or more lines are too long
16
.beads/traces/bf-dijm/metadata.json
Normal file
16
.beads/traces/bf-dijm/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "bf-dijm",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 1,
|
||||
"outcome": "failure",
|
||||
"duration_ms": 176225,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-08T19:26:42.714634080Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
0
.beads/traces/bf-dijm/stderr.txt
Normal file
0
.beads/traces/bf-dijm/stderr.txt
Normal file
3657
.beads/traces/bf-dijm/stdout.txt
Normal file
3657
.beads/traces/bf-dijm/stdout.txt
Normal file
File diff suppressed because one or more lines are too long
16
.beads/traces/bf-jap1/metadata.json
Normal file
16
.beads/traces/bf-jap1/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "bf-jap1",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 0,
|
||||
"outcome": "success",
|
||||
"duration_ms": 117752,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-08T19:25:53.105343822Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
0
.beads/traces/bf-jap1/stderr.txt
Normal file
0
.beads/traces/bf-jap1/stderr.txt
Normal file
1853
.beads/traces/bf-jap1/stdout.txt
Normal file
1853
.beads/traces/bf-jap1/stdout.txt
Normal file
File diff suppressed because one or more lines are too long
16
.beads/traces/miroir-cdo.1/metadata.json
Normal file
16
.beads/traces/miroir-cdo.1/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "miroir-cdo.1",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 1,
|
||||
"outcome": "failure",
|
||||
"duration_ms": 202124,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-09T14:44:26.162212833Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
2
.beads/traces/miroir-cdo.1/stderr.txt
Normal file
2
.beads/traces/miroir-cdo.1/stderr.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
SessionEnd hook [/home/coding/.ccdash/hooks/session-end.sh] failed: /bin/sh: line 1: /home/coding/.ccdash/hooks/session-end.sh: cannot execute: required file not found
|
||||
|
||||
2564
.beads/traces/miroir-cdo.1/stdout.txt
Normal file
2564
.beads/traces/miroir-cdo.1/stdout.txt
Normal file
File diff suppressed because one or more lines are too long
16
.beads/traces/miroir-cdo.2/metadata.json
Normal file
16
.beads/traces/miroir-cdo.2/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "miroir-cdo.2",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 1,
|
||||
"outcome": "failure",
|
||||
"duration_ms": 204006,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-09T14:45:05.629528275Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
2
.beads/traces/miroir-cdo.2/stderr.txt
Normal file
2
.beads/traces/miroir-cdo.2/stderr.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
SessionEnd hook [/home/coding/.ccdash/hooks/session-end.sh] failed: /bin/sh: line 1: /home/coding/.ccdash/hooks/session-end.sh: cannot execute: required file not found
|
||||
|
||||
2471
.beads/traces/miroir-cdo.2/stdout.txt
Normal file
2471
.beads/traces/miroir-cdo.2/stdout.txt
Normal file
File diff suppressed because one or more lines are too long
16
.beads/traces/miroir-cdo.4/metadata.json
Normal file
16
.beads/traces/miroir-cdo.4/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "miroir-cdo.4",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 1,
|
||||
"outcome": "failure",
|
||||
"duration_ms": 196499,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-09T15:01:23.134387857Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
2
.beads/traces/miroir-cdo.4/stderr.txt
Normal file
2
.beads/traces/miroir-cdo.4/stderr.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
SessionEnd hook [/home/coding/.ccdash/hooks/session-end.sh] failed: /bin/sh: line 1: /home/coding/.ccdash/hooks/session-end.sh: cannot execute: required file not found
|
||||
|
||||
2070
.beads/traces/miroir-cdo.4/stdout.txt
Normal file
2070
.beads/traces/miroir-cdo.4/stdout.txt
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,2 @@
|
|||
SessionEnd hook [/home/coding/.ccdash/hooks/session-end.sh] failed: /bin/sh: line 1: /home/coding/.ccdash/hooks/session-end.sh: cannot execute: required file not found
|
||||
|
||||
16
.beads/traces/miroir-qon/metadata.json
Normal file
16
.beads/traces/miroir-qon/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "miroir-qon",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 1,
|
||||
"outcome": "failure",
|
||||
"duration_ms": 253396,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-09T14:13:32.501636903Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
2
.beads/traces/miroir-qon/stderr.txt
Normal file
2
.beads/traces/miroir-qon/stderr.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
SessionEnd hook [/home/coding/.ccdash/hooks/session-end.sh] failed: /bin/sh: line 1: /home/coding/.ccdash/hooks/session-end.sh: cannot execute: required file not found
|
||||
|
||||
2120
.beads/traces/miroir-qon/stdout.txt
Normal file
2120
.beads/traces/miroir-qon/stdout.txt
Normal file
File diff suppressed because one or more lines are too long
16
.beads/traces/miroir-r3j.1/metadata.json
Normal file
16
.beads/traces/miroir-r3j.1/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "miroir-r3j.1",
|
||||
"agent": "claude-code-glm-5-1",
|
||||
"provider": "zai",
|
||||
"model": "glm-5.1",
|
||||
"exit_code": 1,
|
||||
"outcome": "failure",
|
||||
"duration_ms": 459142,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-13T23:12:06.004848388Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
2
.beads/traces/miroir-r3j.1/stderr.txt
Normal file
2
.beads/traces/miroir-r3j.1/stderr.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
SessionEnd hook [/home/coding/.ccdash/hooks/session-end.sh] failed: /bin/sh: line 1: /home/coding/.ccdash/hooks/session-end.sh: cannot execute: required file not found
|
||||
|
||||
3049
.beads/traces/miroir-r3j.1/stdout.txt
Normal file
3049
.beads/traces/miroir-r3j.1/stdout.txt
Normal file
File diff suppressed because one or more lines are too long
16
.beads/traces/miroir-r3j.5/metadata.json
Normal file
16
.beads/traces/miroir-r3j.5/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "miroir-r3j.5",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 1,
|
||||
"outcome": "failure",
|
||||
"duration_ms": 198584,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-09T09:45:59.659417710Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
2
.beads/traces/miroir-r3j.5/stderr.txt
Normal file
2
.beads/traces/miroir-r3j.5/stderr.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
SessionEnd hook [/home/coding/.ccdash/hooks/session-end.sh] failed: /bin/sh: line 1: /home/coding/.ccdash/hooks/session-end.sh: cannot execute: required file not found
|
||||
|
||||
3889
.beads/traces/miroir-r3j.5/stdout.txt
Normal file
3889
.beads/traces/miroir-r3j.5/stdout.txt
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,2 @@
|
|||
SessionEnd hook [/home/coding/.ccdash/hooks/session-end.sh] failed: /bin/sh: line 1: /home/coding/.ccdash/hooks/session-end.sh: cannot execute: required file not found
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
SessionEnd hook [/home/coding/.ccdash/hooks/session-end.sh] failed: /bin/sh: line 1: /home/coding/.ccdash/hooks/session-end.sh: cannot execute: required file not found
|
||||
|
||||
16
.beads/traces/miroir-zc2.3/metadata.json
Normal file
16
.beads/traces/miroir-zc2.3/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "miroir-zc2.3",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 1,
|
||||
"outcome": "failure",
|
||||
"duration_ms": 157670,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-08T19:27:37.057396903Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
0
.beads/traces/miroir-zc2.3/stderr.txt
Normal file
0
.beads/traces/miroir-zc2.3/stderr.txt
Normal file
3645
.beads/traces/miroir-zc2.3/stdout.txt
Normal file
3645
.beads/traces/miroir-zc2.3/stdout.txt
Normal file
File diff suppressed because one or more lines are too long
16
.beads/traces/miroir-zc2.4/metadata.json
Normal file
16
.beads/traces/miroir-zc2.4/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "miroir-zc2.4",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 1,
|
||||
"outcome": "failure",
|
||||
"duration_ms": 285213,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-08T19:30:38.669397737Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
0
.beads/traces/miroir-zc2.4/stderr.txt
Normal file
0
.beads/traces/miroir-zc2.4/stderr.txt
Normal file
2026
.beads/traces/miroir-zc2.4/stdout.txt
Normal file
2026
.beads/traces/miroir-zc2.4/stdout.txt
Normal file
File diff suppressed because one or more lines are too long
16
.beads/traces/miroir-zc2.5/metadata.json
Normal file
16
.beads/traces/miroir-zc2.5/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "miroir-zc2.5",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 1,
|
||||
"outcome": "failure",
|
||||
"duration_ms": 208889,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-08T19:29:37.846094488Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
0
.beads/traces/miroir-zc2.5/stderr.txt
Normal file
0
.beads/traces/miroir-zc2.5/stderr.txt
Normal file
7701
.beads/traces/miroir-zc2.5/stdout.txt
Normal file
7701
.beads/traces/miroir-zc2.5/stdout.txt
Normal file
File diff suppressed because one or more lines are too long
16
.beads/traces/miroir-zc2/metadata.json
Normal file
16
.beads/traces/miroir-zc2/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "miroir-zc2",
|
||||
"agent": "claude-code-glm-4.7",
|
||||
"provider": "zai",
|
||||
"model": "glm-4.7",
|
||||
"exit_code": 0,
|
||||
"outcome": "success",
|
||||
"duration_ms": 106390,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-08T19:24:58.966495904Z",
|
||||
"trace_format": "claude_json",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
0
.beads/traces/miroir-zc2/stderr.txt
Normal file
0
.beads/traces/miroir-zc2/stderr.txt
Normal file
1751
.beads/traces/miroir-zc2/stdout.txt
Normal file
1751
.beads/traces/miroir-zc2/stdout.txt
Normal file
File diff suppressed because one or more lines are too long
553
Cargo.lock
generated
553
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -7,7 +7,7 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/jedarden/miroir"
|
||||
rust-version = "1.87"
|
||||
rust-version = "1.88"
|
||||
|
||||
[workspace.dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
|
|
|||
|
|
@ -1,34 +1,20 @@
|
|||
apiVersion: v2
|
||||
name: miroir
|
||||
description: Multi-node Index Replication Orchestrator for Meilisearch
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: 0.1.0
|
||||
description: RAID-like sharding and HA for Meilisearch Community Edition
|
||||
appVersion: "0.1.0"
|
||||
keywords:
|
||||
- search
|
||||
- meilisearch
|
||||
- search
|
||||
- replication
|
||||
- sharding
|
||||
- kubernetes
|
||||
home: https://github.com/jedarden/miroir
|
||||
sources:
|
||||
- https://github.com/jedarden/miroir
|
||||
maintainers:
|
||||
- name: jedarden
|
||||
url: https://github.com/jedarden
|
||||
icon: https://raw.githubusercontent.com/meilisearch/meilisearch/main/assets/logo.svg
|
||||
email: dev@example.com
|
||||
annotations:
|
||||
artifacthub.io/category: database
|
||||
artifacthub.io/license: MIT
|
||||
artifacthub.io/links: |
|
||||
- name: Documentation
|
||||
url: https://github.com/jedarden/miroir
|
||||
artifacthub.io/operator: "false"
|
||||
artifacthub.io/prerelease: "false"
|
||||
kubeVersion: ">=1.25.0-0"
|
||||
# Prometheus Adapter dependency (plan §14.4)
|
||||
# Required for HPA custom metrics: miroir_requests_in_flight, miroir_background_queue_depth
|
||||
dependencies:
|
||||
- name: prometheus-adapter
|
||||
version: "4.x"
|
||||
repository: "https://prometheus-community.github.io/helm-charts"
|
||||
condition: prometheusAdapter.enabled
|
||||
alias: prometheusAdapter
|
||||
|
|
|
|||
|
|
@ -1,25 +1,42 @@
|
|||
Miroir has been installed!
|
||||
Miroir has been deployed successfully!
|
||||
|
||||
Get the service URL:
|
||||
export POD_NAME=$(kubectl get pods -n {{ .Release.Namespace }} -l "app.kubernetes.io/instance={{ .Release.Name }},app.kubernetes.io/component=miroir" -o jsonpath="{.items[0].metadata.name}")
|
||||
kubectl port-forward -n {{ .Release.Namespace }} "$POD_NAME" 7700:7700
|
||||
{{- if .Values.miroir.ingress.enabled }}
|
||||
|
||||
Then visit http://localhost:7700/health to verify the proxy is running.
|
||||
|
||||
Components deployed:
|
||||
- Miroir proxy ({{ .Values.miroir.replicas }} replica(s))
|
||||
- Meilisearch ({{ .Values.meilisearch.replicas }} node(s))
|
||||
{{- if eq (include "miroir.redisEnabled" .) "true" }}
|
||||
- Redis (task store)
|
||||
{{- else }}
|
||||
- SQLite task store (embedded)
|
||||
Your Miroir instance is accessible at:
|
||||
{{- range .Values.miroir.ingress.hosts }}
|
||||
http{{ if $.Values.miroir.ingress.tls }}s{{ end }}://{{ .host }}
|
||||
{{- end }}
|
||||
|
||||
!!! PRODUCTION UPGRADE PATH !!!
|
||||
These defaults are for dev/CI (single-pod evaluation). For production, override:
|
||||
miroir.replicas=2+
|
||||
miroir.replicationFactor=2
|
||||
miroir.replicaGroups=2
|
||||
taskStore.backend=redis
|
||||
redis.enabled=true
|
||||
hpa.enabled=true
|
||||
{{- else if contains "NodePort" .Values.miroir.service.type }}
|
||||
|
||||
Get the NodePort by running:
|
||||
export NODE_PORT=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "miroir.fullname" . }} -o jsonpath='{.spec.ports[0].nodePort}')
|
||||
echo "URL: http://$NODE_PORT"
|
||||
|
||||
{{- else if contains "LoadBalancer" .Values.miroir.service.type }}
|
||||
|
||||
Get the LoadBalancer IP by running:
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "miroir.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
echo "URL: http://$SERVICE_IP:{{ .Values.miroir.service.port }}"
|
||||
|
||||
{{- else if contains "ClusterIP" .Values.miroir.service.type }}
|
||||
|
||||
Get the application URL by running these commands in the same shell:
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "miroir.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
echo "Visit http://127.0.0.1:7700 to use your application"
|
||||
kubectl port-forward --namespace {{ .Release.Namespace }} $POD_NAME 7700:7700
|
||||
|
||||
{{- end }}
|
||||
|
||||
{{- if eq .Values.miroir.taskStore.backend "redis" }}
|
||||
|
||||
Redis is deployed and accessible at:
|
||||
{{ include "miroir.fullname" . }}-redis:6379
|
||||
|
||||
{{- end }}
|
||||
|
||||
To verify the deployment, run:
|
||||
kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "miroir.name" . }},app.kubernetes.io/instance={{ .Release.Name }}"
|
||||
|
||||
For more information, see the documentation at:
|
||||
https://github.com/jedarden/miroir
|
||||
|
|
|
|||
|
|
@ -21,11 +21,18 @@ Create a default fully qualified app name.
|
|||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "miroir.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "miroir.labels" -}}
|
||||
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
helm.sh/chart: {{ include "miroir.chart" . }}
|
||||
{{ include "miroir.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
|
|
@ -42,7 +49,14 @@ app.kubernetes.io/instance: {{ .Release.Name }}
|
|||
{{- end }}
|
||||
|
||||
{{/*
|
||||
ServiceAccount name
|
||||
Redis enabled
|
||||
*/}}
|
||||
{{- define "miroir.redisEnabled" -}}
|
||||
{{- eq .Values.miroir.taskStore.backend "redis" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Service Account Name
|
||||
*/}}
|
||||
{{- define "miroir.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
|
|
@ -51,228 +65,3 @@ ServiceAccount name
|
|||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Secret name
|
||||
*/}}
|
||||
{{- define "miroir.secretName" -}}
|
||||
{{- if .Values.miroir.existingSecret }}
|
||||
{{- .Values.miroir.existingSecret }}
|
||||
{{- else }}
|
||||
{{- include "miroir.fullname" . }}-secret
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Generate the full DNS address for a Meilisearch node.
|
||||
|
||||
Usage:
|
||||
{{ include "miroir.meilisearchNodeAddress" (dict "release" .Release "namespace" .Namespace "nodeIndex" 0) }}
|
||||
|
||||
Returns:
|
||||
http://release-name-meili-0.release-name-meili-headless.namespace.svc.cluster.local:7700
|
||||
*/}}
|
||||
{{- define "miroir.meilisearchNodeAddress" -}}
|
||||
{{- $ns := .namespace | default "default" -}}
|
||||
http://{{ .release.Name }}-meili-{{ .nodeIndex }}.{{ .release.Name }}-meili-headless.{{ $ns }}.svc.cluster.local:7700
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Generate the list of Meilisearch node addresses for the ConfigMap.
|
||||
|
||||
Usage:
|
||||
{{ include "miroir.meilisearchNodeList" $ }}
|
||||
|
||||
Returns a YAML-formatted list of node entries for the miroir config.
|
||||
*/}}
|
||||
{{- define "miroir.meilisearchNodeList" -}}
|
||||
{{- $meiliReplicas := .Values.meilisearch.replicas | default 2 | int -}}
|
||||
{{- $nodesPerGroup := .Values.meilisearch.nodesPerGroup | default 2 | int -}}
|
||||
{{- $replicaGroups := .Values.miroir.replicaGroups | default 1 | int -}}
|
||||
{{- range $group := until $replicaGroups -}}
|
||||
{{- range $node := until $nodesPerGroup -}}
|
||||
{{- $nodeIndex := add (mul $group $nodesPerGroup) $node }}
|
||||
- id: "meili-{{ $nodeIndex }}"
|
||||
address: {{ include "miroir.meilisearchNodeAddress" (dict "release" $.Release "namespace" $.Namespace "nodeIndex" $nodeIndex) }}
|
||||
replica_group: {{ $group }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Generate the miroir YAML config for the ConfigMap.
|
||||
|
||||
Usage:
|
||||
{{ include "miroir.config" $ }}
|
||||
*/}}
|
||||
{{- define "miroir.config" -}}
|
||||
shards: {{ .Values.miroir.shards | default 64 }}
|
||||
replication_factor: {{ .Values.miroir.replicationFactor | default 1 }}
|
||||
replica_groups: {{ .Values.miroir.replicaGroups | default 1 }}
|
||||
nodes:
|
||||
{{ include "miroir.meilisearchNodeList" . | indent 2 }}
|
||||
task_store:
|
||||
backend: {{ .Values.taskStore.backend | default "sqlite" }}
|
||||
path: {{ .Values.taskStore.path | default "/data/miroir-tasks.db" }}
|
||||
{{- if eq (include "miroir.redisEnabled" .) "true" }}
|
||||
url: redis://{{ .Release.Name }}-redis.{{ .Release.Namespace | default "default" }}.svc.cluster.local:6379
|
||||
{{- end }}
|
||||
health:
|
||||
interval_ms: 5000
|
||||
timeout_ms: 2000
|
||||
unhealthy_threshold: 3
|
||||
recovery_threshold: 2
|
||||
scatter:
|
||||
node_timeout_ms: 5000
|
||||
retry_on_timeout: true
|
||||
unavailable_shard_policy: partial
|
||||
rebalancer:
|
||||
auto_rebalance_on_recovery: true
|
||||
max_concurrent_migrations: 4
|
||||
migration_timeout_s: 3600
|
||||
server:
|
||||
port: 7700
|
||||
bind: "0.0.0.0"
|
||||
max_body_bytes: {{ .Values.miroir.server.max_body_bytes | default 104857600 }}
|
||||
max_concurrent_requests: {{ .Values.miroir.server.max_concurrent_requests | default 500 }}
|
||||
request_timeout_ms: {{ .Values.miroir.server.request_timeout_ms | default 30000 }}
|
||||
connection_pool_per_node:
|
||||
max_idle: {{ .Values.miroir.connection_pool_per_node.max_idle | default 32 }}
|
||||
max_total: {{ .Values.miroir.connection_pool_per_node.max_total | default 128 }}
|
||||
idle_timeout_s: {{ .Values.miroir.connection_pool_per_node.idle_timeout_s | default 60 }}
|
||||
task_registry:
|
||||
cache_size: {{ .Values.miroir.task_registry.cache_size | default 10000 }}
|
||||
redis_pool_max: {{ .Values.miroir.task_registry.redis_pool_max | default 50 }}
|
||||
ttl_seconds: {{ .Values.miroir.task_registry.ttl_seconds | default 604800 }}
|
||||
prune_interval_s: {{ .Values.miroir.task_registry.prune_interval_s | default 300 }}
|
||||
prune_batch_size: {{ .Values.miroir.task_registry.prune_batch_size | default 10000 }}
|
||||
idempotency:
|
||||
enabled: {{ .Values.miroir.idempotency.enabled | default true }}
|
||||
max_cached_keys: {{ .Values.miroir.idempotency.max_cached_keys | default 1000000 }}
|
||||
ttl_seconds: {{ .Values.miroir.idempotency.ttl_seconds | default 86400 }}
|
||||
session_pinning:
|
||||
enabled: {{ .Values.miroir.session_pinning.enabled | default true }}
|
||||
ttl_seconds: {{ .Values.miroir.session_pinning.ttl_seconds | default 900 }}
|
||||
max_sessions: {{ .Values.miroir.session_pinning.max_sessions | default 100000 }}
|
||||
wait_strategy: {{ .Values.miroir.session_pinning.wait_strategy | default "block" }}
|
||||
max_wait_ms: {{ .Values.miroir.session_pinning.max_wait_ms | default 5000 }}
|
||||
query_coalescing:
|
||||
enabled: {{ .Values.miroir.query_coalescing.enabled | default true }}
|
||||
window_ms: {{ .Values.miroir.query_coalescing.window_ms | default 50 }}
|
||||
max_subscribers: {{ .Values.miroir.query_coalescing.max_subscribers | default 1000 }}
|
||||
max_pending_queries: {{ .Values.miroir.query_coalescing.max_pending_queries | default 10000 }}
|
||||
anti_entropy:
|
||||
enabled: {{ .Values.miroir.anti_entropy.enabled | default true }}
|
||||
schedule: {{ .Values.miroir.anti_entropy.schedule | default "every 6h" }}
|
||||
shards_per_pass: {{ .Values.miroir.anti_entropy.shards_per_pass | default 0 }}
|
||||
max_read_concurrency: {{ .Values.miroir.anti_entropy.max_read_concurrency | default 2 }}
|
||||
fingerprint_batch_size: {{ .Values.miroir.anti_entropy.fingerprint_batch_size | default 1000 }}
|
||||
auto_repair: {{ .Values.miroir.anti_entropy.auto_repair | default true }}
|
||||
updated_at_field: {{ .Values.miroir.anti_entropy.updated_at_field | default "_miroir_updated_at" }}
|
||||
resharding:
|
||||
enabled: {{ .Values.miroir.resharding.enabled | default true }}
|
||||
backfill_concurrency: {{ .Values.miroir.resharding.backfill_concurrency | default 4 }}
|
||||
backfill_batch_size: {{ .Values.miroir.resharding.backfill_batch_size | default 1000 }}
|
||||
throttle_docs_per_sec: {{ .Values.miroir.resharding.throttle_docs_per_sec | default 0 }}
|
||||
verify_before_swap: {{ .Values.miroir.resharding.verify_before_swap | default true }}
|
||||
retain_old_index_hours: {{ .Values.miroir.resharding.retain_old_index_hours | default 48 }}
|
||||
allowed_windows: {{ .Values.miroir.resharding.allowed_windows | default list | toJson }}
|
||||
peer_discovery:
|
||||
service_name: {{ .Values.miroir.peer_discovery.service_name | default (printf "%s-headless" (include "miroir.fullname" .)) }}
|
||||
refresh_interval_s: {{ .Values.miroir.peer_discovery.refresh_interval_s | default 15 }}
|
||||
leader_election:
|
||||
enabled: {{ .Values.miroir.leader_election.enabled | default true }}
|
||||
lease_ttl_s: {{ .Values.miroir.leader_election.lease_ttl_s | default 10 }}
|
||||
renew_interval_s: {{ .Values.miroir.leader_election.renew_interval_s | default 3 }}
|
||||
hpa:
|
||||
enabled: {{ .Values.hpa.enabled | default false }}
|
||||
tracing:
|
||||
enabled: {{ .Values.tracing.enabled | default false }}
|
||||
endpoint: {{ .Values.tracing.endpoint | default "http://tempo.monitoring.svc:4317" }}
|
||||
service_name: {{ .Values.tracing.serviceName | default "miroir" }}
|
||||
sample_rate: {{ .Values.tracing.sampleRate | default 0.1 }}
|
||||
search_ui:
|
||||
enabled: {{ .Values.search_ui.enabled | default true }}
|
||||
scoped_key_max_age_days: {{ .Values.search_ui.scoped_key_max_age_days | default 60 }}
|
||||
scoped_key_rotate_before_expiry_days: {{ .Values.search_ui.scoped_key_rotate_before_expiry_days | default 30 }}
|
||||
scoped_key_rotation_drain_s: {{ .Values.search_ui.scoped_key_rotation_drain_s | default 120 }}
|
||||
admin_ui:
|
||||
enabled: {{ .Values.admin_ui.enabled | default true }}
|
||||
path: {{ .Values.admin_ui.path | default "/_miroir/admin" }}
|
||||
auth: {{ .Values.admin_ui.auth | default "key" }}
|
||||
session_ttl_s: {{ .Values.admin_ui.session_ttl_s | default 3600 }}
|
||||
read_only_mode: {{ .Values.admin_ui.read_only_mode | default false }}
|
||||
allowed_origins: {{ .Values.admin_ui.allowed_origins | default list "same-origin" | toJson }}
|
||||
cors_allowed_origins: {{ .Values.admin_ui.cors_allowed_origins | default list | toJson }}
|
||||
rate_limit:
|
||||
per_ip: {{ .Values.admin_ui.rate_limit.per_ip | default "10/minute" }}
|
||||
backend: {{ .Values.admin_ui.rate_limit.backend | default "redis" }}
|
||||
redis_key_prefix: {{ .Values.admin_ui.rate_limit.redis_key_prefix | default "miroir:ratelimit:adminlogin:" }}
|
||||
redis_ttl_s: {{ .Values.admin_ui.rate_limit.redis_ttl_s | default 60 }}
|
||||
failed_attempt_threshold: {{ .Values.admin_ui.rate_limit.failed_attempt_threshold | default 5 }}
|
||||
backoff_start_minutes: {{ .Values.admin_ui.rate_limit.backoff_start_minutes | default 10 }}
|
||||
backoff_max_hours: {{ .Values.admin_ui.rate_limit.backoff_max_hours | default 24 }}
|
||||
{{- if .Values.miroir.cdc }}
|
||||
cdc:
|
||||
enabled: {{ .Values.miroir.cdc.enabled | default true }}
|
||||
emit_ttl_deletes: {{ .Values.miroir.cdc.emit_ttl_deletes | default false }}
|
||||
emit_internal_writes: {{ .Values.miroir.cdc.emit_internal_writes | default false }}
|
||||
buffer:
|
||||
primary: {{ .Values.miroir.cdc.buffer.primary | default "memory" }}
|
||||
memory_bytes: {{ .Values.miroir.cdc.buffer.memory_bytes | default 67108864 }}
|
||||
overflow: {{ .Values.miroir.cdc.buffer.overflow | default "drop" }}
|
||||
{{- if eq (include "miroir.redisEnabled" .) "true" }}
|
||||
redis_bytes: {{ .Values.miroir.cdc.buffer.redis_bytes | default 1073741824 }}
|
||||
{{- end }}
|
||||
{{- if eq (include "miroir.cdcPvcEnabled" .) "true" }}
|
||||
pvc_path: /data/cdc
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Return "true" if Redis is enabled, "false" otherwise.
|
||||
*/}}
|
||||
{{- define "miroir.redisEnabled" -}}
|
||||
{{- if .Values.redis.enabled -}}true{{- else -}}false{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Redis auth secret name.
|
||||
*/}}
|
||||
{{- define "miroir.redisSecretName" -}}
|
||||
{{- if .Values.redis.auth.existingSecret -}}
|
||||
{{- .Values.redis.auth.existingSecret -}}
|
||||
{{- else -}}
|
||||
{{- include "miroir.fullname" . }}-redis-secret
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Return "true" if CDC PVC should be created, "false" otherwise.
|
||||
PVC is rendered when cdc.buffer.primary=="pvc" or cdc.buffer.overflow=="pvc".
|
||||
*/}}
|
||||
{{- define "miroir.cdcPvcEnabled" -}}
|
||||
{{- if and .Values.miroir.cdc (or (eq .Values.miroir.cdc.buffer.primary "pvc") (eq .Values.miroir.cdc.buffer.overflow "pvc")) -}}true{{- else -}}false{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Cross-field validations that JSON Schema draft-7 cannot express.
|
||||
Rendered as an empty ConfigMap; fails template rendering on invalid config.
|
||||
*/}}
|
||||
{{- define "miroir.validate.values" -}}
|
||||
{{- if .Values.search_ui -}}
|
||||
{{- if and (hasKey .Values.search_ui "scoped_key_rotate_before_expiry_days") (hasKey .Values.search_ui "scoped_key_max_age_days") -}}
|
||||
{{- if ge (int .Values.search_ui.scoped_key_rotate_before_expiry_days) (int .Values.search_ui.scoped_key_max_age_days) -}}
|
||||
{{- fail (printf "search_ui.scoped_key_rotate_before_expiry_days (%d) must be strictly less than scoped_key_max_age_days (%d); otherwise rotation fires before/at key issuance, producing a continuous rotation loop" (int .Values.search_ui.scoped_key_rotate_before_expiry_days) (int .Values.search_ui.scoped_key_max_age_days)) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if .Values.miroir.leader_election -}}
|
||||
{{- if and (hasKey .Values.miroir.leader_election "lease_ttl_s") (hasKey .Values.miroir.leader_election "renew_interval_s") -}}
|
||||
{{- if le (int .Values.miroir.leader_election.lease_ttl_s) (int .Values.miroir.leader_election.renew_interval_s) -}}
|
||||
{{- fail (printf "leader_election.lease_ttl_s (%d) must be greater than leader_election.renew_interval_s (%d); otherwise the lease expires before it can be renewed" (int .Values.miroir.leader_election.lease_ttl_s) (int .Values.miroir.leader_election.renew_interval_s)) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
|
|
|||
23
charts/miroir/templates/configmap.yaml
Normal file
23
charts/miroir/templates/configmap.yaml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "miroir.fullname" . }}-config
|
||||
labels:
|
||||
{{- include "miroir.labels" . | nindent 4 }}
|
||||
data:
|
||||
config.yaml: |
|
||||
miroir:
|
||||
shards: {{ .Values.miroir.shards }}
|
||||
replication_factor: {{ .Values.miroir.replicationFactor }}
|
||||
replica_groups: {{ .Values.miroir.replicaGroups }}
|
||||
scatter:
|
||||
unavailable_shard_policy: {{ .Values.miroir.scatter.unavailableShardPolicy }}
|
||||
task_store:
|
||||
backend: {{ .Values.miroir.taskStore.backend }}
|
||||
{{- if eq .Values.miroir.taskStore.backend "sqlite" }}
|
||||
path: {{ .Values.miroir.taskStore.path }}
|
||||
{{- else if eq .Values.miroir.taskStore.backend "redis" }}
|
||||
url: {{ .Values.miroir.taskStore.url }}
|
||||
{{- end }}
|
||||
admin:
|
||||
enabled: {{ .Values.miroir.admin.enabled }}
|
||||
73
charts/miroir/templates/deployment.yaml
Normal file
73
charts/miroir/templates/deployment.yaml
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "miroir.fullname" . }}
|
||||
labels:
|
||||
{{- include "miroir.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.miroir.hpa.enabled }}
|
||||
replicas: {{ .Values.miroir.replicas }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "miroir.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
|
||||
{{- with .Values.miroir.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "miroir.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.miroir.podSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "miroir.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: miroir
|
||||
{{- with .Values.miroir.image }}
|
||||
image: "{{ .repository }}:{{ .tag | default $.Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .pullPolicy }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 7700
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: MIROIR_SHARDS
|
||||
value: {{ .Values.miroir.shards | quote }}
|
||||
- name: MIROIR_REPLICATION_FACTOR
|
||||
value: {{ .Values.miroir.replicationFactor | quote }}
|
||||
- name: MIROIR_REPLICA_GROUPS
|
||||
value: {{ .Values.miroir.replicaGroups | quote }}
|
||||
- name: MIROIR_TASK_STORE_BACKEND
|
||||
value: {{ .Values.miroir.taskStore.backend }}
|
||||
{{- if eq .Values.miroir.taskStore.backend "sqlite" }}
|
||||
- name: MIROIR_TASK_STORE_PATH
|
||||
value: {{ .Values.miroir.taskStore.path | quote }}
|
||||
{{- else if eq .Values.miroir.taskStore.backend "redis" }}
|
||||
- name: MIROIR_TASK_STORE_URL
|
||||
value: {{ .Values.miroir.taskStore.url | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.miroir.existingSecret }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: {{ .Values.miroir.existingSecret }}
|
||||
{{- end }}
|
||||
{{- with .Values.miroir.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
{{- with .Values.miroir.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
32
charts/miroir/templates/hpa.yaml
Normal file
32
charts/miroir/templates/hpa.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{{- if .Values.miroir.hpa.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "miroir.fullname" . }}
|
||||
labels:
|
||||
{{- include "miroir.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "miroir.fullname" . }}
|
||||
minReplicas: {{ .Values.miroir.hpa.minReplicas }}
|
||||
maxReplicas: {{ .Values.miroir.hpa.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.miroir.hpa.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.miroir.hpa.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.miroir.hpa.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.miroir.hpa.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
@ -1,138 +1,82 @@
|
|||
{{/*
|
||||
Redis Deployment (only when redis.enabled=true)
|
||||
*/}}
|
||||
{{- if eq (include "miroir.redisEnabled" .) "true" }}
|
||||
{{- if and (include "miroir.redisEnabled" .) .Values.redis.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "miroir.fullname" . }}-redis
|
||||
labels:
|
||||
{{- include "miroir.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: redis
|
||||
app: redis
|
||||
spec:
|
||||
replicas: {{ .Values.redis.replicas }}
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "miroir.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: redis
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
{{- with .Values.redis.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "miroir.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: redis
|
||||
{{- with .Values.redis.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
app: redis
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 999
|
||||
fsGroup: 999
|
||||
containers:
|
||||
- name: redis
|
||||
{{- with .Values.redis.image }}
|
||||
image: "{{ .repository }}:{{ .tag }}"
|
||||
imagePullPolicy: {{ .pullPolicy }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: redis
|
||||
image: "{{ .Values.redis.image.repository }}:{{ .Values.redis.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.redis.image.pullPolicy }}
|
||||
ports:
|
||||
- name: redis
|
||||
containerPort: 6379
|
||||
protocol: TCP
|
||||
{{- if .Values.redis.auth.enabled }}
|
||||
env:
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "miroir.redisSecretName" . }}
|
||||
key: redis-password
|
||||
{{- end }}
|
||||
args:
|
||||
{{- if .Values.redis.auth.enabled }}
|
||||
- --requirepass
|
||||
- $(REDIS_PASSWORD)
|
||||
{{- end }}
|
||||
{{- if .Values.redis.persistence.enabled }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- redis-cli
|
||||
{{- if .Values.redis.auth.enabled }}
|
||||
- -a
|
||||
- $(REDIS_PASSWORD)
|
||||
{{- end }}
|
||||
- ping
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- redis-cli
|
||||
{{- if .Values.redis.auth.enabled }}
|
||||
- -a
|
||||
- $(REDIS_PASSWORD)
|
||||
{{- end }}
|
||||
- ping
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 5
|
||||
resources:
|
||||
{{- toYaml .Values.redis.resources | nindent 12 }}
|
||||
containerPort: 6379
|
||||
protocol: TCP
|
||||
{{- with .Values.redis.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
{{- if .Values.redis.persistence.enabled }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
{{- end }}
|
||||
{{- if .Values.redis.persistence.enabled }}
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "miroir.fullname" . }}-redis-data
|
||||
{{- end }}
|
||||
{{- with .Values.redis.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.redis.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.redis.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "miroir.fullname" . }}-redis
|
||||
{{- end }}
|
||||
---
|
||||
{{- if and (include "miroir.redisEnabled" .) .Values.redis.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "miroir.fullname" . }}-redis
|
||||
labels:
|
||||
{{- include "miroir.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: redis
|
||||
app: redis
|
||||
spec:
|
||||
type: {{ .Values.redis.service.type | default "ClusterIP" }}
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: {{ .Values.redis.service.port | default 6379 }}
|
||||
targetPort: redis
|
||||
protocol: TCP
|
||||
name: redis
|
||||
- port: 6379
|
||||
targetPort: redis
|
||||
protocol: TCP
|
||||
name: redis
|
||||
selector:
|
||||
{{- include "miroir.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: redis
|
||||
{{- if .Values.redis.persistence.enabled }}
|
||||
app: redis
|
||||
{{- end }}
|
||||
---
|
||||
{{- if and (include "miroir.redisEnabled" .) .Values.redis.enabled .Values.redis.persistence.enabled }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "miroir.fullname" . }}-redis-data
|
||||
name: {{ include "miroir.fullname" . }}-redis
|
||||
labels:
|
||||
{{- include "miroir.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: redis
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.redis.persistence.size | default "1Gi" }}
|
||||
storage: {{ .Values.redis.persistence.size }}
|
||||
{{- if .Values.redis.persistence.storageClass }}
|
||||
storageClassName: {{ .Values.redis.persistence.storageClass }}
|
||||
{{- end }}
|
||||
|
|
|
|||
19
charts/miroir/templates/service.yaml
Normal file
19
charts/miroir/templates/service.yaml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "miroir.fullname" . }}
|
||||
labels:
|
||||
{{- include "miroir.labels" . | nindent 4 }}
|
||||
{{- with .Values.miroir.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.miroir.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.miroir.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "miroir.selectorLabels" . | nindent 4 }}
|
||||
|
|
@ -1,11 +1,8 @@
|
|||
{{/*
|
||||
ServiceAccount
|
||||
*/}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "miroir.serviceAccountName" . }}
|
||||
name: {{ include "miroir.fullname" . }}
|
||||
labels:
|
||||
{{- include "miroir.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,33 @@
|
|||
# Helm Chart Validation Tests
|
||||
# Miroir Helm Chart Tests
|
||||
|
||||
## Running All Tests
|
||||
This directory contains test cases and validation scripts for the Miroir Helm chart.
|
||||
|
||||
## Schema Validation Tests
|
||||
|
||||
The `test_schema.py` script validates that the `values.schema.json` constraints are working correctly.
|
||||
|
||||
### Test Cases
|
||||
|
||||
| Test File | Description | Expected Result |
|
||||
|-----------|-------------|-----------------|
|
||||
| `replicas-1-sqlite.yaml` | Single replica with SQLite | PASS |
|
||||
| `replicas-2-sqlite.yaml` | Multiple replicas with SQLite | FAIL (error: backend must be redis) |
|
||||
| `replicas-2-redis.yaml` | Multiple replicas with Redis | PASS |
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
./charts/miroir/tests/run-tests.sh
|
||||
# Using Python (works without helm installed)
|
||||
python3 tests/test_schema.py
|
||||
|
||||
# Using helm lint (requires helm)
|
||||
helm lint --strict -f tests/replicas-1-sqlite.yaml .
|
||||
helm lint --strict -f tests/replicas-2-sqlite.yaml . # Should fail
|
||||
helm lint --strict -f tests/replicas-2-redis.yaml .
|
||||
```
|
||||
|
||||
This runs both `helm lint --strict` (schema rules) and `helm template` (render-time rules) for all test cases.
|
||||
### Constraint Details
|
||||
|
||||
## Test Cases
|
||||
The values.schema.json enforces that when `miroir.replicas > 1`, the `taskStore.backend` must be `"redis"`. SQLite is a single-writer database and cannot be shared across multiple pods.
|
||||
|
||||
### Schema rejection tests (`helm lint --strict`)
|
||||
|
||||
| File | Rule | Description |
|
||||
|------|------|-------------|
|
||||
| `invalid-multi-replica-sqlite.yaml` | 1 | `replicas>1` with `taskStore.backend: sqlite` — SQLite cannot be shared across pods |
|
||||
| `bad-hpa-no-redis.yaml` | 2a | `hpa.enabled: true` with `taskStore.backend: sqlite` — autoscaling requires Redis |
|
||||
| `bad-hpa-single-replica.yaml` | 2b | `hpa.enabled: true` with `replicas: 1` — HPA requires `replicas >= 2` |
|
||||
| `bad-search-ui-rate-limit-local-multi.yaml` | 3 | `search_ui.rate_limit.backend: local` with `replicas>1` — per-pod limits don't share state |
|
||||
| `bad-admin-login-rate-limit-local-multi.yaml` | 4 | `admin_ui.login_rate_limit.backend: local` with `replicas>1` — per-pod limits don't share state |
|
||||
|
||||
### Template rejection tests (`helm template`)
|
||||
|
||||
| File | Rule | Description |
|
||||
|------|------|-------------|
|
||||
| `bad-scoped-key-rotate-gte-max.yaml` | 5a | `rotate_before_expiry >= max_age` — rotation fires at/before issuance |
|
||||
| `bad-scoped-key-rotate-gt-max.yaml` | 5b | `rotate_before_expiry > max_age` — negative rotation window |
|
||||
|
||||
Rule 5 uses template-level `fail()` because JSON Schema draft-7 cannot compare sibling property values.
|
||||
|
||||
### Positive tests
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `valid-single-replica-sqlite.yaml` | Single replica with SQLite (dev default) |
|
||||
| `valid-single-pod-oversized.yaml` | Single-pod oversized mode (4 vCPU / 8 GB) for dev/small deployments |
|
||||
| `valid-multi-replica-redis.yaml` | Multi-replica with Redis |
|
||||
| `good-production.yaml` | Full production config with HPA, Redis rate limiting, and scoped keys |
|
||||
| `good-dev-no-ui.yaml` | Minimal dev defaults |
|
||||
See values.schema.json lines 142-161 for the constraint implementation.
|
||||
|
|
|
|||
7
charts/miroir/tests/replicas-1-sqlite.yaml
Normal file
7
charts/miroir/tests/replicas-1-sqlite.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Test case: replicas: 1, backend: sqlite -> should PASS
|
||||
# Single replica can use SQLite (single-writer is fine)
|
||||
miroir:
|
||||
replicas: 1
|
||||
taskStore:
|
||||
backend: sqlite
|
||||
path: /data/miroir-tasks.db
|
||||
7
charts/miroir/tests/replicas-2-redis.yaml
Normal file
7
charts/miroir/tests/replicas-2-redis.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Test case: replicas: 2, backend: redis -> should PASS
|
||||
# Multiple replicas MUST use Redis (shared backend)
|
||||
miroir:
|
||||
replicas: 2
|
||||
taskStore:
|
||||
backend: redis
|
||||
url: redis://redis:6379
|
||||
7
charts/miroir/tests/replicas-2-sqlite.yaml
Normal file
7
charts/miroir/tests/replicas-2-sqlite.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Test case: replicas: 2, backend: sqlite -> should FAIL
|
||||
# Multiple replicas CANNOT use SQLite (single-writer cannot be shared)
|
||||
miroir:
|
||||
replicas: 2
|
||||
taskStore:
|
||||
backend: sqlite
|
||||
path: /data/miroir-tasks.db
|
||||
139
charts/miroir/tests/test_schema.py
Executable file
139
charts/miroir/tests/test_schema.py
Executable file
|
|
@ -0,0 +1,139 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test values.schema.json validation constraints.
|
||||
|
||||
Tests the SQLite + multiple replicas rejection rule:
|
||||
- replicas: 1 + sqlite -> PASS
|
||||
- replicas: 2 + sqlite -> FAIL
|
||||
- replicas: 2 + redis -> PASS
|
||||
|
||||
Usage:
|
||||
python3 test_schema.py # Run tests
|
||||
helm lint --strict -f tests/replicas-2-sqlite.yaml . # Run with helm
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def evaluate_condition(instance, if_cond):
|
||||
"""Evaluate a JSON Schema if condition against an instance."""
|
||||
if "properties" in if_cond:
|
||||
for prop_path, schema in if_cond["properties"].items():
|
||||
parts = prop_path.split(".")
|
||||
value = instance
|
||||
for part in parts:
|
||||
if not isinstance(value, dict):
|
||||
return False
|
||||
if part not in value:
|
||||
return False
|
||||
value = value[part]
|
||||
|
||||
# Check the constraint
|
||||
if "const" in schema:
|
||||
if value != schema["const"]:
|
||||
return False
|
||||
elif "minimum" in schema:
|
||||
if not isinstance(value, (int, float)):
|
||||
return False
|
||||
if value < schema["minimum"]:
|
||||
return False
|
||||
elif "type" in schema:
|
||||
if schema["type"] == "boolean":
|
||||
if value != schema.get(True, False):
|
||||
return False
|
||||
|
||||
if "required" in if_cond:
|
||||
for req in if_cond["required"]:
|
||||
if req not in instance:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validate_schema(schema: dict, instance: dict, path: str = "") -> list:
|
||||
"""Validate instance against schema, return list of errors."""
|
||||
errors = []
|
||||
|
||||
# Check allOf constraints
|
||||
if "allOf" in schema:
|
||||
for constraint in schema["allOf"]:
|
||||
if "if" in constraint and "then" in constraint:
|
||||
if evaluate_condition(instance, constraint["if"]):
|
||||
# The 'if' condition is true, check 'then' constraint
|
||||
then_schema = constraint["then"]
|
||||
|
||||
# Check nested properties in 'then'
|
||||
if "properties" in then_schema:
|
||||
for prop, prop_schema in then_schema["properties"].items():
|
||||
if prop in instance:
|
||||
if "properties" in prop_schema:
|
||||
for nested, nested_schema in prop_schema["properties"].items():
|
||||
if nested in instance[prop]:
|
||||
actual = instance[prop][nested]
|
||||
if "const" in nested_schema:
|
||||
if actual != nested_schema["const"]:
|
||||
msg = f"{path}{prop}.{nested}: expected {nested_schema['const']}, got {actual}"
|
||||
if "errorMessage" in constraint:
|
||||
msg = constraint["errorMessage"]
|
||||
errors.append(msg)
|
||||
|
||||
# Check required fields in 'then'
|
||||
if "required" in then_schema:
|
||||
for req in then_schema["required"]:
|
||||
if req not in instance:
|
||||
errors.append(f"{path}{req} is required")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def test_schema_constraints():
|
||||
chart_dir = Path(__file__).parent.parent
|
||||
schema_path = chart_dir / "values.schema.json"
|
||||
tests_dir = Path(__file__).parent
|
||||
|
||||
schema = load_json(schema_path)
|
||||
|
||||
test_cases = [
|
||||
# (replicas, backend, should_pass, description)
|
||||
(1, "sqlite", True, "replicas: 1 + sqlite should PASS"),
|
||||
(2, "sqlite", False, "replicas: 2 + sqlite should FAIL"),
|
||||
(2, "redis", True, "replicas: 2 + redis should PASS"),
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for replicas, backend, should_pass, description in test_cases:
|
||||
instance = {
|
||||
"replicas": replicas,
|
||||
"taskStore": {"backend": backend}
|
||||
}
|
||||
|
||||
miroir_schema = schema["properties"]["miroir"]
|
||||
errors = validate_schema(miroir_schema, instance)
|
||||
|
||||
is_valid = len(errors) == 0
|
||||
|
||||
if is_valid == should_pass:
|
||||
print(f"✓ {description}")
|
||||
passed += 1
|
||||
else:
|
||||
print(f"✗ {description}")
|
||||
for err in errors:
|
||||
print(f" Error: {err}")
|
||||
failed += 1
|
||||
|
||||
print(f"\n{passed} passed, {failed} failed")
|
||||
return failed == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = test_schema_constraints()
|
||||
sys.exit(0 if success else 1)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-7/schema#",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Miroir Helm Chart Values",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -9,9 +9,9 @@
|
|||
"image": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repository": { "type": "string" },
|
||||
"tag": { "type": "string" },
|
||||
"pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] }
|
||||
"repository": {"type": "string"},
|
||||
"tag": {"type": "string"},
|
||||
"pullPolicy": {"type": "string", "enum": ["Always", "IfNotPresent", "Never"]}
|
||||
}
|
||||
},
|
||||
"replicas": {
|
||||
|
|
@ -30,496 +30,192 @@
|
|||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"existingSecret": { "type": "string" },
|
||||
"logLevel": { "type": "string", "enum": ["trace", "debug", "info", "warn", "error"] },
|
||||
"podAnnotations": { "type": "object" },
|
||||
"podLabels": { "type": "object" },
|
||||
"resources": {
|
||||
"taskStore": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limits": { "type": "object" },
|
||||
"requests": { "type": "object" }
|
||||
"backend": {
|
||||
"type": "string",
|
||||
"enum": ["sqlite", "redis"]
|
||||
},
|
||||
"path": {"type": "string"},
|
||||
"url": {"type": "string"}
|
||||
},
|
||||
"required": ["backend"]
|
||||
},
|
||||
"admin": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"}
|
||||
}
|
||||
},
|
||||
"nodeSelector": { "type": "object" },
|
||||
"tolerations": { "type": "array" },
|
||||
"affinity": { "type": "object" },
|
||||
"cdc": {
|
||||
"scatter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"emit_ttl_deletes": { "type": "boolean" },
|
||||
"emit_internal_writes": { "type": "boolean" },
|
||||
"buffer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"primary": { "type": "string", "enum": ["memory", "pvc", "redis"] },
|
||||
"overflow": { "type": "string", "enum": ["drop", "pvc", "redis"] },
|
||||
"pvc_size": { "type": "string" },
|
||||
"pvc_storage_class": { "type": "string" },
|
||||
"memory_bytes": { "type": "integer", "minimum": 0 },
|
||||
"redis_bytes": { "type": "integer", "minimum": 0 }
|
||||
"unavailableShardPolicy": {
|
||||
"type": "string",
|
||||
"enum": ["partial", "error"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"hpa": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"minReplicas": {"type": "integer", "minimum": 1},
|
||||
"maxReplicas": {"type": "integer", "minimum": 1},
|
||||
"targetCPUUtilizationPercentage": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||
"targetMemoryUtilizationPercentage": {"type": "integer", "minimum": 1, "maximum": 100}
|
||||
}
|
||||
},
|
||||
"ingress": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"className": {"type": "string"},
|
||||
"annotations": {"type": "object"},
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"host": {"type": "string"},
|
||||
"paths": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string"},
|
||||
"pathType": {"type": "string"}
|
||||
},
|
||||
"required": ["path", "pathType"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["host"]
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secretName": {"type": "string"},
|
||||
"hosts": {"type": "array", "items": {"type": "string"}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"resources": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max_body_bytes": { "type": "integer", "minimum": 0 },
|
||||
"max_concurrent_requests": { "type": "integer", "minimum": 1 },
|
||||
"request_timeout_ms": { "type": "integer", "minimum": 0 }
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cpu": {"type": "string"},
|
||||
"memory": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"requests": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cpu": {"type": "string"},
|
||||
"memory": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"connection_pool_per_node": {
|
||||
"podAnnotations": {"type": "object"},
|
||||
"podSecurityContext": {"type": "object"},
|
||||
"securityContext": {"type": "object"},
|
||||
"service": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max_idle": { "type": "integer", "minimum": 0 },
|
||||
"max_total": { "type": "integer", "minimum": 1 },
|
||||
"idle_timeout_s": { "type": "integer", "minimum": 1 }
|
||||
"type": {"type": "string"},
|
||||
"port": {"type": "integer"},
|
||||
"annotations": {"type": "object"}
|
||||
}
|
||||
},
|
||||
"task_registry": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cache_size": { "type": "integer", "minimum": 0 },
|
||||
"redis_pool_max": { "type": "integer", "minimum": 1 },
|
||||
"ttl_seconds": { "type": "integer", "minimum": 1 },
|
||||
"prune_interval_s": { "type": "integer", "minimum": 1 },
|
||||
"prune_batch_size": { "type": "integer", "minimum": 1 }
|
||||
"existingSecret": {"type": "string"}
|
||||
},
|
||||
"required": ["replicas", "shards", "replicationFactor", "replicaGroups", "taskStore"],
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"replicas": {"minimum": 2}
|
||||
},
|
||||
"required": ["replicas"]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"taskStore": {
|
||||
"properties": {
|
||||
"backend": {"const": "redis"}
|
||||
},
|
||||
"required": ["backend"]
|
||||
}
|
||||
},
|
||||
"errorMessage": "taskStore.backend must be 'redis' when replicas > 1 (SQLite is single-writer and cannot be shared across pods)"
|
||||
}
|
||||
},
|
||||
"idempotency": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"max_cached_keys": { "type": "integer", "minimum": 0 },
|
||||
"ttl_seconds": { "type": "integer", "minimum": 1 }
|
||||
}
|
||||
},
|
||||
"session_pinning": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"ttl_seconds": { "type": "integer", "minimum": 1 },
|
||||
"max_sessions": { "type": "integer", "minimum": 0 },
|
||||
"wait_strategy": { "type": "string", "enum": ["block", "route_pin"] },
|
||||
"max_wait_ms": { "type": "integer", "minimum": 0 }
|
||||
}
|
||||
},
|
||||
"query_coalescing": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"window_ms": { "type": "integer", "minimum": 0 },
|
||||
"max_subscribers": { "type": "integer", "minimum": 1 },
|
||||
"max_pending_queries": { "type": "integer", "minimum": 1 }
|
||||
}
|
||||
},
|
||||
"anti_entropy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"schedule": { "type": "string" },
|
||||
"shards_per_pass": { "type": "integer", "minimum": 0 },
|
||||
"max_read_concurrency": { "type": "integer", "minimum": 1 },
|
||||
"fingerprint_batch_size": { "type": "integer", "minimum": 1 },
|
||||
"auto_repair": { "type": "boolean" },
|
||||
"updated_at_field": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"resharding": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"backfill_concurrency": { "type": "integer", "minimum": 1 },
|
||||
"backfill_batch_size": { "type": "integer", "minimum": 1 },
|
||||
"throttle_docs_per_sec": { "type": "integer", "minimum": 0 },
|
||||
"verify_before_swap": { "type": "boolean" },
|
||||
"retain_old_index_hours": { "type": "integer", "minimum": 0 },
|
||||
"allowed_windows": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
},
|
||||
"peer_discovery": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service_name": { "type": "string" },
|
||||
"refresh_interval_s": { "type": "integer", "minimum": 1 }
|
||||
}
|
||||
},
|
||||
"leader_election": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"lease_ttl_s": { "type": "integer", "minimum": 1 },
|
||||
"renew_interval_s": { "type": "integer", "minimum": 1 }
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"hpa": {
|
||||
"properties": {
|
||||
"enabled": true
|
||||
},
|
||||
"required": ["enabled"]
|
||||
}
|
||||
},
|
||||
"required": ["hpa"]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"replicas": {"minimum": 2},
|
||||
"taskStore": {
|
||||
"properties": {
|
||||
"backend": {"const": "redis"}
|
||||
},
|
||||
"required": ["backend"]
|
||||
}
|
||||
},
|
||||
"errorMessage": "HPA requires replicas >= 2 and taskStore.backend='redis'"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"taskStore": {
|
||||
"redis": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"backend": {
|
||||
"type": "string",
|
||||
"enum": ["sqlite", "redis"]
|
||||
"enabled": {"type": "boolean"},
|
||||
"image": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repository": {"type": "string"},
|
||||
"tag": {"type": "string"},
|
||||
"pullPolicy": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"path": { "type": "string" },
|
||||
"url": { "type": "string" }
|
||||
},
|
||||
"required": ["backend"]
|
||||
},
|
||||
"hpa": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"minReplicas": { "type": "integer", "minimum": 1 },
|
||||
"maxReplicas": { "type": "integer", "minimum": 1 },
|
||||
"targetCPUUtilizationPercentage": { "type": "integer", "minimum": 1, "maximum": 100 },
|
||||
"targetMemoryUtilizationPercentage": { "type": "integer", "minimum": 1, "maximum": 100 },
|
||||
"targetRequestsInFlight": { "type": "string" },
|
||||
"targetBackgroundQueueDepth": { "type": "string" },
|
||||
"annotations": { "type": "object" },
|
||||
"behavior": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"tracing": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"endpoint": { "type": "string" },
|
||||
"serviceName": { "type": "string" },
|
||||
"sampleRate": { "type": "number", "minimum": 0, "maximum": 1 }
|
||||
"resources": {"type": "object"},
|
||||
"persistence": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"size": {"type": "string"},
|
||||
"storageClass": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"serviceAccount": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"create": { "type": "boolean" },
|
||||
"name": { "type": "string" },
|
||||
"annotations": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"service": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "string" },
|
||||
"annotations": { "type": "object" },
|
||||
"ports": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"http": { "type": "integer", "minimum": 1, "maximum": 65535 },
|
||||
"metrics": { "type": "integer", "minimum": 1, "maximum": 65535 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"serviceMonitor": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"interval": { "type": "string" },
|
||||
"annotations": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"dashboards": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"annotations": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"prometheusRule": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"annotations": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"headless": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"annotations": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"meilisearch": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"image": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repository": { "type": "string" },
|
||||
"tag": { "type": "string" },
|
||||
"pullPolicy": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"replicas": { "type": "integer", "minimum": 1 },
|
||||
"nodesPerGroup": { "type": "integer", "minimum": 1 },
|
||||
"podAnnotations": { "type": "object" },
|
||||
"podLabels": { "type": "object" },
|
||||
"resources": { "type": "object" },
|
||||
"nodeSelector": { "type": "object" },
|
||||
"tolerations": { "type": "array" },
|
||||
"affinity": { "type": "object" },
|
||||
"persistence": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"size": { "type": "string" },
|
||||
"storageClass": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"env": { "type": "array" },
|
||||
"masterKey": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"image": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repository": { "type": "string" },
|
||||
"tag": { "type": "string" },
|
||||
"pullPolicy": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"replicas": { "type": "integer", "minimum": 1 },
|
||||
"podAnnotations": { "type": "object" },
|
||||
"podLabels": { "type": "object" },
|
||||
"resources": { "type": "object" },
|
||||
"nodeSelector": { "type": "object" },
|
||||
"tolerations": { "type": "array" },
|
||||
"affinity": { "type": "object" },
|
||||
"persistence": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"size": { "type": "string" },
|
||||
"storageClass": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"service": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "string" },
|
||||
"port": { "type": "integer", "minimum": 1, "maximum": 65535 }
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"existingSecret": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"search_ui": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"scoped_key_max_age_days": { "type": "integer", "minimum": 2 },
|
||||
"scoped_key_rotate_before_expiry_days": { "type": "integer", "minimum": 1 },
|
||||
"scoped_key_rotation_drain_s": { "type": "integer", "minimum": 10 },
|
||||
"rate_limit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"backend": { "type": "string", "enum": ["local", "redis"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"admin_ui": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"rate_limit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"per_ip": { "type": "string" },
|
||||
"backend": { "type": "string", "enum": ["local", "redis"] },
|
||||
"failed_attempt_threshold": { "type": "integer", "minimum": 1 },
|
||||
"backoff_start_minutes": { "type": "integer", "minimum": 1 },
|
||||
"backoff_max_hours": { "type": "integer", "minimum": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"prometheusAdapter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"customMetrics": { "type": "array" }
|
||||
"create": {"type": "boolean"},
|
||||
"annotations": {"type": "object"},
|
||||
"name": {"type": "string"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"description": "Rule 0: taskStore.backend: redis requires miroir.replicas > 1 (HA mode requires multiple replicas)",
|
||||
"if": {
|
||||
"properties": {
|
||||
"taskStore": {
|
||||
"properties": {
|
||||
"backend": { "const": "redis" }
|
||||
},
|
||||
"required": ["backend"]
|
||||
}
|
||||
},
|
||||
"required": ["taskStore"]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"miroir": {
|
||||
"properties": {
|
||||
"replicas": { "type": "integer", "exclusiveMinimum": 1 }
|
||||
},
|
||||
"required": ["replicas"]
|
||||
}
|
||||
},
|
||||
"required": ["miroir"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Rule 1: miroir.replicas > 1 requires taskStore.backend: redis",
|
||||
"if": {
|
||||
"properties": {
|
||||
"miroir": {
|
||||
"properties": {
|
||||
"replicas": { "type": "integer", "exclusiveMinimum": 1 }
|
||||
},
|
||||
"required": ["replicas"]
|
||||
}
|
||||
},
|
||||
"required": ["miroir"]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"taskStore": {
|
||||
"properties": {
|
||||
"backend": {
|
||||
"const": "redis"
|
||||
}
|
||||
},
|
||||
"required": ["backend"]
|
||||
}
|
||||
},
|
||||
"required": ["taskStore"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Rule 2: hpa.enabled requires replicas >= 2 AND taskStore.backend: redis",
|
||||
"if": {
|
||||
"properties": {
|
||||
"hpa": {
|
||||
"properties": {
|
||||
"enabled": { "const": true }
|
||||
},
|
||||
"required": ["enabled"]
|
||||
}
|
||||
},
|
||||
"required": ["hpa"]
|
||||
},
|
||||
"then": {
|
||||
"allOf": [
|
||||
{
|
||||
"properties": {
|
||||
"miroir": {
|
||||
"properties": {
|
||||
"replicas": {
|
||||
"type": "integer",
|
||||
"minimum": 2
|
||||
}
|
||||
},
|
||||
"required": ["replicas"]
|
||||
}
|
||||
},
|
||||
"required": ["miroir"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"taskStore": {
|
||||
"properties": {
|
||||
"backend": {
|
||||
"const": "redis"
|
||||
}
|
||||
},
|
||||
"required": ["backend"]
|
||||
}
|
||||
},
|
||||
"required": ["taskStore"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Rule 3: search_ui.rate_limit.backend must be redis when miroir.replicas > 1 (local is per-pod, not shared)",
|
||||
"if": {
|
||||
"properties": {
|
||||
"miroir": {
|
||||
"properties": {
|
||||
"replicas": { "type": "integer", "exclusiveMinimum": 1 }
|
||||
},
|
||||
"required": ["replicas"]
|
||||
}
|
||||
},
|
||||
"required": ["miroir"]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"search_ui": {
|
||||
"properties": {
|
||||
"rate_limit": {
|
||||
"properties": {
|
||||
"backend": {
|
||||
"enum": ["redis"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Rule 4: admin_ui.rate_limit.backend must be redis when miroir.replicas > 1 (local is per-pod, not shared)",
|
||||
"if": {
|
||||
"properties": {
|
||||
"miroir": {
|
||||
"properties": {
|
||||
"replicas": { "type": "integer", "exclusiveMinimum": 1 }
|
||||
},
|
||||
"required": ["replicas"]
|
||||
}
|
||||
},
|
||||
"required": ["miroir"]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"admin_ui": {
|
||||
"properties": {
|
||||
"rate_limit": {
|
||||
"properties": {
|
||||
"backend": {
|
||||
"const": "redis"
|
||||
}
|
||||
},
|
||||
"required": ["backend"]
|
||||
}
|
||||
},
|
||||
"required": ["rate_limit"]
|
||||
}
|
||||
},
|
||||
"required": ["admin_ui"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Rule 5: scoped_key_rotate_before_expiry_days must be strictly less than scoped_key_max_age_days (enforced at render time via _helpers.tpl since JSON Schema draft-7 cannot compare sibling properties)"
|
||||
},
|
||||
{
|
||||
"description": "Rule 6: leader_election.lease_ttl_s must be greater than leader_election.renew_interval_s (enforced at render time via _helpers.tpl since JSON Schema draft-7 cannot compare sibling properties)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,313 +1,115 @@
|
|||
# Miroir Helm Chart Values
|
||||
# These defaults boot a working single-pod install for evaluation and CI.
|
||||
# For production, override to: replicas=2+, replicationFactor=2, replicaGroups=2,
|
||||
# taskStore.backend=redis, redis.enabled=true, hpa.enabled=true
|
||||
#
|
||||
# SECRET INVENTORY (plan §9):
|
||||
# ┌──────────────────────────┬───────────────────────────────┬─────────────────────┐
|
||||
# │ Secret │ Managed by │ Rotation │
|
||||
# ├──────────────────────────┼───────────────────────────────┼─────────────────────┤
|
||||
# │ masterKey │ Helm / ESO │ Manual key change │
|
||||
# │ nodeMasterKey │ Helm / ESO │ Zero-downtime (§9) │
|
||||
# │ MEILI_MASTER_KEY │ Helm / ESO │ Pod restart only │
|
||||
# │ adminApiKey │ Helm / ESO │ Manual + restart │
|
||||
# │ adminSessionSealKey │ Helm / ESO │ Manual + restart │
|
||||
# │ searchUiJwtSecret │ Helm / ESO │ Zero-downtime (§9) │
|
||||
# │ searchUiJwtSecretPrev │ Helm / ESO │ Overlap window │
|
||||
# │ searchUiSharedKey │ Helm / ESO │ Manual + restart │
|
||||
# │ redis_password │ Helm / ESO │ Manual + restart │
|
||||
# │ ghcr_credentials │ Argo Workflows (CI only) │ Manual │
|
||||
# │ github_token │ Argo Workflows (CI only) │ Manual │
|
||||
# └──────────────────────────┴───────────────────────────────┴─────────────────────┘
|
||||
# ghcr_credentials and github_token are NOT in this chart — they belong to the
|
||||
# Argo Workflows service account in the iad-ci cluster (see CLAUDE.md CI/CD section).
|
||||
# MEILI_MASTER_KEY is set at Meilisearch process start and cannot be rotated
|
||||
# without a pod restart (documented in the Meilisearch docs).
|
||||
# Default values for miroir.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
miroir:
|
||||
image:
|
||||
repository: ghcr.io/jedarden/miroir
|
||||
tag: "" # defaults to Chart.appVersion
|
||||
tag: "" # Defaults to Chart.appVersion
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
replicas: 1 # dev default: override to 2+ in production (requires taskStore.backend=redis)
|
||||
|
||||
# Sharding configuration
|
||||
shards: 64
|
||||
replicationFactor: 1 # dev default: override to 2 in production
|
||||
replicaGroups: 1 # dev default: override to 2 in production
|
||||
masterKey: "" # auto-generated if empty; ignored when existingSecret is set
|
||||
nodeMasterKey: "" # auto-generated if empty; admin-scoped key for Meilisearch nodes (plan §9 Model B)
|
||||
adminApiKey: "" # auto-generated if empty; ignored when existingSecret is set
|
||||
adminSessionSealKey: "" # 64-byte base64 key for sealing admin session cookies; auto-generated if empty (plan §9)
|
||||
searchUiJwtSecret: "" # HMAC secret for search UI JWTs; auto-generated when search_ui.enabled=true (plan §9)
|
||||
searchUiJwtSecretPrevious: "" # Previous JWT secret during rotation overlap window (plan §9)
|
||||
searchUiSharedKey: "" # Shared key for X-Search-UI-Key header when search_ui.auth.mode=shared_key (plan §9)
|
||||
logLevel: info # RUST_LOG: trace, debug, info, warn, error
|
||||
existingSecret: "" # name of K8s Secret with masterKey, nodeMasterKey, adminApiKey
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
|
||||
# Task store backend (plan §4)
|
||||
# IMPORTANT: When replicas > 1, backend MUST be "redis" (enforced by values.schema.json)
|
||||
taskStore:
|
||||
backend: sqlite # sqlite | redis
|
||||
path: /data/miroir-tasks.db
|
||||
url: "" # for redis: redis://host:6379
|
||||
|
||||
# Admin UI
|
||||
admin:
|
||||
enabled: true
|
||||
|
||||
# Scatter policy for unavailable shards
|
||||
scatter:
|
||||
unavailableShardPolicy: partial # partial | error
|
||||
|
||||
# Horizontal Pod Autoscaler
|
||||
hpa:
|
||||
enabled: false # dev default; production: true (requires taskStore.backend=redis)
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# Ingress configuration
|
||||
ingress:
|
||||
enabled: false
|
||||
className: "nginx"
|
||||
annotations: {}
|
||||
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
hosts:
|
||||
- host: search.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls: []
|
||||
# - secretName: search-tls
|
||||
# hosts:
|
||||
# - search.example.com
|
||||
|
||||
# Resource limits (plan §14.2: 2 vCPU / 3.75 GB per pod)
|
||||
resources:
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 3584Mi # 3.5 GiB (leaves headroom under 3.75 GB envelope)
|
||||
memory: 3840Mi
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
cdc:
|
||||
enabled: true
|
||||
emit_ttl_deletes: false
|
||||
emit_internal_writes: false
|
||||
buffer:
|
||||
primary: memory # memory | pvc | redis
|
||||
overflow: drop # drop | pvc | redis
|
||||
pvc_size: 10Gi
|
||||
pvc_storage_class: ""
|
||||
memory_bytes: 67108864
|
||||
# §14.8 Resource-aware configuration defaults
|
||||
# All defaults sized for 2 vCPU / 3.75 GB envelope
|
||||
server:
|
||||
max_body_bytes: 104857600 # 100 MiB per request
|
||||
max_concurrent_requests: 500
|
||||
request_timeout_ms: 30000
|
||||
connection_pool_per_node:
|
||||
max_idle: 32
|
||||
max_total: 128
|
||||
idle_timeout_s: 60
|
||||
task_registry:
|
||||
cache_size: 10000
|
||||
redis_pool_max: 50
|
||||
ttl_seconds: 604800 # 7 days
|
||||
prune_interval_s: 300
|
||||
prune_batch_size: 10000
|
||||
idempotency:
|
||||
enabled: true
|
||||
max_cached_keys: 1000000 # ~100 MB
|
||||
ttl_seconds: 86400
|
||||
session_pinning:
|
||||
enabled: true
|
||||
ttl_seconds: 900
|
||||
max_sessions: 100000 # ~50 MB
|
||||
wait_strategy: block # block | route_pin
|
||||
max_wait_ms: 5000
|
||||
query_coalescing:
|
||||
enabled: true
|
||||
window_ms: 50
|
||||
max_subscribers: 1000
|
||||
max_pending_queries: 10000
|
||||
anti_entropy:
|
||||
enabled: true
|
||||
schedule: every 6h
|
||||
shards_per_pass: 0 # 0 = all shards
|
||||
max_read_concurrency: 2
|
||||
fingerprint_batch_size: 1000
|
||||
auto_repair: true
|
||||
updated_at_field: _miroir_updated_at
|
||||
resharding:
|
||||
enabled: true
|
||||
backfill_concurrency: 4
|
||||
backfill_batch_size: 1000
|
||||
throttle_docs_per_sec: 0 # 0 = no throttle
|
||||
verify_before_swap: true
|
||||
retain_old_index_hours: 48
|
||||
allowed_windows: [] # e.g., ["22:00-04:00 UTC"]
|
||||
peer_discovery:
|
||||
service_name: "" # default: "<fullname>-headless" (auto-derived from release name)
|
||||
refresh_interval_s: 15
|
||||
leader_election:
|
||||
enabled: true # auto-true when replicas > 1
|
||||
lease_ttl_s: 10
|
||||
renew_interval_s: 3
|
||||
cpu: 1000m
|
||||
memory: 2048Mi
|
||||
|
||||
search_ui:
|
||||
enabled: true
|
||||
scoped_key_max_age_days: 60 # Maximum lifetime of a scoped Meilisearch key (days)
|
||||
scoped_key_rotate_before_expiry_days: 30 # Rotate this many days before max_age (must be < max_age)
|
||||
scoped_key_rotation_drain_s: 120 # Seconds to wait for all pods to observe new key before revoking old
|
||||
|
||||
admin_ui:
|
||||
enabled: true
|
||||
path: "/_miroir/admin"
|
||||
auth: key # key | oauth (future) | none (dev only)
|
||||
session_ttl_s: 3600 # 1 hour
|
||||
read_only_mode: false
|
||||
allowed_origins:
|
||||
- "same-origin"
|
||||
cors_allowed_origins: []
|
||||
rate_limit:
|
||||
per_ip: "10/minute" # Rate limit per source IP
|
||||
backend: redis # redis | local (schema enforces redis when replicas > 1)
|
||||
redis_key_prefix: "miroir:ratelimit:adminlogin:"
|
||||
redis_ttl_s: 60
|
||||
failed_attempt_threshold: 5 # Consecutive failures before exponential backoff
|
||||
backoff_start_minutes: 10 # Initial backoff after threshold
|
||||
backoff_max_hours: 24 # Maximum backoff cap
|
||||
|
||||
taskStore:
|
||||
backend: sqlite # sqlite | redis
|
||||
path: /data/miroir-tasks.db
|
||||
url: "" # for redis: redis://host:6379
|
||||
|
||||
# Horizontal Pod Autoscaler (disabled by default for dev)
|
||||
# Per plan §14.4, requires prometheus-adapter when custom metrics are enabled
|
||||
# When HPA is enabled, prometheus-adapter is auto-enabled as a dependency
|
||||
hpa:
|
||||
enabled: false
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 75
|
||||
targetRequestsInFlight: "500" # type: Pods AverageValue (per-pod metric)
|
||||
targetBackgroundQueueDepth: "10" # type: External Value (global metric)
|
||||
annotations: {}
|
||||
behavior:
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 300
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 50
|
||||
periodSeconds: 60
|
||||
scaleUp:
|
||||
stabilizationWindowSeconds: 0
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 100
|
||||
periodSeconds: 30
|
||||
- type: Pods
|
||||
value: 2
|
||||
periodSeconds: 60
|
||||
selectPolicy: Max
|
||||
|
||||
# ServiceAccount
|
||||
serviceAccount:
|
||||
create: true
|
||||
name: "" # defaults to release name
|
||||
annotations: {}
|
||||
|
||||
# Services
|
||||
service:
|
||||
type: ClusterIP
|
||||
annotations: {}
|
||||
ports:
|
||||
http: 7700
|
||||
metrics: 9090
|
||||
|
||||
headless:
|
||||
annotations: {}
|
||||
|
||||
# Meilisearch StatefulSet
|
||||
meilisearch:
|
||||
enabled: true
|
||||
image:
|
||||
repository: getmeilisearch/meilisearch
|
||||
tag: v1.12
|
||||
pullPolicy: IfNotPresent
|
||||
replicas: 2 # 1 group × 2 nodes (dev default)
|
||||
nodesPerGroup: 2 # nodes per replica group
|
||||
# Pod annotations
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
resources:
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 2Gi
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 10Gi
|
||||
storageClass: "" # uses default storage class
|
||||
env: []
|
||||
masterKey: "" # defaults to auto-generated; NOT zero-downtime rotatable (fixed at process start)
|
||||
|
||||
# Redis deployment (only when taskStore.backend=redis)
|
||||
# Pod security context
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
|
||||
# Container security context
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
# Service configuration
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 7700
|
||||
annotations: {}
|
||||
|
||||
# Existing secret with keys: masterKey, nodeMasterKey, adminApiKey
|
||||
existingSecret: ""
|
||||
|
||||
# Redis deployment (when taskStore.backend=redis)
|
||||
redis:
|
||||
enabled: false # dev default: enable for production
|
||||
enabled: false
|
||||
image:
|
||||
repository: redis
|
||||
tag: 7.4-alpine
|
||||
tag: 7-alpine
|
||||
pullPolicy: IfNotPresent
|
||||
replicas: 1
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
memory: 256Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 5Gi
|
||||
enabled: false
|
||||
size: 1Gi
|
||||
storageClass: ""
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 6379
|
||||
auth:
|
||||
enabled: true
|
||||
existingSecret: "" # auto-generated as <release>-redis-secret when empty; password comes from K8s Secret or ESO
|
||||
|
||||
# Prometheus Operator integration (plan §10 + §14.9)
|
||||
serviceMonitor:
|
||||
enabled: false # requires prometheus-operator in cluster
|
||||
interval: 30s
|
||||
# Service account
|
||||
serviceAccount:
|
||||
create: true
|
||||
annotations: {}
|
||||
|
||||
prometheusRule:
|
||||
enabled: false # requires prometheus-operator in cluster
|
||||
annotations: {}
|
||||
|
||||
# Grafana dashboard ConfigMap (requires grafana-dashboard sidecar)
|
||||
dashboards:
|
||||
enabled: false # creates a ConfigMap labeled grafana_dashboard=1
|
||||
annotations: {}
|
||||
|
||||
# OpenTelemetry tracing (plan §10)
|
||||
tracing:
|
||||
enabled: false # disabled by default for zero overhead
|
||||
endpoint: "http://tempo.monitoring.svc:4317" # OTLP gRPC endpoint
|
||||
serviceName: "miroir" # service name for trace identification
|
||||
sampleRate: 0.1 # head-based sampling: 0.1 = ~10% of requests traced
|
||||
|
||||
# External Secrets Operator integration (plan §6 + §9)
|
||||
# When enabled, ESO pulls secrets from OpenBao via ClusterSecretStore.
|
||||
# Set miroir.existingSecret to "<release>-secret" to use the ESO-managed Secret.
|
||||
eso:
|
||||
enabled: false # disabled by default; enable for production with OpenBao
|
||||
refreshInterval: "15m" # how often ESO re-syncs from the backend
|
||||
secretStoreRef:
|
||||
name: "openbao-backend" # ClusterSecretStore name in the cluster
|
||||
kind: "ClusterSecretStore"
|
||||
secretPath: "kv/search/miroir" # OpenBao KV v2 path
|
||||
includePreviousJwt: false # set true during JWT rotation overlap window
|
||||
includeSharedKey: false # set true when search_ui.auth.mode=shared_key
|
||||
includeRedisPassword: false # set true when redis.auth.enabled=true
|
||||
|
||||
# Prometheus Adapter (plan §14.4)
|
||||
# Required for HPA custom metrics (miroir_requests_in_flight, miroir_background_queue_depth).
|
||||
# When HPA is enabled, prometheus-adapter is automatically installed as a dependency.
|
||||
prometheusAdapter:
|
||||
enabled: false # auto-enabled when hpa.enabled=true
|
||||
# Prometheus adapter configuration (plan §14.4)
|
||||
# Custom metrics rules for Miroir HPA
|
||||
customMetrics:
|
||||
# Per-pod metric: miroir_requests_in_flight (type: Pods AverageValue)
|
||||
- name: miroir_requests_in_flight
|
||||
metricsQuery: |
|
||||
sum(mirol_requests_in_flight{<<.LabelMatchers>>}) by (<<.GroupBy>>)
|
||||
type: Pods
|
||||
resource:
|
||||
name: miroir_requests_in_flight
|
||||
container: miroir
|
||||
# Global metric: miroir_background_queue_depth (type: External Value)
|
||||
- name: miroir_background_queue_depth
|
||||
metricsQuery: |
|
||||
sum(miroir_background_queue_depth{<<.LabelMatchers>>})
|
||||
type: Value
|
||||
name: ""
|
||||
|
|
|
|||
99
coverage/html/control.js
Normal file
99
coverage/html/control.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
|
||||
function next_uncovered(selector, reverse, scroll_selector) {
|
||||
function visit_element(element) {
|
||||
element.classList.add("seen");
|
||||
element.classList.add("selected");
|
||||
|
||||
if (!scroll_selector) {
|
||||
scroll_selector = "tr:has(.selected) td.line-number"
|
||||
}
|
||||
|
||||
const scroll_to = document.querySelector(scroll_selector);
|
||||
if (scroll_to) {
|
||||
scroll_to.scrollIntoView({behavior: "smooth", block: "center", inline: "end"});
|
||||
}
|
||||
}
|
||||
|
||||
function select_one() {
|
||||
if (!reverse) {
|
||||
const previously_selected = document.querySelector(".selected");
|
||||
|
||||
if (previously_selected) {
|
||||
previously_selected.classList.remove("selected");
|
||||
}
|
||||
|
||||
return document.querySelector(selector + ":not(.seen)");
|
||||
} else {
|
||||
const previously_selected = document.querySelector(".selected");
|
||||
|
||||
if (previously_selected) {
|
||||
previously_selected.classList.remove("selected");
|
||||
previously_selected.classList.remove("seen");
|
||||
}
|
||||
|
||||
const nodes = document.querySelectorAll(selector + ".seen");
|
||||
if (nodes) {
|
||||
const last = nodes[nodes.length - 1]; // last
|
||||
return last;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reset_all() {
|
||||
if (!reverse) {
|
||||
const all_seen = document.querySelectorAll(selector + ".seen");
|
||||
|
||||
if (all_seen) {
|
||||
all_seen.forEach(e => e.classList.remove("seen"));
|
||||
}
|
||||
} else {
|
||||
const all_seen = document.querySelectorAll(selector + ":not(.seen)");
|
||||
|
||||
if (all_seen) {
|
||||
all_seen.forEach(e => e.classList.add("seen"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const uncovered = select_one();
|
||||
|
||||
if (uncovered) {
|
||||
visit_element(uncovered);
|
||||
} else {
|
||||
reset_all();
|
||||
|
||||
const uncovered = select_one();
|
||||
|
||||
if (uncovered) {
|
||||
visit_element(uncovered);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function next_line(reverse) {
|
||||
next_uncovered("td.uncovered-line", reverse)
|
||||
}
|
||||
|
||||
function next_region(reverse) {
|
||||
next_uncovered("span.red.region", reverse);
|
||||
}
|
||||
|
||||
function next_branch(reverse) {
|
||||
next_uncovered("span.red.branch", reverse);
|
||||
}
|
||||
|
||||
document.addEventListener("keypress", function(event) {
|
||||
const reverse = event.shiftKey;
|
||||
if (event.code == "KeyL") {
|
||||
next_line(reverse);
|
||||
}
|
||||
if (event.code == "KeyB") {
|
||||
next_branch(reverse);
|
||||
}
|
||||
if (event.code == "KeyR") {
|
||||
next_region(reverse);
|
||||
}
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
coverage/html/index.html
Normal file
1
coverage/html/index.html
Normal file
File diff suppressed because one or more lines are too long
194
coverage/html/style.css
Normal file
194
coverage/html/style.css
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
.red {
|
||||
background-color: #f004;
|
||||
}
|
||||
.cyan {
|
||||
background-color: cyan;
|
||||
}
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, sans-serif;
|
||||
}
|
||||
pre {
|
||||
margin-top: 0px !important;
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
.source-name-title {
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid #8888;
|
||||
background-color: #0002;
|
||||
line-height: 35px;
|
||||
}
|
||||
.centered {
|
||||
display: table;
|
||||
margin-left: left;
|
||||
margin-right: auto;
|
||||
border: 1px solid #8888;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.expansion-view {
|
||||
margin-left: 0px;
|
||||
margin-top: 5px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid #8888;
|
||||
border-radius: 3px;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.light-row {
|
||||
border: 1px solid #8888;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
.light-row-bold {
|
||||
border: 1px solid #8888;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
.column-entry {
|
||||
text-align: left;
|
||||
}
|
||||
.column-entry-bold {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
.column-entry-yellow {
|
||||
text-align: left;
|
||||
background-color: #ff06;
|
||||
}
|
||||
.column-entry-red {
|
||||
text-align: left;
|
||||
background-color: #f004;
|
||||
}
|
||||
.column-entry-gray {
|
||||
text-align: left;
|
||||
background-color: #fff4;
|
||||
}
|
||||
.column-entry-green {
|
||||
text-align: left;
|
||||
background-color: #0f04;
|
||||
}
|
||||
.line-number {
|
||||
text-align: right;
|
||||
}
|
||||
.covered-line {
|
||||
text-align: right;
|
||||
color: #06d;
|
||||
}
|
||||
.uncovered-line {
|
||||
text-align: right;
|
||||
color: #d00;
|
||||
}
|
||||
.uncovered-line.selected {
|
||||
color: #f00;
|
||||
font-weight: bold;
|
||||
}
|
||||
.region.red.selected {
|
||||
background-color: #f008;
|
||||
font-weight: bold;
|
||||
}
|
||||
.branch.red.selected {
|
||||
background-color: #f008;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline;
|
||||
background-color: #bef;
|
||||
text-decoration: none;
|
||||
}
|
||||
.tooltip span.tooltip-content {
|
||||
position: absolute;
|
||||
width: 100px;
|
||||
margin-left: -50px;
|
||||
color: #FFFFFF;
|
||||
background: #000000;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
visibility: hidden;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.tooltip span.tooltip-content:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
width: 0; height: 0;
|
||||
border-top: 8px solid #000000;
|
||||
border-right: 8px solid transparent;
|
||||
border-left: 8px solid transparent;
|
||||
}
|
||||
:hover.tooltip span.tooltip-content {
|
||||
visibility: visible;
|
||||
opacity: 0.8;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
z-index: 999;
|
||||
}
|
||||
th, td {
|
||||
vertical-align: top;
|
||||
padding: 2px 8px;
|
||||
border-collapse: collapse;
|
||||
border-right: 1px solid #8888;
|
||||
border-left: 1px solid #8888;
|
||||
text-align: left;
|
||||
}
|
||||
td pre {
|
||||
display: inline-block;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
td:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
td:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
tr:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) {
|
||||
background-color: #8884;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
.control {
|
||||
position: fixed;
|
||||
top: 0em;
|
||||
right: 0em;
|
||||
padding: 1em;
|
||||
background: #FFF8;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #222;
|
||||
color: whitesmoke;
|
||||
}
|
||||
tr:hover {
|
||||
background-color: #111;
|
||||
}
|
||||
.covered-line {
|
||||
color: #39f;
|
||||
}
|
||||
.uncovered-line {
|
||||
color: #f55;
|
||||
}
|
||||
.tooltip {
|
||||
background-color: #068;
|
||||
}
|
||||
.control {
|
||||
background: #2228;
|
||||
}
|
||||
tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) {
|
||||
background-color: #8884;
|
||||
}
|
||||
}
|
||||
4593
coverage/lcov.info
Normal file
4593
coverage/lcov.info
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -7,30 +7,49 @@ repository.workspace = true
|
|||
autobenches = false
|
||||
|
||||
[dependencies]
|
||||
# Core serialization
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = "0.9"
|
||||
twox-hash = "2"
|
||||
|
||||
# Error handling and logging
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
# UUIDs
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
|
||||
# Configuration
|
||||
config = "0.14"
|
||||
rusqlite = { workspace = true }
|
||||
futures-util = "0.3"
|
||||
# Redis support (optional — enable via `redis-store` feature)
|
||||
redis = { version = "0.27", features = ["aio", "tokio-comp", "connection-manager"], optional = true }
|
||||
|
||||
# Crypto
|
||||
rand = "0.8"
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
|
||||
# Task store backends
|
||||
rusqlite = { workspace = true }
|
||||
redis = { version = "0.27", features = ["aio", "tokio-comp", "connection-manager"], optional = true }
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "macros", "fs", "io-util"] }
|
||||
async-trait = "0.1"
|
||||
rand = "0.8"
|
||||
futures-util = "0.3"
|
||||
|
||||
# HTTP
|
||||
reqwest = { version = "0.12", features = ["json"], default-features = false }
|
||||
urlencoding = "2"
|
||||
sha2 = "0.10"
|
||||
|
||||
# Utilities
|
||||
indexmap = "2"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
regex = "1"
|
||||
|
||||
# DNS peer discovery (optional)
|
||||
trust-dns-resolver = { version = "0.12", optional = true }
|
||||
# Axum integration (optional — enable via `axum` feature)
|
||||
|
||||
# Axum integration (optional)
|
||||
axum = { version = "0.7", optional = true }
|
||||
|
||||
# Raft prototype (P12.OP2 research) — not for production use
|
||||
|
|
@ -45,13 +64,18 @@ redis-store = ["redis"]
|
|||
axum = ["dep:axum"]
|
||||
peer-discovery = ["trust-dns-resolver"]
|
||||
test-helpers = []
|
||||
|
||||
# Enable when openraft compiles on stable Rust:
|
||||
# raft-full = ["openraft", "bincode"]
|
||||
# (openraft dep removed from manifest — restore when upstream fixes let_chains on stable)
|
||||
|
||||
[[bin]]
|
||||
name = "bench-reshard-load"
|
||||
path = "benches/reshard_load.rs"
|
||||
[[bench]]
|
||||
name = "reshard_load"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "score_comparability"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "merger_bench"
|
||||
|
|
@ -68,9 +92,10 @@ harness = false
|
|||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
proptest = "1"
|
||||
criterion = "0.5"
|
||||
criterion = { workspace = true }
|
||||
tokio = { version = "1", features = ["rt", "macros", "time"] }
|
||||
testcontainers = "0.23"
|
||||
testcontainers-modules = { version = "0.11", features = ["redis"] }
|
||||
mockall = "0.13"
|
||||
meilisearch-sdk = { workspace = true }
|
||||
arbitrary = { version = "1", features = ["derive"] }
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ fn main() {
|
|||
result.old_shard_cv,
|
||||
result.new_shard_cv,
|
||||
);
|
||||
println!(" (computed in {:.2?})", elapsed);
|
||||
println!(" (computed in {elapsed:.2?})");
|
||||
}
|
||||
|
||||
println!();
|
||||
|
|
@ -111,7 +111,7 @@ fn main() {
|
|||
println!();
|
||||
|
||||
for (label, params, result, _elapsed) in &results {
|
||||
println!("--- {} ---", label);
|
||||
println!("--- {label} ---");
|
||||
println!(" doc_size: {} bytes", params.doc_size_bytes);
|
||||
println!(
|
||||
" corpus: {} bytes ({:.2} GB)",
|
||||
|
|
@ -203,7 +203,7 @@ fn main() {
|
|||
let cv_ok = result.old_shard_cv < 0.05 && result.new_shard_cv < 0.05;
|
||||
|
||||
let status = |ok| if ok { "PASS" } else { "FAIL" };
|
||||
println!(" {}:", label);
|
||||
println!(" {label}:");
|
||||
println!(
|
||||
" storage amplification == 2.0×: {} ({:.4}×)",
|
||||
status(storage_ok),
|
||||
|
|
|
|||
253
crates/miroir-core/benches/score_comparability.rs
Normal file
253
crates/miroir-core/benches/score_comparability.rs
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
//! Score comparability benchmark: Plan §15 OP#4 validation.
|
||||
//!
|
||||
//! Runs a matrix of tests to validate that `_rankingScore` values remain
|
||||
//! comparable across shards with very different document-count distributions.
|
||||
//!
|
||||
//! For each configuration, we measure Kendall Tau correlation between
|
||||
//! sharded and ground-truth (single-index) result orderings across many
|
||||
//! random queries.
|
||||
|
||||
use miroir_core::score_comparability::{simulate, SimParams};
|
||||
|
||||
const THRESHOLD: f64 = 0.95;
|
||||
|
||||
fn main() {
|
||||
println!("╔══════════════════════════════════════════════════════════════════╗");
|
||||
println!("║ Score Comparability Benchmark — Plan §15 OP#4 Validation ║");
|
||||
println!("╚══════════════════════════════════════════════════════════════════╝");
|
||||
println!();
|
||||
|
||||
let test_matrix: Vec<(&str, SimParams)> = vec![
|
||||
(
|
||||
"Baseline: uniform distribution, small corpus",
|
||||
SimParams {
|
||||
total_docs: 10_000,
|
||||
shard_count: 8,
|
||||
skew_factor: 1.0, // Uniform
|
||||
num_queries: 1000,
|
||||
top_k: 20,
|
||||
seed: 42,
|
||||
},
|
||||
),
|
||||
(
|
||||
"Moderate skew (10×), medium corpus",
|
||||
SimParams {
|
||||
total_docs: 100_000,
|
||||
shard_count: 16,
|
||||
skew_factor: 10.0,
|
||||
num_queries: 1000,
|
||||
top_k: 20,
|
||||
seed: 42,
|
||||
},
|
||||
),
|
||||
(
|
||||
"High skew (100×), large corpus",
|
||||
SimParams {
|
||||
total_docs: 1_000_000,
|
||||
shard_count: 32,
|
||||
skew_factor: 100.0,
|
||||
num_queries: 1000,
|
||||
top_k: 20,
|
||||
seed: 42,
|
||||
},
|
||||
),
|
||||
(
|
||||
"Extreme skew (1000×), many shards",
|
||||
SimParams {
|
||||
total_docs: 500_000,
|
||||
shard_count: 64,
|
||||
skew_factor: 1000.0,
|
||||
num_queries: 1000,
|
||||
top_k: 20,
|
||||
seed: 42,
|
||||
},
|
||||
),
|
||||
(
|
||||
"Worst case: tiny sparse shards (0.01× median)",
|
||||
SimParams {
|
||||
total_docs: 200_000,
|
||||
shard_count: 32,
|
||||
skew_factor: 10000.0,
|
||||
num_queries: 1000,
|
||||
top_k: 20,
|
||||
seed: 42,
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
for (label, params) in &test_matrix {
|
||||
println!("Running: {label}");
|
||||
let start = std::time::Instant::now();
|
||||
let result = simulate(params);
|
||||
let elapsed = start.elapsed();
|
||||
results.push((label, params.clone(), result, elapsed));
|
||||
println!(" Completed in {elapsed:.2?}");
|
||||
println!();
|
||||
}
|
||||
|
||||
// Print summary table.
|
||||
println!("═══════════════════════════════════════════════════════════════════");
|
||||
println!("Summary Table");
|
||||
println!("═══════════════════════════════════════════════════════════════════");
|
||||
println!();
|
||||
|
||||
println!(
|
||||
"{:<45} {:>10} {:>10} {:>8} {:>8} {:>8} {:>8} {:>10}",
|
||||
"Scenario", "Docs", "Shards", "Skew", "PopCV", "Meanτ", "Stdτ", "%≥0.95"
|
||||
);
|
||||
println!("{}", "-".repeat(120));
|
||||
|
||||
for (label, params, result, _elapsed) in &results {
|
||||
let docs_str = format!("{:.1}M", params.total_docs as f64 / 1_000_000.0);
|
||||
let skew_str = format!("{:.0}×", params.skew_factor);
|
||||
let cv_str = format!("{:.2}%", result.aggregate.shard_pop_cv * 100.0);
|
||||
|
||||
println!(
|
||||
"{:<45} {:>10} {:>10} {:>8} {:>8} {:>8.3} {:>8.3} {:>10.1}%",
|
||||
label,
|
||||
docs_str,
|
||||
params.shard_count,
|
||||
skew_str,
|
||||
cv_str,
|
||||
result.aggregate.mean_kendall_tau,
|
||||
result.aggregate.std_kendall_tau,
|
||||
result.aggregate.percent_above_threshold
|
||||
);
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("═══════════════════════════════════════════════════════════════════");
|
||||
println!("Detailed Results (JSON)");
|
||||
println!("═══════════════════════════════════════════════════════════════════");
|
||||
println!();
|
||||
|
||||
for (label, params, result, elapsed) in &results {
|
||||
println!("--- {label} ---");
|
||||
println!(" configuration:");
|
||||
println!(" total_docs: {}", params.total_docs);
|
||||
println!(" shard_count: {}", params.shard_count);
|
||||
println!(" skew_factor: {:.0}×", params.skew_factor);
|
||||
println!(" num_queries: {}", params.num_queries);
|
||||
println!(" top_k: {}", params.top_k);
|
||||
println!();
|
||||
println!(" shard population:");
|
||||
println!(
|
||||
" cv: {:.4} ({:.2}%)",
|
||||
result.aggregate.shard_pop_cv,
|
||||
result.aggregate.shard_pop_cv * 100.0
|
||||
);
|
||||
println!(
|
||||
" max/median ratio: {:.2}×",
|
||||
result.aggregate.shard_pop_ratio
|
||||
);
|
||||
println!(" per-shard counts: {:?}", result.shard_doc_counts);
|
||||
println!();
|
||||
println!(" kendall tau:");
|
||||
println!(
|
||||
" mean: {:.4} ± {:.4}",
|
||||
result.aggregate.mean_kendall_tau, result.aggregate.std_kendall_tau
|
||||
);
|
||||
println!(
|
||||
" min: {:.4}, max: {:.4}",
|
||||
result.aggregate.min_kendall_tau, result.aggregate.max_kendall_tau
|
||||
);
|
||||
println!(
|
||||
" ≥ {}: {:.1}%",
|
||||
THRESHOLD, result.aggregate.percent_above_threshold
|
||||
);
|
||||
println!();
|
||||
println!(" jaccard similarity:");
|
||||
println!(" mean: {:.4}", result.aggregate.mean_jaccard);
|
||||
println!();
|
||||
println!(" first divergence:");
|
||||
println!(
|
||||
" mean position: {:.1}",
|
||||
result.aggregate.mean_first_divergence
|
||||
);
|
||||
println!();
|
||||
println!(" computed in: {elapsed:.2?}");
|
||||
println!();
|
||||
}
|
||||
|
||||
// Print worst-case queries (those with lowest Kendall Tau).
|
||||
println!("═══════════════════════════════════════════════════════════════════");
|
||||
println!("Worst-Case Queries (lowest τ)");
|
||||
println!("═══════════════════════════════════════════════════════════════════");
|
||||
println!();
|
||||
|
||||
for (label, _params, result, _elapsed) in &results {
|
||||
let mut worst_queries: Vec<_> = result.query_results.iter().collect();
|
||||
worst_queries.sort_by(|a, b| a.kendall_tau.partial_cmp(&b.kendall_tau).unwrap());
|
||||
|
||||
println!("--- {label} ---");
|
||||
for qr in worst_queries.iter().take(5) {
|
||||
println!(
|
||||
" Query {}: τ={:.4}, Jaccard={:.4}, first_div={}",
|
||||
qr.query_id, qr.kendall_tau, qr.jaccard_similarity, qr.first_divergence_position
|
||||
);
|
||||
println!(" Shard stats:");
|
||||
for stat in &qr.shard_score_stats {
|
||||
println!(
|
||||
" Shard {}: {} docs, {} hits, score range [{:.3}, {:.3}]",
|
||||
stat.shard_id, stat.doc_count, stat.hit_count, stat.min_score, stat.max_score
|
||||
);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
// Validate threshold.
|
||||
println!("═══════════════════════════════════════════════════════════════════");
|
||||
println!("Threshold Validation (τ ≥ {THRESHOLD:.2})");
|
||||
println!("═══════════════════════════════════════════════════════════════════");
|
||||
println!();
|
||||
|
||||
let mut all_pass = true;
|
||||
for (label, _params, result, _) in &results {
|
||||
let passes = result.aggregate.mean_kendall_tau >= THRESHOLD;
|
||||
let status = if passes { "PASS" } else { "FAIL" };
|
||||
println!(
|
||||
" [{}] {}: mean τ = {:.4}",
|
||||
status, label, result.aggregate.mean_kendall_tau
|
||||
);
|
||||
|
||||
if !passes {
|
||||
all_pass = false;
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
if all_pass {
|
||||
println!("All scenarios PASSED the τ ≥ {THRESHOLD:.2} threshold.");
|
||||
println!("Score comparability is maintained across shard population skew.");
|
||||
} else {
|
||||
println!("Some scenarios FAILED the τ ≥ {THRESHOLD:.2} threshold.");
|
||||
println!("Score normalization may be needed for skewed shard distributions.");
|
||||
}
|
||||
|
||||
// Generate JSON output for programmatic consumption.
|
||||
println!();
|
||||
println!("═══════════════════════════════════════════════════════════════════");
|
||||
println!("JSON Output");
|
||||
println!("═══════════════════════════════════════════════════════════════════");
|
||||
println!();
|
||||
|
||||
let json_output: Vec<serde_json::Value> = results
|
||||
.iter()
|
||||
.map(|(label, _params, result, _elapsed)| {
|
||||
serde_json::json!({
|
||||
"scenario": label,
|
||||
"aggregate": result.aggregate,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&json_output).unwrap());
|
||||
|
||||
// Exit with appropriate code.
|
||||
if !all_pass {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -69,3 +69,127 @@ pub fn from_yaml(yaml: &str) -> Result<MiroirConfig, ConfigError> {
|
|||
cfg.validate()?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_from_yaml_valid_config() {
|
||||
let yaml = r#"
|
||||
shards: 32
|
||||
replication_factor: 1
|
||||
cdc:
|
||||
enabled: false
|
||||
search_ui:
|
||||
rate_limit:
|
||||
backend: local
|
||||
nodes: []
|
||||
"#;
|
||||
let cfg = from_yaml(yaml).expect("should parse valid config");
|
||||
assert_eq!(cfg.shards, 32);
|
||||
assert_eq!(cfg.replication_factor, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_yaml_with_nodes() {
|
||||
let yaml = r#"
|
||||
shards: 64
|
||||
replication_factor: 1
|
||||
replica_groups: 2
|
||||
task_store:
|
||||
backend: redis
|
||||
url: "redis://localhost:6379"
|
||||
nodes:
|
||||
- id: "node1"
|
||||
address: "http://node1:7700"
|
||||
replica_group: 0
|
||||
- id: "node2"
|
||||
address: "http://node2:7700"
|
||||
replica_group: 1
|
||||
"#;
|
||||
let cfg = from_yaml(yaml).expect("should parse config with nodes");
|
||||
assert_eq!(cfg.nodes.len(), 2);
|
||||
assert_eq!(cfg.nodes[0].id, "node1");
|
||||
assert_eq!(cfg.nodes[1].replica_group, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_yaml_invalid_yaml_fails() {
|
||||
let yaml = r#"
|
||||
shards: 32
|
||||
replication_factor: invalid
|
||||
nodes: []
|
||||
"#;
|
||||
let result = from_yaml(yaml);
|
||||
assert!(result.is_err(), "should fail on invalid YAML");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_yaml_validation_fails_on_ha_with_sqlite() {
|
||||
let yaml = r#"
|
||||
shards: 64
|
||||
replication_factor: 2
|
||||
nodes: []
|
||||
"#;
|
||||
let result = from_yaml(yaml);
|
||||
assert!(result.is_err(), "should fail validation: RF=2 requires redis");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_yaml_validation_fails_on_zero_shards() {
|
||||
let yaml = r#"
|
||||
shards: 0
|
||||
replication_factor: 1
|
||||
nodes: []
|
||||
"#;
|
||||
let result = from_yaml(yaml);
|
||||
assert!(result.is_err(), "should fail validation: zero shards");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_yaml_with_all_sections() {
|
||||
let yaml = r#"
|
||||
shards: 64
|
||||
replication_factor: 1
|
||||
replica_groups: 2
|
||||
master_key: "test-key"
|
||||
node_master_key: "node-key"
|
||||
nodes:
|
||||
- id: "node1"
|
||||
address: "http://node1:7700"
|
||||
replica_group: 0
|
||||
task_store:
|
||||
backend: redis
|
||||
url: "redis://localhost:6379"
|
||||
admin:
|
||||
enabled: true
|
||||
api_key: "admin-key"
|
||||
health:
|
||||
interval_ms: 5000
|
||||
timeout_ms: 2000
|
||||
unhealthy_threshold: 3
|
||||
recovery_threshold: 2
|
||||
scatter:
|
||||
node_timeout_ms: 5000
|
||||
retry_on_timeout: true
|
||||
unavailable_shard_policy: partial
|
||||
rebalancer:
|
||||
auto_rebalance_on_recovery: true
|
||||
max_concurrent_migrations: 4
|
||||
migration_timeout_s: 3600
|
||||
server:
|
||||
port: 7700
|
||||
bind: "0.0.0.0"
|
||||
max_body_bytes: 104857600
|
||||
leader_election:
|
||||
enabled: true
|
||||
"#;
|
||||
let cfg = from_yaml(yaml).expect("should parse full config");
|
||||
assert_eq!(cfg.shards, 64);
|
||||
assert_eq!(cfg.master_key, "test-key");
|
||||
assert_eq!(cfg.admin.api_key, "admin-key");
|
||||
assert_eq!(cfg.health.interval_ms, 5000);
|
||||
assert_eq!(cfg.scatter.node_timeout_ms, 5000);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,14 +8,6 @@ pub fn validate(cfg: &MiroirConfig) -> Result<(), ConfigError> {
|
|||
));
|
||||
}
|
||||
|
||||
// replica_groups > 1 requires redis backend
|
||||
if cfg.replica_groups > 1 && cfg.task_store.backend == "sqlite" {
|
||||
return Err(ConfigError::Validation(
|
||||
"replica_groups > 1 requires task_store.backend = 'redis' (SQLite is single-writer)"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Nodes must belong to a valid replica group
|
||||
if cfg.replica_groups > 0 {
|
||||
for node in &cfg.nodes {
|
||||
|
|
@ -55,8 +47,7 @@ pub fn validate(cfg: &MiroirConfig) -> Result<(), ConfigError> {
|
|||
let rotate_before = cfg.search_ui.scoped_key_rotate_before_expiry_days;
|
||||
if rotate_before >= max_age {
|
||||
return Err(ConfigError::Validation(format!(
|
||||
"search_ui.scoped_key_rotate_before_expiry_days ({}) must be strictly less than scoped_key_max_age_days ({})",
|
||||
rotate_before, max_age
|
||||
"search_ui.scoped_key_rotate_before_expiry_days ({rotate_before}) must be strictly less than scoped_key_max_age_days ({max_age})"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
@ -78,11 +69,18 @@ pub fn validate(cfg: &MiroirConfig) -> Result<(), ConfigError> {
|
|||
));
|
||||
}
|
||||
|
||||
// Leader election should be enabled when replica_groups > 1
|
||||
if cfg.replica_groups > 1 && !cfg.leader_election.enabled {
|
||||
return Err(ConfigError::Validation(
|
||||
"leader_election.enabled must be true when replica_groups > 1".into(),
|
||||
));
|
||||
// replica_groups > 1 requires both redis backend and leader election
|
||||
if cfg.replica_groups > 1 {
|
||||
if cfg.task_store.backend != "redis" {
|
||||
return Err(ConfigError::Validation(
|
||||
"replica_groups > 1 requires task_store.backend = 'redis' (SQLite is single-writer)".into(),
|
||||
));
|
||||
}
|
||||
if !cfg.leader_election.enabled {
|
||||
return Err(ConfigError::Validation(
|
||||
"leader_election.enabled must be true when replica_groups > 1".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Tenant affinity dedicated_groups must be within valid range
|
||||
|
|
|
|||
|
|
@ -194,6 +194,11 @@ impl HedgingManager {
|
|||
pub fn should_hedge(&self, elapsed: Duration, deadline: Duration) -> bool {
|
||||
elapsed >= deadline
|
||||
}
|
||||
|
||||
/// Get configuration.
|
||||
pub fn config(&self) -> &HedgingConfig {
|
||||
&self.config
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HedgingManager {
|
||||
|
|
@ -216,7 +221,6 @@ pub enum HedgeOutcome {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_config_default() {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ pub mod router;
|
|||
pub mod scatter;
|
||||
pub mod schema_migrations;
|
||||
pub mod scoped_key_rotation;
|
||||
pub mod score_comparability;
|
||||
pub mod session_pinning;
|
||||
pub mod settings;
|
||||
pub mod shadow;
|
||||
|
|
@ -55,8 +56,9 @@ pub mod tenant;
|
|||
pub mod topology;
|
||||
pub mod ttl;
|
||||
|
||||
#[cfg(feature = "raft-proto")]
|
||||
pub mod raft_proto;
|
||||
// Raft prototype temporarily disabled (openraft 0.9.22 fails on Rust 1.87)
|
||||
// #[cfg(feature = "raft-proto")]
|
||||
// pub mod raft_proto;
|
||||
|
||||
// Public re-exports
|
||||
pub use api_error::{ErrorType, MeilisearchError, MiroirCode};
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue