First attempt at isolated transactions

I pushed some changes that implement a first attempt at dealing with isolated transactions and snapshots. There are quite likely still issues with it, although it isn’t likely to break much as long as you do not use them. Possibly some performance degradation, but on my tests they seem neglectable. As SWI-Prolog follows the Release Early, Release Often model, it is time to share :slight_smile:

To play, just pull the git and build. I’ve tested the builds on multi and single threaded versions on Linux, MacOS and Windows (32 and 64 bits). Tests are in src/Tests/transactions/.

My questions are

  • Do you see potential in this and if so, what is lacking to make it really work for you?
  • Can we find programs where it badly affects performance? There is a little more synchronization in multiple threads performing assert/retract and the code skipping invisible clauses (due to the logical update view or isolated in a transaction) is a bit more complicated. Other than that, all works exactly the same and possible performance changes are just variations.

Below are the docs in their current state, copy-pasted from the HTML. You can also read them locally after installing using

?- help(transactions).

Enjoy --- Jan

4.13.1.1 Transactions

Traditionally, Prolog database updates add or remove individual clauses. The Logical Update View ensures that a goal that is started on a dynamic predicate does not see modifications due to assert/1 or retract/1 during its life time. See section 4.13.5. In a multi-threaded context this assumption still holds for individual predicates: concurrent modifications to a dynamic predicate are invisible.

Transactions allow running a goal in isolation . Database changes from other threads are invisible while changes inside the transaction are invisible to other threads and become atomically visible to other threads if the transaction is committed . Transactions have several benefits.

  • If a database update requires multiple assert/1 and/or retract/1 operations, a transaction ensure either all are executed or the database remains unchanged. Notably unexpected exceptions or failures cannot leave the database in an inconsistent state.
  • Other threads do not see the intermediate inconsistent states when performing a database update as above. Notably, this avoids the need for locking for a database update that updates a clause by retracting it and asserting a new one. For example, when using the code below to update temperature/1 , consumers of this fact do not need synchronization because temperature/1 is always present and always has exactly one clause.
update_temperature(Temp) :-
    transaction(( retractall(temperature(_)),
                  asserta(temperature(Temp)))).
  • Transactions allow for “what if’’ reasoning over the dynamic database. This is particularly useful when combined with the deductive database facilities provided by tabling (see section 7]).

SWI-Prolog transactions only affect the dynamic database. Static predicates are globally visible and shared at all times. In particular, transactions do not affect loading source files and thus, source files loaded inside a transaction (e.g., due to autoloading ) are immediately globally visible. This may pose problems if loading source files provide clauses for dynamic predicates.

transaction (:Goal)

Run Goal as once/1 in a transaction. This implies that access to dynamic predicates sees the dynamic predicates at the moment the transaction is started, together with the modifications issued by Goal. Thus, Goal does not see changes to dynamic predicates from other threads and other threads do not see modifications by Goal ( isolation ). If Goal succeeds, all modifications become atomically visible to the other threads. If Goal fails or raises an exception all local modifications are discarded and transaction/1 fails or passes the exception.

Currently the number of database changes inside a transaction (or snapshot, see snapshot/1) is limited to 2 ** 32 -1. If this limit is exceeded a representation_error(transaction_generations) exception is raised.

Transactions may be nested. The above mentioned limitation for the number of database changes applies to the combined number in nested transactions. A discarded nested transaction or snapshot resets the database counter for the outer transaction.

snapshot (:Goal)

Similar to transaction/1, but always discards the local modifications. In other words, snapshot/1 allows a thread to examine a frozen state of the dynamic predicates and/or make isolated modifications without affecting other threads and without making permanent changes to the database.

Issues The current implementation does not guard against multiple transactions performing conflicting database updates. For example, running the above update_temperature/1 concurrently may result in a situation where we have multiple clauses for temperature/1 . If two transactions run this at the same time, both will remove the existing clause and both will add a new clause. Currently the only way to gurantee consistency is by wrapping transactions that may perform conflicting updates using with_mutex/2. Future versions may provide database constraints to prevent a transaction from creating an inconsistent state.

Status SWI-Prolog transaction management is highly experimental. Interaction with other parts of the system such as the library library(persistency) , incremental tabling (section 7.7]), etc. still have to be settled. Future versions may also support non-determinism through transactions and snapshots. This is merely a first step to explore isolated changes to the dynamic predicate database.

7 Likes