Current section

53 Versions

Jump to

Compare versions

35 files changed
+874 additions
-4252 deletions
  @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7 7
8 8 ## [Unreleased]
9 9
10 + ## [0.9.1] - 2026-01-09
11 +
12 + ### Added
13 + - `ClientSupervisor` wrapper for safe gRPC client supervision across gRPC variants.
14 + - gRPC server request logging interceptor with optional `:grpc_request_logging` and category-aware debug output.
15 + - `mix snakepit.python_test` task to bootstrap and run the Python test suite (supports `--no-bootstrap`).
16 + - Pool reconciliation loop to restore minimum worker counts after crash storms (configurable via `pool_reconcile_interval_ms` and `pool_reconcile_batch_size`).
17 + - Configurable restart intensity for worker starters and worker supervisors (`worker_starter_*` and `worker_supervisor_*` defaults).
18 +
19 + ### Changed
20 + - gRPC client and worker stream defaults now derive from `grpc_command_timeout/0` and `stream_timeout/0`.
21 + - Pool and worker execution now handle `:infinity` timeouts without deadline bookkeeping.
22 + - Python gRPC server now runs sync adapter calls in worker threads by default; use `thread_sensitive` metadata or `SNAKEPIT_THREAD_SENSITIVE` to keep execution on the main thread.
23 + - `Snakepit.Pool` metadata validation now accepts `Snakepit.Pool` as the default pool identifier.
24 + - gRPC is pinned to `0.11.5` and protobuf is pinned to `0.16.0` (override).
25 +
26 + ### Fixed
27 + - gRPC status code 4 now maps to `{:error, :timeout}` in the client.
28 + - Process group shutdown waits for group exit using `/proc` or `ps`, avoiding zombie false positives.
29 + - Test suite now tracks and terminates leaked external Python processes after runs.
30 +
10 31 ## [0.9.0] - 2026-01-02
11 32
12 33 ### Added
  @@ -1505,7 +1526,9 @@ This release also rolls up the previously undocumented fail-fast docs/tests work
1505 1526 - Configurable pool sizes and timeouts
1506 1527 - Built-in bridge scripts for Python and JavaScript
1507 1528
1508 - [Unreleased]: https://github.com/nshkrdotcom/snakepit/compare/v0.8.5...HEAD
1529 + [Unreleased]: https://github.com/nshkrdotcom/snakepit/compare/v0.9.1...HEAD
1530 + [0.9.1]: https://github.com/nshkrdotcom/snakepit/releases/tag/v0.9.1
1531 + [0.9.0]: https://github.com/nshkrdotcom/snakepit/releases/tag/v0.9.0
1509 1532 [0.8.5]: https://github.com/nshkrdotcom/snakepit/releases/tag/v0.8.5
1510 1533 [0.8.4]: https://github.com/nshkrdotcom/snakepit/releases/tag/v0.8.4
1511 1534 [0.8.3]: https://github.com/nshkrdotcom/snakepit/releases/tag/v0.8.3
  @@ -31,7 +31,7 @@ Add `snakepit` to your dependencies in `mix.exs`:
