Skip to content

#551: Keep fact replacement atomic#554

Open
GHX5T-SOL wants to merge 1 commit into
zerocracy:masterfrom
GHX5T-SOL:551
Open

#551: Keep fact replacement atomic#554
GHX5T-SOL wants to merge 1 commit into
zerocracy:masterfrom
GHX5T-SOL:551

Conversation

@GHX5T-SOL

Copy link
Copy Markdown
Contributor

/claim #551

Closes #551.

Summary

  • Move the delete step for Fbe.overwrite, Fbe.delete, and Fbe.delete_one inside the same fb.txn block as the replacement insert.
  • Add regression tests proving the original fact remains when the replacement insert raises.

Validation

  • bundle check
  • bundle exec ruby -Itest test/fbe/test_overwrite.rb -- --no-cov
  • bundle exec ruby -Itest test/fbe/test_delete.rb -- --no-cov
  • bundle exec ruby -Itest test/fbe/test_delete_one.rb -- --no-cov
  • git diff --check
  • bundle exec rubocop lib/fbe/overwrite.rb lib/fbe/delete.rb lib/fbe/delete_one.rb test/fbe/test_overwrite.rb test/fbe/test_delete.rb test/fbe/test_delete_one.rb
  • bundle exec rake test
  • bundle exec rubocop
  • bundle exec rake

No secrets or live-user data used; validation was local and within the repo scope.

@bibonix bibonix left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The delete-then-insert sequence in Fbe.delete, Fbe.delete_one, and both branches of Fbe.overwrite now sits inside the fb.txn block, so a raised insert rolls the delete back instead of leaving the factbase with the old fact gone. The three new regression tests inject a raising Factbase::Pre to force the insert to fail and then assert that the original fact and every untouched property survive the call. CI is green on actionlint, copyrights, markdown-lint, pdd, reuse, typos, xcop, yamllint, and the rake jobs on ubuntu-24.04 and macos-15. No inline comments.

@yegor256

Copy link
Copy Markdown
Member

@GHX5T-SOL merge conflict here

@yegor256

Copy link
Copy Markdown
Member

Please fix the merge conflicts so this pull request can be merged.

@morphqdd morphqdd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed and verified locally. The fix is correct: moving the delete! inside fb.txn makes the delete part of the same transaction as the re-insert, and since Factbase#txn tapes deletions and only applies them to @maps on successful completion (with Factbase::Pre#txn delegating through), a failing re-insert discards the taped delete and the original fact survives.

I reproduced it both ways with a small script using Factbase::Pre that raises on insert:

  • master: Fbe.delete / Fbe.overwrite -> the fact is gone (facts=0) after the insert raises.
  • this branch: the fact is kept intact (foo=[42], bar=['keep']).

Both overwrite branches, delete, and delete_one are covered, and the new tests genuinely fail on master. LGTM — two minor, non-blocking notes inline.

Comment thread lib/fbe/overwrite.rb
raise(Fbe::Error, "There is no #{fid} in the fact, cannot use Fbe.overwrite") if id.nil?
raise(Fbe::Error, "No facts by #{fid} = #{id}") if fb.query("(eq #{fid} #{id})").delete!.zero?
fb.txn do |fbt|
raise(Fbe::Error, "No facts by #{fid} = #{id}") if fbt.query("(eq #{fid} #{id})").delete!.zero?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good that both code paths (the hash branch here and the scalar branch at line 98) got the delete moved inside fb.txn — they're easy to fix only one of. Verified the mechanism: Factbase#txn records deletions on a LazyTaped tape and only applies them to @maps after the block returns, so a raising re-insert never applies the delete (Factbase::Pre#txn delegates, so it holds for wrapped factbases too). One tiny note: the "No facts by ..." guard now runs its delete! inside the txn even on the raise path — harmless (0 rows, rolled back), just slightly more work than the old pre-check.

Comment thread test/fbe/test_delete.rb
def test_keeps_original_fact_when_reinsert_fails
reject = false
fb =
Factbase::Pre.new(Factbase.new) do |_f, _fbt|

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice regression test — I ran it against master and confirmed it fails there (fact count drops to 0), and passes with this change, so it genuinely guards the atomicity. The Factbase::Pre.new(Factbase.new) { |_f, _fbt| raise if reject } + insert boilerplate is repeated verbatim across all three test files; optional, but a small shared helper would keep them from drifting.

@0crat

0crat commented Jul 7, 2026

Copy link
Copy Markdown

@morphqdd Thanks for the review! You've earned +13 points for this: +18 as a basis; -5 for very few (2) comments. Your running score is +610; don't forget to check your Zerocracy account too).

@yegor256

yegor256 commented Jul 7, 2026

Copy link
Copy Markdown
Member

@GHX5T-SOL merge conflict here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fbe.overwrite, Fbe.delete, Fbe.delete_one delete the old fact outside the txn, losing it if the insert raises

5 participants