fosm-rails 0.2.1.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28f26f433fce210f3d83b05e743813e00af06799377202f38d0607fe8035d225
4
- data.tar.gz: a41302a39d9f2d563bd79d1979447ef5349c7e3a83b5b8a5883f30bcbb8b3026
3
+ metadata.gz: 4c993eea70daa26a211f4fa1bfda6750bbbf97389af2f621d673b12f58e41bac
4
+ data.tar.gz: 45b2cc6617e618c7500dccdf81ccf6bce49821307bad13e0f30374a95c9cbd04
5
5
  SHA512:
6
- metadata.gz: 9efdc72240a07c07533a251a7d853db7a548b2190978354011de4f43cf0355df53c05a26e8e0318c47389f04b7af00dbcd3ea9d21faed23b9e9dee1b455d6c48
7
- data.tar.gz: 39023f14ea79e8a5e09be3f00ac9aff8c1ad76ffaeccd128015baefae3517bf21193b5740faf80b5e8ae23e5560e9130acd5e24734d657d9367ff4d5aec8e610
6
+ metadata.gz: 4e1fa8d26f70a0ff09add103cf8fe0fbb95a63986a5dd4b1e1f052ff22b6cb28fb04e38d3b5e3e5a8be4a0a3d370bf80dc07447f0a1d6bb8292a9ce9d8de189a
7
+ data.tar.gz: 32a6f160c73a05def43c6a31bdd48975060557987f205f89edab4754b8a410ab505eb8917fa7338931695f261c47c4b52b01b15979de48314efe96cc9fd40ebc
data/AGENTS.md CHANGED
@@ -409,6 +409,67 @@ end
409
409
 
410
410
  ---
411
411
 
412
+ ## Database configuration and connection pools
413
+
414
+ ### Single-database apps (the common case)
415
+
416
+ `Fosm::ApplicationRecord` does **not** create a separate Active Record connection pool unless the host app explicitly opts in. This is the correct default for any app with a single database (SQLite, PostgreSQL, MySQL).
417
+
418
+ Do not add a `primary:` database role expecting FOSM to use it — the name `primary` is what Rails assigns to **every** database.yml entry by default, so that condition would fire for all apps and create a redundant pool on the same database.
419
+
420
+ ### Why a redundant pool deadlocks
421
+
422
+ When two separate pools both write to the same database inside nested transactions — which happens automatically when a `Fosm::*` model with `has_one_attached` / `has_many_attached` is saved, or when `ActiveRecord::Base.transaction` wraps a `Fosm::*` save — the result is a structural cross-pool deadlock:
423
+
424
+ ```
425
+ Fosm pool → BEGIN TRANSACTION # holds write lock
426
+ Fosm pool → INSERT INTO fosm_* # succeeds
427
+ Fosm pool → [after_save] ActiveStorage # needs App pool write lock
428
+ App pool → BEGIN TRANSACTION # blocked waiting for Fosm pool
429
+ Fosm pool → waiting for after_save… # blocked waiting for App pool
430
+ → DEADLOCK — neither pool can proceed
431
+ ```
432
+
433
+ This is deterministic, not a race condition. `busy_timeout` and WAL mode do not resolve it because neither mechanism can break a structural deadlock between two pools.
434
+
435
+ ### Opting in to a dedicated FOSM database
436
+
437
+ If your app requires FOSM models on a separate database (e.g., a read replica, a separate shard, or a compliance-isolated store), declare a `fosm` role in `database.yml`:
438
+
439
+ ```yaml
440
+ # config/database.yml
441
+ default: &default
442
+ adapter: sqlite3
443
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
444
+
445
+ development:
446
+ <<: *default
447
+ database: db/development.sqlite3
448
+
449
+ fosm:
450
+ <<: *default
451
+ database: db/fosm.sqlite3 # separate file / separate PostgreSQL URL
452
+ ```
453
+
454
+ `Fosm::ApplicationRecord` detects the `fosm` role at boot via `find_db_config("fosm")` and calls `connects_to database: { writing: :fosm }`. Without this entry, FOSM shares the host app's default pool.
455
+
456
+ ### Workaround for apps affected by the old behavior (pre-0.2.2)
457
+
458
+ If you are on a version earlier than 0.2.2 and cannot upgrade immediately, create this file in your host app to override the gem's class. Zeitwerk gives app files precedence over engine files:
459
+
460
+ ```ruby
461
+ # app/models/fosm/application_record.rb (in the HOST app, not the gem)
462
+ module Fosm
463
+ class ApplicationRecord < ::ApplicationRecord
464
+ self.abstract_class = true
465
+ end
466
+ end
467
+ ```
468
+
469
+ This collapses the Fosm pool into the app's default pool, eliminating the deadlock. Upgrade to 0.2.2 and remove this file when possible.
470
+
471
+ ---
472
+
412
473
  ## Contributing guidelines
