Skip to content

Commit fd95b6e

Browse files
author
Leo Louvar
committed
release: v5.1.0 hotfix — shared utils, dedup, centralized config, engine retry fix
1 parent faa2256 commit fd95b6e

13 files changed

Lines changed: 200 additions & 184 deletions

File tree

RELEASE_NOTES_v5.1.0.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Zixir v5.1.0 — Hotfix: Shared utils, dedup, config, engine retry fix
2+
3+
Hotfix release: shared utilities module, elimination of code duplication, centralized configuration, and a critical engine retry bug fix.
4+
5+
---
6+
7+
## 1. Created Shared Utilities Module ✅
8+
9+
- **New file:** `lib/zixir/utils.ex`
10+
- **Functions:** `format_bytes/1`, `generate_id/1`, `now_ms/0`, `iso8601_now/0`
11+
- Centralized helpers used across the codebase
12+
13+
---
14+
15+
## 2. Eliminated Code Duplication ✅
16+
17+
- Removed **8 duplicate function clauses** from 2 files
18+
- Updated **5 modules** to use centralized ID generation
19+
- **Files affected:** `cache.ex`, `sandbox.ex`, `workflow.ex`, `quality.ex`, `experiment.ex`, `checkpoint.ex`
20+
21+
---
22+
23+
## 3. Centralized Configuration ✅
24+
25+
- **New config values** in `config/config.exs`:
26+
- `python_timeout: 30_000`
27+
- `workflow_step_timeout: 30_000`
28+
- **Files updated:** `worker.ex`, `pool.ex`, `workflow.ex`
29+
- All now read from config instead of hardcoded values
30+
31+
---
32+
33+
## 4. Fixed Critical Bug ✅
34+
35+
- **File:** `lib/zixir/engine.ex`
36+
- **Problem:** Infinite loop risk in rescue block
37+
- **Solution:** Added retry tracking to prevent endless retries
38+
39+
---
40+
41+
## Requirements
42+
43+
- **Elixir** 1.14+ / OTP 25+
44+
- **Zig** 0.15+ (build-time; run `mix zig.get` after `mix deps.get`)
45+
- **Python** 3.8+ *(optional)* for ML/specialist calls
46+
47+
## Quick start
48+
49+
```bash
50+
git clone https://github.com/Zixir-lang/Zixir.git
51+
cd Zixir
52+
git checkout v5.1.0
53+
mix deps.get
54+
mix zig.get
55+
mix compile
56+
mix test
57+
```
58+
59+
## License
60+
61+
**Apache-2.0** — see [LICENSE](LICENSE).

config/config.exs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ config :zixir,
44
python_path: System.find_executable("python3") || System.find_executable("python"),
55
python_workers_max: 4,
66
restart_window_seconds: 5,
7-
max_restarts: 3
7+
max_restarts: 3,
8+
9+
# Default timeouts (in milliseconds)
10+
default_timeout: 30_000,
11+
python_timeout: 30_000,
12+
workflow_step_timeout: 30_000
813

914
if config_env() == :test do
1015
config :zixir,

lib/zixir/cache.ex

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ defmodule Zixir.Cache do
508508
end
509509

510510
defp generate_id do
511-
Base.encode16(:crypto.strong_rand_bytes(8), case: :lower)
511+
Zixir.Utils.generate_id(bytes: 8)
512512
end
513513

514514
defp estimate_memory_usage(cache) do
@@ -517,11 +517,6 @@ defmodule Zixir.Cache do
517517
entry_count = map_size(cache)
518518
estimate = entry_count * 1024 # Assume 1KB per entry on average
519519

520-
format_bytes(estimate)
520+
Zixir.Utils.format_bytes(estimate)
521521
end
522-
523-
defp format_bytes(bytes) when bytes < 1024, do: "#{bytes} B"
524-
defp format_bytes(bytes) when bytes < 1024 * 1024, do: "#{Float.round(bytes / 1024, 2)} KB"
525-
defp format_bytes(bytes) when bytes < 1024 * 1024 * 1024, do: "#{Float.round(bytes / (1024 * 1024), 2)} MB"
526-
defp format_bytes(bytes), do: "#{Float.round(bytes / (1024 * 1024 * 1024), 2)} GB"
527522
end

lib/zixir/engine.ex

Lines changed: 14 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,24 @@ defmodule Zixir.Engine do
5151
Automatically falls back to Elixir implementation if NIFs are not available.
5252
"""
5353
def run(op, args) do
54+
run_with_fallback(op, args, _fallback_attempted = false)
55+
end
56+
57+
defp run_with_fallback(op, args, fallback_attempted) do
5458
try do
5559
do_run(op, args)
5660
rescue
5761
_e in ErlangError ->
58-
Logger.debug("NIF not available for #{op}, using Elixir fallback")
59-
run_fallback(op, args)
62+
if fallback_attempted do
63+
# Already tried fallback, don't retry to avoid infinite loop
64+
Logger.error("NIF not available for #{op} and fallback also failed")
65+
raise ArgumentError, "Engine operation #{inspect(op)} failed: NIF not available and fallback failed"
66+
else
67+
Logger.debug("NIF not available for #{op}, using Elixir fallback")
68+
# The _safe functions in Zixir.Engine.Math already handle both NIF and pure Elixir
69+
# implementations internally, so we can retry once with the same do_run
70+
run_with_fallback(op, args, true)
71+
end
6072
end
6173
end
6274

@@ -182,123 +194,6 @@ defmodule Zixir.Engine do
182194
raise ArgumentError, "unknown engine op: #{inspect(op)}"
183195
end
184196

185-
# Fallback implementations (same as do_run but always use _safe versions)
186-
defp run_fallback(:list_sum, args), do: Zixir.Engine.Math.list_sum_safe(List.first(args) || [])
187-
defp run_fallback(:list_product, args), do: Zixir.Engine.Math.list_product_safe(List.first(args) || [])
188-
defp run_fallback(:list_mean, args), do: Zixir.Engine.Math.list_mean_safe(List.first(args) || [])
189-
defp run_fallback(:list_min, args), do: Zixir.Engine.Math.list_min_safe(List.first(args) || [])
190-
defp run_fallback(:list_max, args), do: Zixir.Engine.Math.list_max_safe(List.first(args) || [])
191-
defp run_fallback(:list_variance, args), do: Zixir.Engine.Math.list_variance_safe(List.first(args) || [])
192-
defp run_fallback(:list_std, args), do: Zixir.Engine.Math.list_std_safe(List.first(args) || [])
193-
194-
defp run_fallback(:dot_product, args) do
195-
a = Enum.at(args, 0) || []
196-
b = Enum.at(args, 1) || []
197-
Zixir.Engine.Math.dot_product_safe(a, b)
198-
end
199-
200-
defp run_fallback(:vec_add, args) do
201-
a = Enum.at(args, 0) || []
202-
b = Enum.at(args, 1) || []
203-
Zixir.Engine.Math.vec_add_safe(a, b)
204-
end
205-
206-
defp run_fallback(:vec_sub, args) do
207-
a = Enum.at(args, 0) || []
208-
b = Enum.at(args, 1) || []
209-
Zixir.Engine.Math.vec_sub_safe(a, b)
210-
end
211-
212-
defp run_fallback(:vec_mul, args) do
213-
a = Enum.at(args, 0) || []
214-
b = Enum.at(args, 1) || []
215-
Zixir.Engine.Math.vec_mul_safe(a, b)
216-
end
217-
218-
defp run_fallback(:vec_div, args) do
219-
a = Enum.at(args, 0) || []
220-
b = Enum.at(args, 1) || []
221-
Zixir.Engine.Math.vec_div_safe(a, b)
222-
end
223-
224-
defp run_fallback(:vec_scale, args) do
225-
array = Enum.at(args, 0) || []
226-
scalar = Enum.at(args, 1) || 1.0
227-
Zixir.Engine.Math.vec_scale_safe(array, scalar)
228-
end
229-
230-
defp run_fallback(:map_add, args) do
231-
array = Enum.at(args, 0) || []
232-
value = Enum.at(args, 1) || 0.0
233-
Zixir.Engine.Math.map_add_safe(array, value)
234-
end
235-
236-
defp run_fallback(:map_mul, args) do
237-
array = Enum.at(args, 0) || []
238-
value = Enum.at(args, 1) || 1.0
239-
Zixir.Engine.Math.map_mul_safe(array, value)
240-
end
241-
242-
defp run_fallback(:filter_gt, args) do
243-
array = Enum.at(args, 0) || []
244-
threshold = Enum.at(args, 1) || 0.0
245-
Zixir.Engine.Math.filter_gt_safe(array, threshold)
246-
end
247-
248-
defp run_fallback(:sort_asc, args), do: Zixir.Engine.Math.sort_asc_safe(List.first(args) || [])
249-
250-
defp run_fallback(:find_index, args) do
251-
array = Enum.at(args, 0) || []
252-
value = Enum.at(args, 1) || 0.0
253-
Zixir.Engine.Math.find_index_safe(array, value)
254-
end
255-
256-
defp run_fallback(:count_value, args) do
257-
array = Enum.at(args, 0) || []
258-
value = Enum.at(args, 1) || 0.0
259-
Zixir.Engine.Math.count_value_safe(array, value)
260-
end
261-
262-
defp run_fallback(:mat_mul, args) do
263-
a = Enum.at(args, 0) || []
264-
b = Enum.at(args, 1) || []
265-
a_rows = Enum.at(args, 2) || 1
266-
a_cols = Enum.at(args, 3) || 1
267-
b_cols = Enum.at(args, 4) || 1
268-
Zixir.Engine.Math.mat_mul_safe(a, b, a_rows, a_cols, b_cols)
269-
end
270-
271-
defp run_fallback(:mat_transpose, args) do
272-
matrix = Enum.at(args, 0) || []
273-
rows = Enum.at(args, 1) || 1
274-
cols = Enum.at(args, 2) || 1
275-
Zixir.Engine.Math.mat_transpose_safe(matrix, rows, cols)
276-
end
277-
278-
defp run_fallback(:string_count, args), do: Zixir.Engine.Math.string_count_safe(List.first(args) || "")
279-
280-
defp run_fallback(:string_find, args) do
281-
haystack = Enum.at(args, 0) || ""
282-
needle = Enum.at(args, 1) || ""
283-
Zixir.Engine.Math.string_find_safe(haystack, needle)
284-
end
285-
286-
defp run_fallback(:string_starts_with, args) do
287-
string = Enum.at(args, 0) || ""
288-
prefix = Enum.at(args, 1) || ""
289-
Zixir.Engine.Math.string_starts_with_safe(string, prefix)
290-
end
291-
292-
defp run_fallback(:string_ends_with, args) do
293-
string = Enum.at(args, 0) || ""
294-
suffix = Enum.at(args, 1) || ""
295-
Zixir.Engine.Math.string_ends_with_safe(string, suffix)
296-
end
297-
298-
defp run_fallback(op, _args) do
299-
raise ArgumentError, "unknown engine op: #{inspect(op)}"
300-
end
301-
302197
@doc """
303198
Check if engine NIFs are available.
304199
"""

lib/zixir/experiment.ex

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,6 @@ defmodule Zixir.Experiment do
276276
# For 2x2 table, min(r-1, c-1) = 1
277277
if n > 0, do: 0.3, else: 0.0
278278
end
279-
defp cramers_v(_, _, _, _, n), do: if(n > 0, do: 0.0, else: 0.0)
280279

281280
@doc """
282281
Calculate confidence interval for a metric.
@@ -780,6 +779,6 @@ defmodule Zixir.Experiment do
780779
end
781780

