1+ <p align =" center " >
2+ <img src =" https://github.com/user-attachments/assets/fde2b47f-d853-4ee3-bdad-54a18b852730 " alt =" EctoLiteFS " style =" max-width : 100% ; height : auto ;" />
3+ </p >
4+
15# EctoLiteFS
26
37LiteFS-aware Ecto middleware for automatic write forwarding in distributed SQLite clusters.
@@ -6,44 +10,108 @@ EctoLiteFS detects which node is the current LiteFS primary and automatically fo
610write operations to it, allowing replicas to handle reads locally while writes are
711transparently routed to the primary.
812
13+ > ** Built on [ EctoMiddleware] ( https://hex.pm/packages/ecto_middleware ) :** EctoLiteFS includes
14+ > EctoMiddleware as a dependency, so installing ` ecto_litefs ` gives you everything you need!
15+
16+ ## Features
17+
18+ - ** Automatic write forwarding** - Writes on replicas are transparently forwarded to primary
19+ - ** Local reads** - Replicas handle reads locally for low latency
20+ - ** Multiple detection methods** - Filesystem polling + HTTP event stream + database tracking
21+ - ** Zero-config in dev/test** - Gracefully degrades when LiteFS is not present
22+ - ** Minimal setup** - Just add middleware and a supervisor to your app
23+ - ** Works with multiple repos** - Can track any number of Ecto repos in the same app
24+ - ** Telemetry integration** - Monitor write forwarding performance and failures
25+
926## Installation
1027
1128Add ` ecto_litefs ` to your list of dependencies in ` mix.exs ` :
1229
1330``` elixir
1431def deps do
1532 [
16- {:ecto_litefs , " ~> 0. 1.0" }
33+ {:ecto_litefs , " ~> 1.0" }
1734 ]
1835end
1936```
2037
21- ## Usage
38+ ## Quick Start
39+
40+ ### 1. Add to Supervision Tree
41+
42+ Add ` EctoLiteFS.Supervisor ` to your application, ** after** your Repo:
43+
44+ ``` elixir
45+ defmodule MyApp .Application do
46+ def start (_type , _args ) do
47+ children = [
48+ MyApp .Repo ,
49+ {EctoLiteFS .Supervisor ,
50+ repo: MyApp .Repo ,
51+ primary_file: " /litefs/.primary" ,
52+ poll_interval: 30_000 ,
53+ event_stream_url: " http://localhost:20202/events"
54+ }
55+ ]
56+
57+ Supervisor .start_link (children, strategy: :one_for_one )
58+ end
59+ end
60+ ```
61+
62+ ### 2. Add Middleware to Repo
2263
23- Add the supervisor to your application's supervision tree, after your Repo:
64+ ``` elixir
65+ defmodule MyApp .Repo do
66+ use Ecto .Repo , otp_app: :my_app
67+ use EctoMiddleware .Repo # Included with ecto_litefs!
68+
69+ @impl EctoMiddleware .Repo
70+ def middleware (_action , _resource ) do
71+ [EctoLiteFS .Middleware ] # Add anywhere in your middleware stack
72+ end
73+ end
74+ ```
75+
76+ ### 3. Use Your Repo Normally
2477
2578``` elixir
26- children = [
27- MyApp .Repo ,
28- {EctoLiteFS .Supervisor ,
29- repo: MyApp .Repo ,
30- primary_file: " /litefs/.primary" ,
31- poll_interval: 30_000 ,
32- event_stream_url: " http://localhost:20202/events"
33- }
34- ]
79+ # On primary node - executes locally
80+ MyApp .Repo .insert! (%User {name: " Alice" })
81+
82+ # On replica node - automatically forwarded to primary
83+ MyApp .Repo .insert! (%User {name: " Bob" })
84+
85+ # Reads always execute locally (low latency!)
86+ MyApp .Repo .all (User )
3587```
3688
89+ That's it! Writes are automatically forwarded when running on a replica.
90+
91+ > ** Future Plans:** Support for forwarding transactions and bulk operations is planned for future releases.
92+
3793## Configuration Options
3894
39- * ` :repo ` - Required. The Ecto Repo module to track.
95+ All options are configured when starting the supervisor:
96+
97+ * ` :repo ` - ** Required** . The Ecto Repo module to track.
4098* ` :primary_file ` - Path to LiteFS ` .primary ` file. Default: ` "/litefs/.primary" `
4199* ` :poll_interval ` - Filesystem poll interval in ms. Default: ` 30_000 `
42100* ` :event_stream_url ` - LiteFS HTTP events endpoint. Default: ` "http://localhost:20202/events" `
43101* ` :table_name ` - Database table for primary tracking. Default: ` "_ecto_litefs_primary" `
44102* ` :cache_ttl ` - Cache TTL in ms. Default: ` 5_000 `
45103* ` :erpc_timeout ` - Timeout for RPC calls to primary. Default: ` 15_000 `
46104
105+ ### Minimal Configuration
106+
107+ For most use cases, you only need to specify ` :repo ` :
108+
109+ ``` elixir
110+ {EctoLiteFS .Supervisor , repo: MyApp .Repo }
111+ ```
112+
113+ All other options use sensible defaults that work with standard LiteFS configurations.
114+
47115## How It Works
48116
49117EctoLiteFS uses multiple detection methods to determine primary status:
@@ -55,6 +123,72 @@ EctoLiteFS uses multiple detection methods to determine primary status:
55123When a write operation is detected on a replica node, it's automatically forwarded
56124to the primary node via ` :erpc.call/4 ` .
57125
126+ By default, both filesystem polling and event streaming are enabled for robust detection, but either
127+ of these can be disabled if desired.
128+
129+ ### Architecture
130+
131+ ```
132+ ┌─────────────────────┐ ┌─────────────────────┐
133+ │ Primary Node │ │ Replica Node │
134+ │ │ │ │
135+ │ ┌──────────────┐ │ │ ┌──────────────┐ │
136+ │ │ Repo.insert │ │ :erpc │ │ Repo.insert │───┼──┐
137+ │ └──────┬───────┘ │ ◄───────┼──│ (forwarded)│ │ │
138+ │ │ │ │ └──────────────┘ │ │
139+ │ ┌────▼────┐ │ │ │ │
140+ │ │ SQLite │◄─────┼─────────┼─► Reads happen │ │
141+ │ │ (write) │ │ replicate│ locally │ │
142+ │ └─────────┘ │ │ │ │
143+ └─────────────────────┘ └─────────────────────┘ │
144+ │
145+ Middleware detects write ────────────┘
146+ and forwards to primary
147+ ```
148+
149+ ## Development & Testing
150+
151+ EctoLiteFS gracefully handles environments where LiteFS is not present:
152+
153+ - ** Production (with LiteFS):** Forwards writes to primary, reads from replica
154+ - ** Development/Test (no LiteFS):** Executes all operations locally
155+
156+ This means you can add the middleware to your Repo without any conditional logic -
157+ it will "just work" in all environments!
158+
159+ ## Monitoring with Telemetry
160+
161+ EctoLiteFS emits telemetry events for observability:
162+
163+ ``` elixir
164+ # Monitor slow write forwards
165+ :telemetry .attach (
166+ " log-slow-forwards" ,
167+ [:ecto_litefs , :forward , :stop ],
168+ fn _event , %{duration: duration}, %{repo: repo, action: action}, _config ->
169+ if duration > 5_000_000 do # 5ms
170+ Logger .warning (" Slow forward: #{ action } took #{ duration } ns" )
171+ end
172+ end ,
173+ nil
174+ )
175+
176+ # Track forwarding failures
177+ :telemetry .attach (
178+ " track-forward-errors" ,
179+ [:ecto_litefs , :forward , :exception ],
180+ fn _event , _measurements , %{repo: repo, reason: reason}, _config ->
181+ Logger .error (" Forward failed: #{ inspect (reason)} " )
182+ end ,
183+ nil
184+ )
185+ ```
186+
187+ Available events:
188+ - ` [:ecto_litefs, :forward, :start] ` - Write forwarding initiated
189+ - ` [:ecto_litefs, :forward, :stop] ` - Forwarding completed successfully
190+ - ` [:ecto_litefs, :forward, :exception] ` - Forwarding failed
191+
58192## Testing
59193
60194### Unit Tests
@@ -84,6 +218,14 @@ Test scenarios:
842183 . Write forwarding from replica to primary
852194 . Primary failover - replica promoted, data preserved
86220
221+ ## Similar Projects
222+
223+ - ** [ litefs] ( https://hex.pm/packages/litefs ) ** - The original LiteFS library for Elixir by [ @sheertj ] ( https://git.sr.ht/~sheertj ) .
224+ Uses a repo wrapper approach where you rename your repo to ` MyApp.Repo.Local ` and create a new
225+ ` MyApp.Repo ` that proxies writes, and relies on filesystem polling only. EctoLiteFS differs by
226+ using middleware instead (so your existing repo stays unchanged), and adds HTTP event streaming
227+ for faster primary detection.
228+
87229## License
88230
89231MIT License - see [ LICENSE] ( LICENSE ) for details.
0 commit comments