413
474
 
414
475
  1. **Read the design principles above** before writing any code
@@ -1,6 +1,32 @@
1
1
  module Fosm
2
2
  class ApplicationRecord < ActiveRecord::Base
3
3
  self.abstract_class = true
4
- connects_to database: { writing: :primary } if ActiveRecord::Base.configurations.find_db_config("primary")
4
+
5
+ # Only create a dedicated connection pool when the host app has explicitly
6
+ # declared a "fosm" database role in database.yml.
7
+ #
8
+ # The previous guard — find_db_config("primary") — fired for every Rails app
9
+ # because "primary" is the default role name Rails assigns to every
10
+ # database.yml entry. This created a redundant second pool targeting the same
11
+ # database, which caused a structural cross-pool deadlock with ActiveStorage
12
+ # (and any ActiveRecord::Base.transaction block that touched Fosm::* models)
13
+ # on all single-database setups (SQLite, PostgreSQL, MySQL):
14
+ #
15
+ # Fosm pool → BEGIN TRANSACTION (holds write lock)
16
+ # App pool → BEGIN TRANSACTION (needs write lock — blocked)
17
+ # Fosm pool → after_save triggers ActiveStorage write via App pool
18
+ # → DEADLOCK: each pool waiting for the other to release the write lock
19
+ #
20
+ # The fix: only split into a separate pool when the host app opts in by
21
+ # declaring a "fosm" role in database.yml. Single-database apps share the
22
+ # default ActiveRecord::Base pool and are not affected.
23
+ #
24
+ # To use a dedicated FOSM database, add to database.yml:
25
+ #
26
+ # fosm:
27
+ # <<: *default
28
+ # database: db/fosm.sqlite3 # or a separate PostgreSQL/MySQL URL
29
+ #
30
+ connects_to database: { writing: :fosm } if ActiveRecord::Base.configurations.find_db_config("fosm")
5
31
  end
6
32
  end
data/lib/fosm/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Fosm
2
- VERSION = "0.2.1.1"
2
+ VERSION = "0.2.2"
3
3
  end
@@ -64,6 +64,29 @@ module Fosm
64
64
  or `app/agents/fosm/`, you **must** read and follow the instructions in:
65
65
 
66
66
  `#{agents_md_path}`
67
+
68
+ ### Database / connection pool (IMPORTANT — read before touching database.yml)
69
+
70
+ `Fosm::ApplicationRecord` only creates a separate Active Record connection pool
71
+ when the host app has a `fosm:` role in `database.yml`. Without it, all FOSM
72
+ models share the default app pool — this is correct for single-database setups.
73
+
74
+ **Do NOT** add a `primary:` database role for FOSM. `primary` is the default role
75
+ name Rails assigns to every `database.yml` entry, so that condition would create a
76
+ redundant pool on the same database and cause a deterministic cross-pool deadlock
77
+ whenever a `Fosm::*` model with `has_one_attached` / `has_many_attached` is saved,
78
+ or when `ActiveRecord::Base.transaction` wraps a `Fosm::*` save.
79
+
80
+ To opt in to a dedicated FOSM database, add this to `database.yml`:
81
+
82
+ ```yaml
83
+ fosm:
84
+ <<: *default
85
+ database: db/fosm.sqlite3 # or a separate PostgreSQL/MySQL URL
86
+ ```
87
+
88
+ See the "Database configuration and connection pools" section in AGENTS.md for
89
+ the full explanation and the pre-0.2.2 workaround.
67
90
  MARKDOWN
68
91
 
69
92
  if File.exist?(claude_md)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fosm-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abhishek Parolkar
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-24 00:00:00.000000000 Z
11
+ date: 2026-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails