Skip to content

fix(TipCache): Committed transactions becoming invisible after TransactionStatusBlock is re-mapped - #9233

Open
TreeHunter9 wants to merge 6 commits into
FirebirdSQL:masterfrom
TreeHunter9:master-tip-cache-tsb-zeroing
Open

TreeHunter9 wants to merge 6 commits into
FirebirdSQL:masterfrom
TreeHunter9:master-tip-cache-tsb-zeroing

Conversation

@TreeHunter9

Copy link
Copy Markdown
Contributor

Description of the problem

The TIP cache consists of fixed-size blocks called TransactionStatusBlock (TSB) that contain a CN (Commit/Snapshot Number), where the transaction number serves as the index within that TSB. When a TSB fills up, the next TSB is created; that is, there can be several of them, and they are allocated as needed. A TSB is initialized upon the first connection to the database; it is then filled at runtime as transactions are executed. TSBs are also unloaded if no one is using a given block anymore. This is where the key problem lies: after a block is unloaded, it disappears, and if it is needed again, a new TSB will be allocated and initialized to zero, while the TIP will not be read from disk to load the current transaction statuses. In other words, when reading this TSB, we’ll get a CN equal to 0, where 0 represents the transaction status tra_active. This confusion leads to the following: a check begins to determine whether the transaction is actually active; the answer is negative; next, we read the TIP to obtain the current state, and it returns tra_committed. The two states received tra_active + tra_committed are interpreted as the transaction was committed after our transaction began, which means it should not be visible in our snapshot. As a result, these records become invisible to us, and we skip them, even though we should actually see them.

Reproducable example

  1. In the firebird.conf, set ServerMode = Classic and TipCacheBlockSize = 65536 to fill the TSB more quickly (a single TSB can hold ~8k transactions);
  2. Create a persistent connection to the database, this is needed to initialize the TIP cache and to ensure that GlobalTpcHeader (which stores latest_commit_number, used to generate the CN) remains active;
  3. Connect to the database (using new connection) and start incrementing the transaction number and CN to create a second TSB, for example, using:
set term ^;

execute block as
  declare i bigint = 0;
  declare d integer;
begin
  while (i < 11240) do
  begin
    in autonomous transaction do
      select 1 from rdb$database into :d;
    i = i + 1;
  end
end^

set term ;^

The number 11240 was chosen to ensure we go exactly beyond the first TSB. Commit and disconnect;

  1. Connect, create a table create table tab1(a int);, commit, insert a record insert into tab1 values (1);, commit, and disconnect;
  2. Connect, execute select * from tab1, and see that nothing is returned;

The main idea behind the fix

Basically, do not purge or memset to zero TSB when there is at least one connection to the database. Do it only when:

  • OIT is moved beyond TSB range (this logic was before me, doesn't change it);
  • Last connection is disconnected;
  • First connection is connected to database. This is done to remove any TSBs that may remain after a crash or an unfortunate shutdown;

Description of changes that is not straight forward related to TipCache

  • I decided to improve the performance of directory iteration because it now can affect performance of first connection and last disconnection. On Windows implementation is basically the same, only small improvements. On Linux implementation has been reworked, it now uses a much faster method for iterating through files that does not require calling stat for each file.
  • I aligned the SharedMemory initialization logic on Windows with Linux implementation. I discovered that on Windows there is initialization of zeros to newly mapped region, whereas on Linux the newly mapped region is left untouched, so this is creates some differences between platforms.
    I looked at Vlad's patch, which adds this initialization of zeros on Windows (by doing truncation of the file to 0 and then extend it back), and as I understand it, the main idea was not to perform memset, but to trigger an error to see whether any other instance of our server is holding the file (as I understand, because of IPC namespace/session differences, our Event will not be visible to other instance). I have reimplement this check by doing exclusive access to file, so if there is any other instance that holds the file, we will get an error about it. I hope I'm not missing something about it, so I think @hvlad should look at this.
    And I think, the SharedMemory should not memset mapped region to zero anyway, his responsibility is to simply perform a mmap. The decision of whether to perform memset or not should be left to the caller, not to the mapping mechanism.

Affected versions

v6 and v5. I haven't tested this on other versions, but if the code is the same, then the issue applies to them as well.

@hvlad

hvlad commented Oct 21, 2026

Copy link
Copy Markdown
Member

Try this branch first: work/gh-9017-trace

@hvlad

hvlad commented Oct 21, 2026 •

Copy link
Copy Markdown
Member
  • I looked at Vlad's patch, which adds this initialization of zeros on Windows (by doing truncation of the file to 0 and then extend it back)

No, file was truncated before that patch. Lines 2227-2237 in file before patch

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.

2 participants