31 31 ```elixir
32 32 def deps do
33 33 [
34 - {:snakepit, "~> 0.9.0"}
34 + {:snakepit, "~> 0.9.1"}
35 35 ]
36 36 end
37 37 ```
  @@ -50,7 +50,7 @@ For higher-level Python integration with compile-time type generation, use [Snak
50 50
51 51 ```elixir
52 52 def deps do
53 - [{:snakebridge, "~> 0.8.0"}]
53 + [{:snakebridge, "~> 0.9.1"}]
54 54 end
55 55
56 56 def project do
  @@ -126,7 +126,7 @@ config :snakepit, log_level: :none # Complete silence
126 126 config :snakepit, log_level: :debug, log_categories: [:grpc, :pool]
127 127 ```
128 128
129 - ### Runtime Configurable Defaults (v0.8.8+)
129 + ### Runtime Configurable Defaults
130 130
131 131 All hardcoded timeout and sizing values are now configurable via `Application.get_env/3`.
132 132 Values are read at runtime, allowing configuration changes without recompilation.
  @@ -151,6 +151,16 @@ config :snakepit,
151 151 pool_startup_batch_size: 10, # Workers started per batch
152 152 pool_startup_batch_delay_ms: 500, # Delay between startup batches
153 153
154 + # Pool recovery
155 + pool_reconcile_interval_ms: 1_000, # Reconcile worker count interval (0 disables)
156 + pool_reconcile_batch_size: 2, # Max workers respawned per tick
157 +
158 + # Worker supervisor restart intensity
159 + worker_starter_max_restarts: 3,
160 + worker_starter_max_seconds: 5,
161 + worker_supervisor_max_restarts: 3,
162 + worker_supervisor_max_seconds: 5,
163 +
154 164 # Retry policy
155 165 retry_max_attempts: 3,
156 166 retry_backoff_sequence: [100, 200, 400, 800, 1600],
  @@ -1,55 +0,0 @@
1 - <svg width="48" height="48" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
2 - <defs>
3 - <linearGradient id="pitBg" x1="0%" y1="0%" x2="100%" y2="100%">
4 - <stop offset="0%" style="stop-color:#1c2331; stop-opacity:1" />
5 - <stop offset="100%" style="stop-color:#0d1117; stop-opacity:1" />
6 - </linearGradient>
7 - <linearGradient id="snake1" x1="0%" y1="0%" x2="100%" y2="100%">
8 - <stop offset="0%" style="stop-color:#306998; stop-opacity:1" />
9 - <stop offset="100%" style="stop-color:#4b8bbe; stop-opacity:1" />
10 - </linearGradient>
11 - <linearGradient id="snake2" x1="0%" y1="0%" x2="100%" y2="100%">
12 - <stop offset="0%" style="stop-color:#ffd43b; stop-opacity:1" />
13 - <stop offset="100%" style="stop-color:#ffe873; stop-opacity:1" />
14 - </linearGradient>
15 - <filter id="glow">
16 - <feGaussianBlur stdDeviation="1" result="coloredBlur"/>
17 - <feMerge>
18 - <feMergeNode in="coloredBlur"/>
19 - <feMergeNode in="SourceGraphic"/>
20 - </feMerge>
21 - </filter>
22 - </defs>
23 -
24 - <!-- Hexagonal Pit -->
25 - <polygon points="50,8 90,28 90,72 50,92 10,72 10,28"
26 - fill="url(#pitBg)" stroke="#2a3441" stroke-width="2"/>
27 -
28 - <!-- Blue Snake (left/top) -->
29 - <g>
30 - <path d="M 30,75 Q 25,60 30,45 Q 35,30 45,25 Q 55,20 60,30 Q 62,35 58,40"
31 - fill="none" stroke="url(#snake1)" stroke-width="7" stroke-linecap="round"/>
32 - <path d="M 30,45 L 24,42 L 28,38 C 32,36 34,38 32,42 Z"
33 - fill="url(#snake1)"/>
34 - <circle cx="28" cy="40" r="1.5" fill="#ffd43b" filter="url(#glow)"/>
35 - </g>
36 -
37 - <!-- Yellow Snake (right/bottom) -->
38 - <g>
39 - <path d="M 70,25 Q 75,40 70,55 Q 65,70 55,75 Q 45,80 40,70 Q 38,65 42,60"
40 - fill="none" stroke="url(#snake2)" stroke-width="7" stroke-linecap="round"/>
41 - <path d="M 70,55 L 76,58 L 72,62 C 68,64 66,62 68,58 Z"
42 - fill="url(#snake2)"/>
43 - <circle cx="72" cy="60" r="1.5" fill="#306998" filter="url(#glow)"/>
44 - </g>
45 -
46 - <!-- Intertwined center section -->
47 - <path d="M 58,40 Q 50,45 42,60"
48 - fill="none" stroke="url(#snake1)" stroke-width="6" stroke-linecap="round" opacity="0.9"/>
49 - <path d="M 42,60 Q 50,55 58,40"
50 - fill="none" stroke="url(#snake2)" stroke-width="6" stroke-linecap="round" opacity="0.9"/>
51 -
52 - <!-- Pit edge highlights -->
53 - <polygon points="50,8 90,28 90,72 50,92 10,72 10,28"
54 - fill="none" stroke="#4a5568" stroke-width="1" opacity="0.3"/>
55 - </svg>
  @@ -1,387 +0,0 @@
1 - <svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
2 - <defs>
3 - <!-- Python Blue Gradient -->
4 - <linearGradient id="pythonBlue" x1="0%" y1="0%" x2="100%" y2="100%">
5 - <stop offset="0%" stop-color="#306998"/>
6 - <stop offset="50%" stop-color="#4b8bbe"/>
7 - <stop offset="100%" stop-color="#5a9fd4"/>
8 - </linearGradient>
9 -
10 - <!-- Python Yellow Gradient -->
11 - <linearGradient id="pythonYellow" x1="0%" y1="0%" x2="100%" y2="100%">
12 - <stop offset="0%" stop-color="#ffd43b"/>
13 - <stop offset="50%" stop-color="#ffe873"/>
14 - <stop offset="100%" stop-color="#fff3a3"/>
15 - </linearGradient>
16 -
17 - <!-- Dark Pit Gradient (radial for depth) -->
18 - <radialGradient id="pitDepth" cx="50%" cy="50%" r="60%">
19 - <stop offset="0%" stop-color="#0a0f1a"/>
20 - <stop offset="40%" stop-color="#0d121f"/>
21 - <stop offset="70%" stop-color="#141b2d"/>
22 - <stop offset="100%" stop-color="#1c2331"/>
23 - </radialGradient>
24 -
25 - <!-- Hexagon Border Gradient -->
26 - <linearGradient id="hexBorder" x1="0%" y1="0%" x2="100%" y2="100%">
27 - <stop offset="0%" stop-color="#4b8bbe"/>
28 - <stop offset="50%" stop-color="#ffd43b"/>
29 - <stop offset="100%" stop-color="#4b8bbe"/>
30 - </linearGradient>
31 -
32 - <!-- Snake Scale Pattern -->
33 - <pattern id="snakeScales" patternUnits="userSpaceOnUse" width="6" height="5">
34 - <ellipse cx="3" cy="2.5" rx="2.8" ry="2.2" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="0.4"/>
35 - </pattern>
36 -
37 - <!-- Eye Glow Filter -->
38 - <filter id="eyeGlow" x="-50%" y="-50%" width="200%" height="200%">
39 - <feGaussianBlur stdDeviation="1.5" result="glow"/>
40 - <feMerge>
41 - <feMergeNode in="glow"/>
42 - <feMergeNode in="glow"/>
43 - <feMergeNode in="SourceGraphic"/>
44 - </feMerge>
45 - </filter>
46 -
47 - <!-- Snake Body Glow -->
48 - <filter id="snakeGlow" x="-20%" y="-20%" width="140%" height="140%">
49 - <feGaussianBlur stdDeviation="2" result="blur"/>
50 - <feMerge>
51 - <feMergeNode in="blur"/>
52 - <feMergeNode in="SourceGraphic"/>
53 - </feMerge>
54 - </filter>
55 -
56 - <!-- Inner Shadow for Pit -->
57 - <filter id="pitShadow" x="-10%" y="-10%" width="120%" height="120%">
58 - <feGaussianBlur stdDeviation="4" result="shadow"/>
59 - <feOffset dx="0" dy="2"/>
60 - <feComposite in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1"/>
61 - <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0"/>
62 - <feMerge>
63 - <feMergeNode/>
64 - <feMergeNode in="SourceGraphic"/>
65 - </feMerge>
66 - </filter>
67 -
68 - <!-- Ambient Light -->
69 - <filter id="ambientLight">
70 - <feSpecularLighting surfaceScale="3" specularConstant="0.8" specularExponent="25" lighting-color="#ffffff" result="specOut">
71 - <fePointLight x="100" y="50" z="80"/>
72 - </feSpecularLighting>
73 - <feComposite in="SourceGraphic" in2="specOut" operator="arithmetic" k1="0" k2="1" k3="0.3" k4="0"/>
74 - </filter>
75 -
76 - <!-- Hexagon Clip Path -->
77 - <clipPath id="hexClip">
78 - <polygon points="100,8 181.6,55 181.6,145 100,192 18.4,145 18.4,55"/>
79 - </clipPath>
80 -
81 - <!-- Snake Body Texture -->
82 - <linearGradient id="blueSnakeBody" x1="0%" y1="0%" x2="0%" y2="100%">
83 - <stop offset="0%" stop-color="#5a9fd4"/>
84 - <stop offset="30%" stop-color="#4b8bbe"/>
85 - <stop offset="70%" stop-color="#306998"/>
86 - <stop offset="100%" stop-color="#254f73"/>
87 - </linearGradient>
88 -
89 - <linearGradient id="yellowSnakeBody" x1="0%" y1="0%" x2="0%" y2="100%">
90 - <stop offset="0%" stop-color="#fff3a3"/>
91 - <stop offset="30%" stop-color="#ffe873"/>
92 - <stop offset="70%" stop-color="#ffd43b"/>
93 - <stop offset="100%" stop-color="#e6b800"/>
94 - </linearGradient>
95 -
96 - <!-- Subtle noise texture -->
97 - <filter id="noiseTexture">
98 - <feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="4" result="noise"/>
99 - <feColorMatrix type="saturate" values="0"/>
100 - <feBlend in="SourceGraphic" in2="noise" mode="multiply" result="blended"/>
101 - <feComposite in="blended" in2="SourceAlpha" operator="in"/>
102 - </filter>
103 - </defs>
104 -
105 - <!-- Outer Hexagon Border/Glow -->
106 - <polygon
107 - points="100,5 184.6,53.5 184.6,146.5 100,195 15.4,146.5 15.4,53.5"
108 - fill="none"
109 - stroke="url(#hexBorder)"
110 - stroke-width="2"
111 - opacity="0.6"
112 - />
113 -
114 - <!-- Main Hexagon (The Pit) -->
115 - <polygon
116 - points="100,8 181.6,55 181.6,145 100,192 18.4,145 18.4,55"
117 - fill="url(#pitDepth)"
118 - stroke="url(#hexBorder)"
119 - stroke-width="3"
120 - filter="url(#pitShadow)"
121 - />
122 -
123 - <!-- Pit Floor Pattern (subtle) -->
124 - <g clip-path="url(#hexClip)" opacity="0.3">
125 - <circle cx="100" cy="130" r="50" fill="none" stroke="#1a2540" stroke-width="1"/>
126 - <circle cx="100" cy="130" r="35" fill="none" stroke="#1a2540" stroke-width="1"/>
127 - <circle cx="100" cy="130" r="20" fill="none" stroke="#1a2540" stroke-width="1"/>
128 - </g>
129 -
130 - <!-- Snake Pit Contents -->
131 - <g clip-path="url(#hexClip)">
132 -
133 - <!-- ===== DEEPEST LAYER - Faded coils at bottom ===== -->
134 -
135 - <!-- Blue snake coil in deep background -->
136 - <g filter="url(#snakeGlow)" opacity="0.35">
137 - <path
138 - d="M 60,148 C 75,155 95,145 110,150 C 125,155 135,148 145,142"
139 - fill="none"
140 - stroke="url(#blueSnakeBody)"
141 - stroke-width="11"
142 - stroke-linecap="round"
143 - />
144 - </g>
145 -
146 - <!-- Yellow snake loop deep -->
147 - <g filter="url(#snakeGlow)" opacity="0.4">
148 - <path
149 - d="M 140,132 C 130,140 112,130 95,136 C 78,142 68,136 60,128"
150 - fill="none"
151 - stroke="url(#yellowSnakeBody)"
152 - stroke-width="9"
153 - stroke-linecap="round"
154 - />
155 - </g>
156 -
157 - <!-- ===== MID LAYER - More visible snakes ===== -->
158 -
159 - <!-- Blue snake winding from bottom-left -->
160 - <g filter="url(#snakeGlow)" opacity="0.6">
161 - <path
162 - d="M 52,130 C 58,118 72,122 78,110 C 84,98 70,92 62,102 C 54,112 50,120 55,128"
163 - fill="none"
164 - stroke="url(#blueSnakeBody)"
165 - stroke-width="13"
166 - stroke-linecap="round"
167 - />
168 - </g>
169 -
170 - <!-- Yellow snake coiling right side -->
171 - <g filter="url(#snakeGlow)" opacity="0.65">
172 - <path
173 - d="M 148,125 C 142,112 128,118 124,105 C 120,92 138,88 144,100 C 150,112 152,120 146,128"
174 - fill="none"
175 - stroke="url(#yellowSnakeBody)"
176 - stroke-width="12"
177 - stroke-linecap="round"
178 - />
179 - </g>
180 -
181 - <!-- ===== MAIN SNAKES - Full detail ===== -->
182 -
183 - <!-- Main Blue Snake - curving from left, head pointing upper-right -->
184 - <g>
185 - <path
186 - d="M 45,95
187 - C 52,82 72,88 82,75
188 - C 92,62 85,52 95,45
189 - C 105,38 118,48 125,58"
190 - fill="none"
191 - stroke="url(#blueSnakeBody)"
192 - stroke-width="15"
193 - stroke-linecap="round"
194 - filter="url(#snakeGlow)"
195 - />
196 -
197 - <!-- Blue Snake Head - detailed anatomical design -->
198 - <g transform="translate(122, 52) rotate(25)">
199 - <!-- Head base shape (slightly triangular) -->
200 - <path d="M -2,0 Q 2,-7 10,-6 Q 18,-4 18,0 Q 18,4 10,6 Q 2,7 -2,0 Z" fill="url(#pythonBlue)"/>
201 - <path d="M -1,0 Q 2,-6 10,-5 Q 16,-3 16,0 Q 16,3 10,5 Q 2,6 -1,0 Z" fill="url(#blueSnakeBody)"/>
202 - <!-- Brow ridge -->
203 - <path d="M 4,-4 Q 8,-5.5 12,-4" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="0.8"/>
204 - <!-- Scale pattern on crown -->
205 - <ellipse cx="10" cy="-3" rx="3" ry="1.5" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.4"/>
206 - <ellipse cx="6" cy="-4" rx="2" ry="1" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.3"/>
207 - <!-- Eye socket -->
208 - <ellipse cx="5" cy="-1.5" rx="2.8" ry="2.5" fill="#0a0e15"/>
209 - <!-- Eye -->
210 - <ellipse cx="5" cy="-1.5" rx="2.2" ry="2" fill="#1a1a1a"/>
211 - <!-- Slit pupil -->
212 - <ellipse cx="5" cy="-1.5" rx="0.6" ry="1.8" fill="#ffd43b" filter="url(#eyeGlow)"/>
213 - <!-- Eye highlights -->
214 - <circle cx="4.2" cy="-2.2" r="0.4" fill="#ffffff" opacity="0.9"/>
215 - <circle cx="5.5" cy="-0.8" r="0.2" fill="#ffffff" opacity="0.5"/>
216 - <!-- Heat pit -->
217 - <ellipse cx="0" cy="-1" rx="0.6" ry="0.4" fill="#1a2540"/>
218 - <!-- Nostrils -->
219 - <circle cx="-1" cy="-0.8" r="0.35" fill="#0a0f1a"/>
220 - <circle cx="-1" cy="0.8" r="0.35" fill="#0a0f1a"/>
221 - <!-- Jaw line -->
222 - <path d="M 2,3 Q 8,4 14,2" fill="none" stroke="rgba(0,0,0,0.2)" stroke-width="0.5"/>
223 - <!-- Forked tongue -->
224 - <path d="M -3,0 L -7,0 L -9,-1.5 M -7,0 L -9,1.5" stroke="#dd3344" stroke-width="0.5" stroke-linecap="round" fill="none"/>
225 - </g>
226 - </g>
227 -
228 - <!-- Main Yellow Snake - curving from bottom-right, head pointing left -->
229 - <g>
230 - <path
231 - d="M 158,120
232 - C 145,112 135,125 115,118
233 - C 95,111 100,95 85,88
234 - C 70,81 55,90 48,78"
235 - fill="none"
236 - stroke="url(#yellowSnakeBody)"
237 - stroke-width="15"
238 - stroke-linecap="round"
239 - filter="url(#snakeGlow)"
240 - />
241 -
242 - <!-- Yellow Snake Head - detailed anatomical design -->
243 - <g transform="translate(52, 80) rotate(-135)">
244 - <!-- Head base shape (slightly triangular) -->
245 - <path d="M -2,0 Q 2,-7 10,-6 Q 18,-4 18,0 Q 18,4 10,6 Q 2,7 -2,0 Z" fill="url(#pythonYellow)"/>
246 - <path d="M -1,0 Q 2,-6 10,-5 Q 16,-3 16,0 Q 16,3 10,5 Q 2,6 -1,0 Z" fill="url(#yellowSnakeBody)"/>
247 - <!-- Brow ridge -->
248 - <path d="M 4,-4 Q 8,-5.5 12,-4" fill="none" stroke="rgba(0,0,0,0.1)" stroke-width="0.8"/>
249 - <!-- Scale pattern on crown -->
250 - <ellipse cx="10" cy="-3" rx="3" ry="1.5" fill="none" stroke="rgba(0,0,0,0.08)" stroke-width="0.4"/>
251 - <ellipse cx="6" cy="-4" rx="2" ry="1" fill="none" stroke="rgba(0,0,0,0.08)" stroke-width="0.3"/>
252 - <!-- Eye socket -->
253 - <ellipse cx="5" cy="-1.5" rx="2.8" ry="2.5" fill="#0a0e15"/>
254 - <!-- Eye -->
255 - <ellipse cx="5" cy="-1.5" rx="2.2" ry="2" fill="#1a1a1a"/>
256 - <!-- Slit pupil -->
257 - <ellipse cx="5" cy="-1.5" rx="0.6" ry="1.8" fill="#306998" filter="url(#eyeGlow)"/>
258 - <!-- Eye highlights -->
259 - <circle cx="4.2" cy="-2.2" r="0.4" fill="#ffffff" opacity="0.9"/>
260 - <circle cx="5.5" cy="-0.8" r="0.2" fill="#ffffff" opacity="0.5"/>
261 - <!-- Heat pit -->
262 - <ellipse cx="0" cy="-1" rx="0.6" ry="0.4" fill="#8b7500"/>
263 - <!-- Nostrils -->
264 - <circle cx="-1" cy="-0.8" r="0.35" fill="#8b6914"/>
265 - <circle cx="-1" cy="0.8" r="0.35" fill="#8b6914"/>
266 - <!-- Jaw line -->
267 - <path d="M 2,3 Q 8,4 14,2" fill="none" stroke="rgba(0,0,0,0.15)" stroke-width="0.5"/>
268 - <!-- Forked tongue -->
269 - <path d="M -3,0 L -7,0 L -9,-1.5 M -7,0 L -9,1.5" stroke="#dd3344" stroke-width="0.5" stroke-linecap="round" fill="none"/>
270 - </g>
271 - </g>
272 -
273 - <!-- Third Snake (Blue) - slithering across middle -->
274 - <g>
275 - <path
276 - d="M 35,108
277 - C 50,100 65,112 85,102
278 - C 105,92 115,105 130,95
279 - C 145,85 155,95 162,88"
280 - fill="none"
281 - stroke="url(#blueSnakeBody)"
282 - stroke-width="12"
283 - stroke-linecap="round"
284 - filter="url(#snakeGlow)"
285 - />
286 -
287 - <!-- Third snake head - detailed smaller version -->
288 - <g transform="translate(159, 86) rotate(20)">
289 - <!-- Head base -->
290 - <path d="M -1,0 Q 1,-5 6,-4.5 Q 11,-3 11,0 Q 11,3 6,4.5 Q 1,5 -1,0 Z" fill="url(#pythonBlue)"/>
291 - <path d="M 0,0 Q 1.5,-4 6,-3.5 Q 10,-2.5 10,0 Q 10,2.5 6,3.5 Q 1.5,4 0,0 Z" fill="url(#blueSnakeBody)"/>
292 - <!-- Brow ridge -->
293 - <path d="M 2,-2.5 Q 5,-3.5 8,-2.5" fill="none" stroke="rgba(255,255,255,0.12)" stroke-width="0.5"/>
294 - <!-- Eye socket -->
295 - <ellipse cx="3" cy="-1" rx="1.8" ry="1.6" fill="#0a0e15"/>
296 - <!-- Eye -->
297 - <ellipse cx="3" cy="-1" rx="1.4" ry="1.3" fill="#1a1a1a"/>
298 - <!-- Slit pupil -->
299 - <ellipse cx="3" cy="-1" rx="0.4" ry="1.1" fill="#ffd43b" filter="url(#eyeGlow)"/>
300 - <!-- Highlight -->
301 - <circle cx="2.5" cy="-1.5" r="0.25" fill="#ffffff" opacity="0.85"/>
302 - <!-- Nostril -->
303 - <circle cx="-0.5" cy="0" r="0.25" fill="#1a2540"/>
304 - <!-- Forked tongue -->
305 - <path d="M -2,0 L -4.5,0 L -6,-1 M -4.5,0 L -6,1" stroke="#dd3344" stroke-width="0.4" stroke-linecap="round" fill="none"/>
306 - </g>
307 - </g>
308 -
309 - <!-- Fourth Snake (Yellow) - smaller, peeking from upper left -->
310 - <g opacity="0.85">
311 - <path
312 - d="M 38,72 C 48,65 62,72 72,62 C 82,52 75,42 85,38"
313 - fill="none"
314 - stroke="url(#yellowSnakeBody)"
315 - stroke-width="10"
316 - stroke-linecap="round"
317 - filter="url(#snakeGlow)"
318 - />
319 -
320 - <!-- Fourth snake head - detailed smaller version -->
321 - <g transform="translate(83, 36) rotate(30)">
322 - <!-- Head base -->
323 - <path d="M -1,0 Q 1,-4.5 5,-4 Q 9,-2.5 9,0 Q 9,2.5 5,4 Q 1,4.5 -1,0 Z" fill="url(#pythonYellow)"/>
324 - <path d="M 0,0 Q 1,-3.5 5,-3 Q 8,-2 8,0 Q 8,2 5,3 Q 1,3.5 0,0 Z" fill="url(#yellowSnakeBody)"/>
325 - <!-- Brow ridge -->
326 - <path d="M 1.5,-2 Q 4,-3 6.5,-2" fill="none" stroke="rgba(0,0,0,0.08)" stroke-width="0.4"/>
327 - <!-- Eye socket -->
328 - <ellipse cx="2.5" cy="-0.8" rx="1.5" ry="1.4" fill="#0a0e15"/>
329 - <!-- Eye -->
330 - <ellipse cx="2.5" cy="-0.8" rx="1.2" ry="1.1" fill="#1a1a1a"/>
331 - <!-- Slit pupil -->
332 - <ellipse cx="2.5" cy="-0.8" rx="0.35" ry="0.9" fill="#306998" filter="url(#eyeGlow)"/>
333 - <!-- Highlight -->
334 - <circle cx="2.1" cy="-1.2" r="0.2" fill="#ffffff" opacity="0.85"/>
335 - <!-- Nostril -->
336 - <circle cx="-0.3" cy="0" r="0.2" fill="#8b6914"/>
337 - <!-- Forked tongue -->
338 - <path d="M -1.5,0 L -3.5,0 L -4.8,-0.8 M -3.5,0 L -4.8,0.8" stroke="#dd3344" stroke-width="0.35" stroke-linecap="round" fill="none"/>
339 - </g>
340 - </g>
341 -
342 - <!-- Fifth Snake (Blue) - tiny one lurking bottom right -->
343 - <g opacity="0.7">
344 - <path
345 - d="M 152,130 C 145,136 135,130 128,138 C 122,146 130,152 125,158"
346 - fill="none"
347 - stroke="url(#blueSnakeBody)"
348 - stroke-width="8"
349 - stroke-linecap="round"
350 - filter="url(#snakeGlow)"
351 - />
352 - </g>
353 -
354 - <!-- Loose coil segment (Yellow) in bottom left -->
355 - <g opacity="0.55">
356 - <path
357 - d="M 62,148 C 72,142 80,150 88,144 C 96,138 92,132 98,128"
358 - fill="none"
359 - stroke="url(#yellowSnakeBody)"
360 - stroke-width="10"
361 - stroke-linecap="round"
362 - filter="url(#snakeGlow)"
363 - />
364 - </g>
365 -
366 - <!-- Background tail fragment -->
367 - <g opacity="0.45">
368 - <path
369 - d="M 115,155 C 108,148 100,155 90,150"
370 - fill="none"
371 - stroke="url(#blueSnakeBody)"
372 - stroke-width="7"
373 - stroke-linecap="round"
374 - />
375 - </g>
376 -
377 - </g>
378 -
379 - <!-- Subtle highlight on hexagon edge -->
380 - <polygon
381 - points="100,8 181.6,55 181.6,145 100,192 18.4,145 18.4,55"
382 - fill="none"
383 - stroke="url(#hexBorder)"
384 - stroke-width="1.5"
385 - opacity="0.4"
386 - />
387 - </svg>
\ No newline at end of file
  @@ -1,512 +0,0 @@
1 - # Snakepit Configuration Guide
2 -
3 - This guide covers all configuration options for Snakepit, from simple single-pool setups to advanced multi-pool deployments with different worker profiles.
4 -
5 - ---
6 -
7 - ## Table of Contents
8 -
9 - 1. [Configuration Formats](#configuration-formats)
10 - 2. [Global Options](#global-options)
11 - 3. [Pool Configuration](#pool-configuration)
12 - 4. [Heartbeat Configuration](#heartbeat-configuration)
13 - 5. [Logging Configuration](#logging-configuration)
14 - 6. [Python Runtime Configuration](#python-runtime-configuration)
15 - 7. [Optional Features](#optional-features)
16 - 8. [Complete Configuration Example](#complete-configuration-example)
17 -
18 - ---
19 -
20 - ## Configuration Formats
21 -
22 - Snakepit supports two configuration formats: legacy (single-pool) and multi-pool (v0.6+).
23 -
24 - ### Simple (Legacy) Configuration
25 -
26 - For backward compatibility with v0.5.x and single-pool deployments:
27 -
28 - ```elixir
29 - # config/config.exs
30 - config :snakepit,
31 - pooling_enabled: true,
32 - adapter_module: Snakepit.Adapters.GRPCPython,
33 - pool_size: 100,
34 - pool_config: %{
35 - startup_batch_size: 8,
36 - startup_batch_delay_ms: 750,
37 - max_workers: 1000
38 - }
39 - ```
40 -
41 - This format creates a single pool named `:default` with the specified settings.
42 -
43 - ### Multi-Pool Configuration (v0.6+)
44 -
45 - For advanced deployments with multiple pools, each with different profiles:
46 -
47 - ```elixir
48 - # config/config.exs
49 - config :snakepit,
50 - pools: [
51 - %{
52 - name: :default,
53 - worker_profile: :process,
54 - pool_size: 100,
55 - adapter_module: Snakepit.Adapters.GRPCPython
56 - },
57 - %{
58 - name: :ml_inference,
59 - worker_profile: :thread,
60 - pool_size: 4,
61 - threads_per_worker: 16,
62 - adapter_module: Snakepit.Adapters.GRPCPython,
63 - adapter_args: ["--adapter", "myapp.ml.InferenceAdapter"]
64 - }
65 - ]
66 - ```
67 -
68 - This creates two pools: `:default` for general tasks and `:ml_inference` for CPU-bound ML workloads.
69 -
70 - ---
71 -
72 - ## Global Options
73 -
74 - These options apply to all pools or the Snakepit application as a whole.
75 -
76 - | Option | Type | Default | Description |
77 - |--------|------|---------|-------------|
78 - | `pooling_enabled` | `boolean()` | `false` | Enable or disable worker pooling. Set to `true` for normal operation. |
79 - | `adapter_module` | `module()` | `nil` | Default adapter module for pools that do not specify one. |
80 - | `pool_size` | `pos_integer()` | `System.schedulers_online() * 2` | Default pool size. Typically 2x CPU cores. |
81 - | `capacity_strategy` | `:pool \| :profile \| :hybrid` | `:pool` | How worker capacity is managed across pools. |
82 - | `pool_startup_timeout` | `pos_integer()` | `10000` | Maximum time (ms) to wait for a worker to start. |
83 - | `pool_queue_timeout` | `pos_integer()` | `5000` | Maximum time (ms) a request waits in queue. |
84 - | `pool_max_queue_size` | `pos_integer()` | `1000` | Maximum queued requests before rejecting new ones. |
85 - | `grpc_port` | `pos_integer()` | `50051` | Port for the Elixir gRPC server (Python-to-Elixir calls). |
86 - | `grpc_host` | `String.t()` | `"localhost"` | Host for gRPC connections. |
87 - | `graceful_shutdown_timeout_ms` | `pos_integer()` | `6000` | Time (ms) to wait for Python to terminate gracefully before SIGKILL. |
88 -
89 - ### Capacity Strategies
90 -
91 - | Strategy | Description |
92 - |----------|-------------|
93 - | `:pool` | Each pool manages its own capacity independently. Default and simplest option. |
94 - | `:profile` | Workers of the same profile share capacity across pools. |
95 - | `:hybrid` | Combination of pool and profile strategies for complex deployments. |
96 -
97 - ---
98 -
99 - ## Pool Configuration
100 -
101 - Each pool can be configured independently with these options.
102 -
103 - ### Required Fields
104 -
105 - | Option | Type | Description |
106 - |--------|------|-------------|
107 - | `name` | `atom()` | Unique pool identifier. Use `:default` for the primary pool. |
108 -
109 - ### Profile Selection
110 -
111 - | Option | Type | Default | Description |
112 - |--------|------|---------|-------------|
113 - | `worker_profile` | `:process \| :thread` | `:process` | Worker execution model. See [Worker Profiles Guide](worker-profiles.md). |
114 -
115 - ### Common Pool Options
116 -
117 - | Option | Type | Default | Description |
118 - |--------|------|---------|-------------|
119 - | `pool_size` | `pos_integer()` | Global setting | Number of workers in this pool. |
120 - | `adapter_module` | `module()` | Global setting | Adapter module for this pool. |
121 - | `adapter_args` | `list(String.t())` | `[]` | CLI arguments passed to the Python server. |
122 - | `adapter_env` | `list({String.t(), String.t()})` | `[]` | Environment variables for Python processes. |
123 - | `adapter_spec` | `String.t()` | `nil` | Python adapter module path (e.g., `"myapp.adapters.MyAdapter"`). |
124 -
125 - ### Process Profile Options
126 -
127 - These options apply when `worker_profile: :process`:
128 -
129 - | Option | Type | Default | Description |
130 - |--------|------|---------|-------------|
131 - | `startup_batch_size` | `pos_integer()` | `8` | Workers started per batch during pool initialization. |
132 - | `startup_batch_delay_ms` | `non_neg_integer()` | `750` | Delay between startup batches (ms). Reduces system load during startup. |
133 -
134 - ### Thread Profile Options
135 -
136 - These options apply when `worker_profile: :thread`:
137 -
138 - | Option | Type | Default | Description |
139 - |--------|------|---------|-------------|
140 - | `threads_per_worker` | `pos_integer()` | `10` | Thread pool size per Python process. Total capacity = `pool_size * threads_per_worker`. |
141 - | `thread_safety_checks` | `boolean()` | `false` | Enable runtime thread safety validation. Useful for development. |
142 -
143 - ### Worker Lifecycle Options
144 -
145 - | Option | Type | Default | Description |
146 - |--------|------|---------|-------------|
147 - | `worker_ttl` | `:infinity \| {value, unit}` | `:infinity` | Maximum worker lifetime before recycling. |
148 - | `worker_max_requests` | `:infinity \| pos_integer()` | `:infinity` | Maximum requests before recycling a worker. |
149 -
150 - **TTL Units:**
151 -
152 - | Unit | Example |
153 - |------|---------|
154 - | `:seconds` | `{3600, :seconds}` - 1 hour |
155 - | `:minutes` | `{60, :minutes}` - 1 hour |
156 - | `:hours` | `{1, :hours}` - 1 hour |
157 -
158 - Worker recycling helps prevent memory leaks and ensures fresh worker state.
159 -
160 - ---
161 -
162 - ## Heartbeat Configuration
163 -
164 - Heartbeats detect unresponsive workers and trigger automatic restarts.
165 -
166 - ### Global Heartbeat Config
167 -
168 - ```elixir
169 - config :snakepit,
170 - heartbeat: %{
171 - enabled: true,
172 - ping_interval_ms: 2000,
173 - timeout_ms: 10000,
174 - max_missed_heartbeats: 3,
175 - initial_delay_ms: 0,
176 - dependent: true
177 - }
178 - ```
179 -
180 - ### Per-Pool Heartbeat Config
181 -
182 - ```elixir
183 - %{
184 - name: :ml_pool,
185 - heartbeat: %{
186 - enabled: true,
187 - ping_interval_ms: 10000,
188 - timeout_ms: 30000,
189 - max_missed_heartbeats: 2
190 - }
191 - }
192 - ```
193 -
194 - ### Heartbeat Options
195 -
196 - | Option | Type | Default | Description |
197 - |--------|------|---------|-------------|
198 - | `enabled` | `boolean()` | `true` | Enable heartbeat monitoring. |
199 - | `ping_interval_ms` | `pos_integer()` | `2000` | Interval between heartbeat pings. |
200 - | `timeout_ms` | `pos_integer()` | `10000` | Maximum time to wait for heartbeat response. |
201 - | `max_missed_heartbeats` | `pos_integer()` | `3` | Missed heartbeats before declaring worker dead. |
202 - | `initial_delay_ms` | `non_neg_integer()` | `0` | Delay before first heartbeat ping. |
203 - | `dependent` | `boolean()` | `true` | Whether worker terminates if heartbeat monitor dies. |
204 -
205 - ### Tuning Guidelines
206 -
207 - - **Fast detection**: Lower `ping_interval_ms` and `max_missed_heartbeats`
208 - - **Reduce overhead**: Higher `ping_interval_ms` for stable workloads
209 - - **Long operations**: Increase `timeout_ms` if workers run long computations
210 - - **ML workloads**: Use `ping_interval_ms: 10000` or higher since inference can block
211 -
212 - ---
213 -
214 - ## Logging Configuration
215 -
216 - Snakepit uses its own logger for internal operations.
217 -
218 - ### Log Level
219 -
220 - ```elixir
221 - config :snakepit,
222 - log_level: :info # :debug | :info | :warning | :error | :none
223 - ```
224 -
225 - | Level | Description |
226 - |-------|-------------|
227 - | `:debug` | Verbose output including worker lifecycle, gRPC calls, heartbeats |
228 - | `:info` | Normal operation messages |
229 - | `:warning` | Potential issues that do not stop operation |
230 - | `:error` | Errors that affect functionality |
231 - | `:none` | Disable all Snakepit logging |
232 -
233 - ### Log Categories
234 -
235 - Fine-grained control over logging categories:
236 -
237 - ```elixir
238 - config :snakepit,
239 - log_level: :info,
240 - log_categories: %{
241 - pool: :debug, # Pool operations
242 - worker: :debug, # Worker lifecycle
243 - heartbeat: :info, # Heartbeat monitoring
244 - grpc: :warning # gRPC communication
245 - }
246 - ```
247 -
248 - ### Python-Side Logging
249 -
250 - The Python bridge respects the `SNAKEPIT_LOG_LEVEL` environment variable:
251 -
252 - ```elixir
253 - %{
254 - name: :default,
255 - adapter_env: [{"SNAKEPIT_LOG_LEVEL", "info"}]
256 - }
257 - ```
258 -
259 - ---
260 -
261 - ## Python Runtime Configuration
262 -
263 - Configure how Python interpreters are discovered and managed.
264 -
265 - ### Interpreter Selection
266 -
267 - ```elixir
268 - config :snakepit,
269 - python_executable: "/path/to/python3"
270 - ```
271 -
272 - Or use environment variable (takes precedence):
273 -
274 - ```bash
275 - export SNAKEPIT_PYTHON="/path/to/python3"
276 - ```
277 -
278 - ### Runtime Strategy
279 -
280 - ```elixir
281 - config :snakepit,
282 - python_runtime: %{
283 - strategy: :venv, # :system | :venv | :managed
284 - managed: false,
285 - version: "3.12"
286 - }
287 - ```
288 -
289 - | Strategy | Description |
290 - |----------|-------------|
291 - | `:system` | Use system Python interpreter |
292 - | `:venv` | Use project virtual environment (`.venv/bin/python3`) |
293 - | `:managed` | Let Snakepit manage Python version (experimental) |
294 -
295 - ### Environment Variables per Pool
296 -
297 - ```elixir
298 - %{
299 - name: :ml_pool,
300 - adapter_env: [
301 - # Control threading in numerical libraries
302 - {"OPENBLAS_NUM_THREADS", "1"},
303 - {"MKL_NUM_THREADS", "1"},
304 - {"OMP_NUM_THREADS", "1"},
305 - {"NUMEXPR_NUM_THREADS", "1"},
306 -
307 - # GPU configuration
308 - {"CUDA_VISIBLE_DEVICES", "0"},
309 -
310 - # Python settings
311 - {"PYTHONUNBUFFERED", "1"},
312 - {"SNAKEPIT_LOG_LEVEL", "warning"}
313 - ]
314 - }
315 - ```
316 -
317 - ---
318 -
319 - ## Optional Features
320 -
321 - ### Zero-Copy Data Transfer
322 -
323 - Enable zero-copy for large binary data:
324 -
325 - ```elixir
326 - config :snakepit,
327 - zero_copy: %{
328 - enabled: true,
329 - threshold_bytes: 1_048_576 # 1 MB
330 - }
331 - ```
332 -
333 - Zero-copy is beneficial for ML workloads with large tensors.
334 -
335 - ### Crash Barrier
336 -
337 - Limit restart attempts for frequently crashing workers:
338 -
339 - ```elixir
340 - config :snakepit,
341 - crash_barrier: %{
342 - enabled: true,
343 - max_restarts: 5,
344 - window_seconds: 60
345 - }
346 - ```
347 -
348 - If a worker restarts more than `max_restarts` times within `window_seconds`, it is permanently removed from the pool.
349 -
350 - ### Circuit Breaker
351 -
352 - Prevent cascading failures:
353 -
354 - ```elixir
355 - config :snakepit,
356 - circuit_breaker: %{
357 - enabled: true,
358 - failure_threshold: 5,
359 - reset_timeout_ms: 30000
360 - }
361 - ```
362 -
363 - After `failure_threshold` consecutive failures, the circuit opens and requests fail fast for `reset_timeout_ms`.
364 -
365 - ---
366 -
367 - ## Complete Configuration Example
368 -
369 - Here is a production-ready configuration demonstrating all major options:
370 -
371 - ```elixir
372 - # config/config.exs
373 - config :snakepit,
374 - # Global settings
375 - pooling_enabled: true,
376 - pool_startup_timeout: 30_000,
377 - pool_queue_timeout: 10_000,
378 - pool_max_queue_size: 5000,
379 - grpc_port: 50051,
380 -
381 - # Logging
382 - log_level: :info,
383 - log_categories: %{
384 - pool: :info,
385 - worker: :warning,
386 - heartbeat: :warning,
387 - grpc: :warning
388 - },
389 -
390 - # Global heartbeat defaults
391 - heartbeat: %{
392 - enabled: true,
393 - ping_interval_ms: 5000,
394 - timeout_ms: 15000,
395 - max_missed_heartbeats: 3
396 - },
397 -
398 - # Multiple pools
399 - pools: [
400 - # Default pool for I/O-bound tasks
401 - %{
402 - name: :default,
403 - worker_profile: :process,
404 - pool_size: 50,
405 - adapter_module: Snakepit.Adapters.GRPCPython,
406 - adapter_args: ["--adapter", "myapp.adapters.GeneralAdapter"],
407 - adapter_env: [
408 - {"OPENBLAS_NUM_THREADS", "1"},
409 - {"OMP_NUM_THREADS", "1"}
410 - ],
411 - startup_batch_size: 10,
412 - startup_batch_delay_ms: 500
413 - },
414 -
415 - # ML inference pool (CPU-bound, thread profile)
416 - %{
417 - name: :ml_inference,
418 - worker_profile: :thread,
419 - pool_size: 4,
420 - threads_per_worker: 8, # 32 total capacity
421 - adapter_module: Snakepit.Adapters.GRPCPython,
422 - adapter_args: ["--adapter", "myapp.ml.InferenceAdapter"],
423 - adapter_env: [
424 - {"OPENBLAS_NUM_THREADS", "8"},
425 - {"OMP_NUM_THREADS", "8"},
426 - {"CUDA_VISIBLE_DEVICES", "0"},
427 - {"PYTORCH_CUDA_ALLOC_CONF", "max_split_size_mb:512"}
428 - ],
429 - thread_safety_checks: false,
430 - worker_ttl: {1800, :seconds},
431 - worker_max_requests: 10000,
432 - heartbeat: %{
433 - enabled: true,
434 - ping_interval_ms: 10000,
435 - timeout_ms: 60000,
436 - max_missed_heartbeats: 2
437 - }
438 - },
439 -
440 - # Background processing pool
441 - %{
442 - name: :background,
443 - worker_profile: :process,
444 - pool_size: 10,
445 - adapter_module: Snakepit.Adapters.GRPCPython,
446 - adapter_args: ["--adapter", "myapp.adapters.BackgroundAdapter"],
447 - adapter_env: [
448 - {"SNAKEPIT_LOG_LEVEL", "warning"}
449 - ],
450 - worker_ttl: {3600, :seconds}
451 - }
452 - ],
453 -
454 - # Optional features
455 - crash_barrier: %{
456 - enabled: true,
457 - max_restarts: 10,
458 - window_seconds: 300
459 - }
460 - ```
461 -
462 - ### Environment-Specific Overrides
463 -
464 - ```elixir
465 - # config/prod.exs
466 - config :snakepit,
467 - log_level: :warning,
468 - pool_max_queue_size: 10000
469 -
470 - # config/dev.exs
471 - config :snakepit,
472 - log_level: :debug,
473 - pool_size: 4
474 -
475 - # config/test.exs
476 - config :snakepit,
477 - pooling_enabled: false
478 - ```
479 -
480 - ---
481 -
482 - ## Validation
483 -
484 - Verify your configuration with the doctor task:
485 -
486 - ```bash
487 - mix snakepit.doctor
488 - ```
489 -
490 - At runtime, check pool status:
491 -
492 - ```elixir
493 - iex> Snakepit.get_stats()
494 - %{
495 - requests: 15432,
496 - queued: 5,
497 - errors: 12,
498 - queue_timeouts: 3,
499 - pool_saturated: 0,
500 - workers: 54,
501 - available: 49,
502 - busy: 5
503 - }
504 - ```
505 -
506 - ---
507 -
508 - ## Related Guides
509 -
510 - - [Getting Started](getting-started.md) - Installation and first steps
511 - - [Worker Profiles](worker-profiles.md) - Process vs Thread profiles
512 - - [Production](production.md) - Performance tuning and deployment checklist
Loading more files…