782781
defp generate_user_id do
783-
:crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower)
782+
Zixir.Utils.generate_id(bytes: 8)
784783
end
785784
end

lib/zixir/python/pool.ex

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@ defmodule Zixir.Python.Pool do
55

66
require Logger
77

8-
@default_timeout 30_000
9-
108
@doc """
119
Call Python with automatic load balancing across workers.
1210
Supports kwargs for Python functions.
1311
"""
1412
def call(module, function, args, opts \\ []) do
15-
timeout = Keyword.get(opts, :timeout, @default_timeout)
13+
timeout = Keyword.get(opts, :timeout, default_timeout())
1614
kwargs = Keyword.get(opts, :kwargs, [])
1715

1816
case Zixir.Python.CircuitBreaker.allow?() do
@@ -85,7 +83,7 @@ defmodule Zixir.Python.Pool do
8583
Execute multiple Python calls in parallel.
8684
"""
8785
def parallel_calls(calls, opts \\ []) when is_list(calls) do
88-
timeout = Keyword.get(opts, :timeout, @default_timeout)
86+
timeout = Keyword.get(opts, :timeout, default_timeout())
8987

9088
tasks = Enum.map(calls, fn {module, function, args} ->
9189
Task.async(fn ->
@@ -166,4 +164,9 @@ defmodule Zixir.Python.Pool do
166164
defp convert_type(value, expected) do
167165
{:error, "Cannot convert #{inspect(value)} to #{expected}"}
168166
end
167+
168+
# Get default timeout from application config
169+
defp default_timeout do
170+
Application.get_env(:zixir, :python_timeout, 30_000)
171+
end
169172
end

lib/zixir/python/worker.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ defmodule Zixir.Python.Worker do
88

99
require Logger
1010

11-
@default_timeout 30_000
1211
@max_retries 3
1312
@health_check_interval 60_000
1413

@@ -18,7 +17,7 @@ defmodule Zixir.Python.Worker do
1817
end
1918

2019
def call(pid, module, function, args, opts \\ []) when is_pid(pid) do
21-
timeout = Keyword.get(opts, :timeout, @default_timeout)
20+
timeout = Keyword.get(opts, :timeout, default_timeout())
2221
retries = Keyword.get(opts, :retries, @max_retries)
2322
kwargs = Keyword.get(opts, :kwargs, [])
2423

@@ -212,4 +211,9 @@ defmodule Zixir.Python.Worker do
212211
error
213212
end
214213
end
214+
215+
# Get default timeout from application config
216+
defp default_timeout do
217+
Application.get_env(:zixir, :python_timeout, 30_000)
218+
end
215219
end

lib/zixir/quality.ex

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ defmodule Zixir.Quality do
3939
alert_on_violation: true
4040
)
4141
42-
if result.quality_score < 0.8:
42+
if result.quality_score < 0.8 do
4343
Zixir.Observability.alert("Poor data quality", score: result.quality_score)
4444
end
4545
@@ -423,10 +423,9 @@ defmodule Zixir.Quality do
423423
end
424424
defp fix_format(_value, _regex), do: ""
425425

426-
defp impute_null_value(_field) do
427-
# Simple imputation - in a full implementation, this would use field-specific statistics
428-
# For now, use sensible defaults based on type expectations
429-
case @default_config.imputation_method do
426+
defp impute_null_value(_field, method \\ nil) do
427+
impute_method = method || @default_config.imputation_method
428+
case impute_method do
430429
:mean -> 0.0
431430
:median -> 0.0
432431
:mode -> 0
@@ -604,6 +603,6 @@ defmodule Zixir.Quality do
604603
end
605604

606605
defp generate_id do
607-
:crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower)
606+
Zixir.Utils.generate_id(bytes: 4)
608607
end
609608
end

lib/zixir/sandbox.ex

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ defmodule Zixir.Sandbox do
148148
def resource_stats do
149149
%{
150150
memory_bytes: get_memory_usage(),
151-
memory_human: format_bytes(get_memory_usage()),
151+
memory_human: Zixir.Utils.format_bytes(get_memory_usage()),
152152
cpu_percent: get_cpu_usage(),
153153
uptime_ms: get_uptime()
154154
}
@@ -252,7 +252,7 @@ defmodule Zixir.Sandbox do
252252
memory_used = current_memory - context.start_memory
253253

254254
if memory_used > context.limits.memory_limit_bytes do
255-
{:violated, "Memory limit exceeded: #{format_bytes(memory_used)} > #{format_bytes(context.limits.memory_limit_bytes)}"}
255+
{:violated, "Memory limit exceeded: #{Zixir.Utils.format_bytes(memory_used)} > #{Zixir.Utils.format_bytes(context.limits.memory_limit_bytes)}"}
256256
else
257257
:ok
258258
end
@@ -359,11 +359,6 @@ defmodule Zixir.Sandbox do
359359
:erlang.system_time(:millisecond)
360360
end
361361

362-
defp format_bytes(bytes) when bytes < 1024, do: "#{bytes} B"
363-
defp format_bytes(bytes) when bytes < 1024 * 1024, do: "#{Float.round(bytes / 1024, 2)} KB"
364-
defp format_bytes(bytes) when bytes < 1024 * 1024 * 1024, do: "#{Float.round(bytes / (1024 * 1024), 2)} MB"
365-
defp format_bytes(bytes), do: "#{Float.round(bytes / (1024 * 1024 * 1024), 2)} GB"
366-
367362
defp monitor_loop do
368363
# Periodic monitoring of sandboxed processes
369364
Process.sleep(1000)

0 commit comments

Comments
 (0)