Project

General

Profile

Bug #10937

fwd-h2 'killedMultiplexesCleanup' must not abend if the batch is not empty

Added by Constantin Asofiei 8 months ago. Updated 3 months ago.

Status:
Test
Priority:
Normal
Assignee:
Target version:
-
Start date:
Due date:
% Done:

100%

billable:
No
vendor_id:
GCD
case_num:
version_reported:
version_resolved:
production:
No
env_name:
topics:

Screenshot from 2026-02-11 20-37-15.png (41.2 KB) Teodor Gorghe, 02/11/2026 01:37 PM

History

#3 Updated by Constantin Asofiei 8 months ago

This is related also to the #10833-80 scenario, where it was proved that nonEmptyTableFlags is broken in case of rollback.

This second scenario is:
  • prog0.p
    def var h1 as handle.
    
    run prog1.p persistent set h1.
    dynamic-function("func0" in h1).
    
    do transaction:
       delete object h1.
       undo, leave.
    end.
    
  • prog1.p
    def temp-table tt2 field f1 as int.
    
    function func0 returns int.
       def var i as int.
    
       do i = 1 to 10:
          create tt2.
          tt2.f1 = i.
          release tt2.
       end.
    end.
    

What matters is that the population of the temp-table is done outside of the transaction, and the rollback inside the transaction acts on a deleted table.

So this scenario where a rollback is done on a deleted table, must not leave records in it...

When the multiplex is closed, the DELETE stmt which clears the records with the specific multiplex ID must not be recorded for undo, as otherwise a rollback will 'revive' them, although the 4gl temp-table has been deleted.

So the title maybe should not be fwd-h2 'killedMultiplexesCleanup' must not abend if the batch is not empty but instead deleting a temp-table inside a transaction must not record undo logs or something like this.

Something else to consider: when a temp-table gets deleted, the reference for any object in some record field gets decremented; thus destructors may executed while the temp-table gets deleted. So, does an UNDO revert any created records by such destructors?

If we set a savepoint in doCloseMultiplexScope() , and after that discard it completely (not commit!), this would affect any actions done in destructors, as those actions will be lost for rollback.

I don't see another way than add a NO-UNDO option for the DELETE statement in H2, so that we can do DELETEs which are not recorded for undoable purposes.

#4 Updated by Alexandru Lungu 8 months ago

I don't see another way than add a NO-UNDO option for the DELETE statement in H2, so that we can do DELETEs which are not recorded for undoable purposes.

This makes sense. The delete object should be aggressive enough to clear the logs so any eventual rollback won't revive records or mark the table as forcefully dropped and skip any undo log related to it. Dropping of table is transactional in H2 for FWD. By default, DROP is non-transactional so this is a bug we technically introduced. Note to self: there might be some complications with extent tables that are still in use for some customers.

Also, I wonder if there are other things that shall be handled:
  • in-memory DMOs rollback
    • this should be skipped as well ... we should not revive DMOs (or reset their DELETED state).
  • other state rollback (RecordNursery, dirty-share, etc.)
    • fortunately, dirty work is not capable of proper rollback, so I don't think it is a problem - but it should be tested.

So, does an UNDO revert any created records by such destructors?

Huh, interesting lead.

#5 Updated by Constantin Asofiei 8 months ago

Alexandru Lungu wrote:

So, does an UNDO revert any created records by such destructors?

Huh, interesting lead.

I've done a test like this:
  • prog0.p:
    def var h as handle.
    run prog1.p persistent set h.
    
    dynamic-function("func0" in h).
    
    find first book.
    message book.isbn.
    do transaction:
       delete object h.
       undo, leave.
    end.
    
    find first book.
    message book.isbn.
    
  • prog1.p:
    def temp-table tt1 field f1 as progress.lang.object.
    
    function func0 returns int.
    create tt1.
    tt1.f1 = new oo.foo().
    release tt1.
    
    end.
    
  • foo.p:
    // block-level on error undo, throw.
    
    class oo.Foo:
    
      destructor public foo().
         for each book: delete book. end.
         create book.
         book.isbn = "abc" + string(mtime).
         message book.isbn.
         release book.
      end.
    end.
    

Looks like there is a difference when the destructor is called from a delete object (i.e. temp-table destroyed) and a delete tt1.. In the delete record case, the destructor changes are rolled back. In the temp-table destroy case, the destructor changes are not rolled back.

So I think more experimentation is needed.

#6 Updated by Alexandru Lungu 8 months ago

In the temp-table destroy case, the destructor changes are not rolled back.

So you can create things in the persistent database that are not sensitive to rollback? Please try to create a book into the transaction from prog0.p - maybe OE thinks that the transaction block is useless considering that there are no proper DB operations and eventually makes the destructor a full transaction (?)

#7 Updated by Alexandru Lungu 8 months ago

Or maybe the destructor is deferred till the end of the transaction and we don't see that. Maybe a message after delete would help, to see if the destructor happens before/after that message.

#8 Updated by Ovidiu Maxiniuc 8 months ago

  • Assignee set to Ovidiu Maxiniuc
  • Status changed from New to WIP

#9 Updated by Constantin Asofiei 8 months ago

Ovidiu, lets solve the nonEmptyTableFlags issue in this task:
  • for NO-UNDO tables, this bitmap will work as is - rollback has no effect on these, so nonEmptyTableFlags will be consistent
  • for undoable tables, then rely on fastHasRecords

Otherwise, if it proves too difficult to solve/investigate the destructor conundrums, lets work on enhancing the UPDATE SQL statement syntax in H2 to add a NO-UNDO (Alex: any better idea on the name?) option - provide the proposed syntax and after we discuss it please implement it.

The point is that removeRecords when called from doCloseMultiplexScope, it will append this option to the DELETE statement. I don't recall if removeRecords will end up using FQL for the DELETE (when closing the multiplex scope) - in such case, we will need to update the fql.g grammar, too.

#10 Updated by Alexandru Lungu 8 months ago

Otherwise, if it proves too difficult to solve/investigate the destructor conundrums, lets work on enhancing the UPDATE SQL statement syntax in H2 to add a NO-UNDO (Alex: any better idea on the name?) option - provide the proposed syntax and after we discuss it please implement it.

I am ok with it. Currently we have CREATE TABLE ... NO-UNDO to symbolize that all actions are no-undo on that table. But if we need to be granular, I think it is fine to do so.
Note to self: ensure reclaiming of id is still stable with this.

The point is that removeRecords when called from doCloseMultiplexScope, it will append this option to the DELETE statement. I don't recall if removeRecords will end up using FQL for the DELETE (when closing the multiplex scope) - in such case, we will need to update the fql.g grammar, too.

Mind that this does TRUNCATE AFAIK.

#11 Updated by Constantin Asofiei 8 months ago

Alexandru Lungu wrote:

Mind that this does TRUNCATE AFAIK.

We can TRUNCATE only if there are separate SQL tables, and we get rid of the MULTIPLEX. Otherwise, we need DELETE ... NO-UNDO for the 'pseudo-truncate' to discard this multiplex.

#12 Updated by Alexandru Lungu 8 months ago

We can TRUNCATE only if there are separate SQL tables, and we get rid of the MULTIPLEX. Otherwise, we need DELETE ... NO-UNDO for the 'pseudo-truncate' to discard this multiplex.

Yes, but I mean the PageStoreTable.truncate. But in order to communicate from FWD to H2 to actually do a no-undo operation, I agree we need a keyword - we can't infer from SQL.

#13 Updated by Ovidiu Maxiniuc 8 months ago

  • % Done changed from 0 to 50

I addressed the no-undo issue on doCloseMultiplexScope. It seems to eliminate the crash, leaving the table in a usable state for other multiplexes. I am going now to create branches for both FWD and forked H2 projects. I will be using the trunks as base for branches.

Next, I will focus on nonEmptyTableFlags.

#14 Updated by Ovidiu Maxiniuc 8 months ago

  • Status changed from WIP to Review
  • % Done changed from 50 to 100
  • reviewer Alexandru Lungu, Constantin Asofiei added

I rebased and committed the final changes to FWD/10937a and H2/10937a_h2 branches. Now the testcases seem to work as expected (and no crashes any more).

#15 Updated by Constantin Asofiei 8 months ago

  • % Done changed from 100 to 80
  • Status changed from Review to WIP

Ovidiu, I don't think the H2 changes are OK.

The NO-UNDO at the DELETE statement when dropping the multiplex in a table may have no effect:
  • do 'create' outside of a transaction
  • do a normal 'delete' inside a transaction.
  • delete the temp-table (kill the multiplex) inside the transaction: this will see the table as 'definitely empty' (thus will not perform any DELETE ... NO-UNDO), but the H2 undo log will still have records.
Please change the approach to this:
  • remove the NO-UNDO at the DELETE - we can not rely on this to cleanup the undo log, just delete the records normally
  • in FWDDirectAccessDriver.killMultiplex needs to always cleanup the undo log for the specified multiplex

#16 Updated by Ovidiu Maxiniuc 7 months ago

Constantin,

I did additional tests and, all I could find was an NPE (which I have fixed and will be available in next commit). I tried your scenario (with both static and dynamic temp-tables) and they worked just fine. The explanation is that since killMultiplex() method is always called in the finally block after removeRecords(..., true /*noundo*/);, the effect is the same as you pointed out.

However, calling session.cleanUndoLog(t, multiplex); in killMultiplex() seems like a better solution. It would avoid tracking the additional noundo parameter over a couple of objects in H2. The downside is that, on the removeRecords() call, the UndoLogRecord will still be created and then marked as NOPs. It does not look like a big effort, but it's something we want to avoid, knowing that those records will actually only require cleanup effort.

#17 Updated by Ovidiu Maxiniuc 7 months ago

  • Status changed from WIP to Review
  • % Done changed from 80 to 100

I did multiple tests and the noundo is actually needed when in the doCloseMultiplexScope(). I committed the final changes in fwd-h2 10937a/66. The fwd/10937a branch remains unchanged (just rebased).

#18 Updated by Ovidiu Maxiniuc 7 months ago

I reverted changes which enhanced DELETE H2 statement with NOUNDO option, the management being performed from FWDDirectAccessDriver.killMultiplex(). I double tested both my testcases and the original test in application and everything seems fine.
Committed revisions:
  • fwd-h2/10937a: r67
  • fwd/10937a: r16319.

Ready for review / merge to 9457c.

#19 Updated by Constantin Asofiei 7 months ago

Alex - please review fwd-h2 changes.

#20 Updated by Alexandru Lungu 7 months ago

I am OK with the changes in fwd-h2/10937a. They are clean: on killMultiplex, replace any undo-entry regarding that table and multiplex with NOP. Cool!

for NO-UNDO tables, this bitmap will work as is - rollback has no effect on these, so nonEmptyTableFlags will be consistent

I just re-read this and I wonder if there is also a case in which we can't determine nonEmptyTableFlags: deleteRecords with WHERE clause. In this case, we don't know if the partial delete made the table empty or not, even if it is NO-UNDO. I guess we need to refresh nonEmptyTableFlags with fastHasRecords after partial delete. I need to think of other places where nonEmptyTableFlags is made inconsistent (copy-temp-table with replace/append? copy-temp-table from empty table? read empty xml into table, intake empty REST request, some things related to date-set relations? before-table related behaviors? etc.).

IMHO, keeping a consistent nonEmptyTableFlags is risky and may not be that relevant, considering that fastHasRecords is indeed fast (search in the b-tree a records with the provided multiplex). Otheriwse, I think we can change the implementation to actually look into MultiplexedScanIndex.lastBatches (map look-up). This will bypass the whole infrastructure to manage nonEmptyTableFlags and risk not keeping it in sync.

#21 Updated by Constantin Asofiei 7 months ago

Ovidiu, at this time I don't think I'll add 10937a to 9457c - 9457c already has a change which always uses this:

   protected boolean isTableDefinitelyEmpty()
   {
      Boolean has = fastHasRecords();
      return has != null && !has.booleanValue();
   }

We need to delay 10937a until we figure out if nonEmptyTableFlags can be kept as is or not.

The main fix is in 10937a_h2 - I'll create a 10937b with the fwd-h2 revision update and merge 10937a_h2 to fwd-h2 trunk.

#22 Updated by Constantin Asofiei 7 months ago

Alexandru - there is another change I want to add to 10937a_h2. In UndoLog.cleanTable, I think we can be no-op if the table is no-undo, right?

#23 Updated by Alexandru Lungu 7 months ago

Constantin Asofiei wrote:

Alexandru - there is another change I want to add to 10937a_h2. In UndoLog.cleanTable, I think we can be no-op if the table is no-undo, right?

This is correct. If the table is no-undo, the undo-log is not populated.

#24 Updated by Constantin Asofiei 7 months ago

Alexandru Lungu wrote:

Constantin Asofiei wrote:

Alexandru - there is another change I want to add to 10937a_h2. In UndoLog.cleanTable, I think we can be no-op if the table is no-undo, right?

This is correct. If the table is no-undo, the undo-log is not populated.

Added to 10937a_h2 rev 69

#25 Updated by Ovidiu Maxiniuc 7 months ago

Constantin,
although I am not against it, I do not think the new code in cleanTable() will be executed. As Alex mentioned, for NOUNDO tables, the undoLog is not initialized (it remains null because no logs are stored) and the only place where the method is invoked is cleanUndoLog(), which makes sure the undoLog was created.

I think this is my fault. With my last commit, I forgot to update the javadoc and error messages in thrown exceptions which may lead to confusion. I will update those to reflect the actual situation.

#26 Updated by Constantin Asofiei 7 months ago

Session.cleanUndoLog usage of undoLog != null is database-wide no-undo for all temp-tables, Table.isNoUndo() is something else (per-table flag). So is needed, please leave my change intact.

#27 Updated by Constantin Asofiei 7 months ago

10937a_h2 was merged to fwd-h2 trunk rev 64 and archived as merged.

10937b was created from trunk rev 16321 - 16322 upgrades the rev to fwd-h2 64.

#28 Updated by Alexandru Lungu 7 months ago

Anything left to review?

#29 Updated by Constantin Asofiei 7 months ago

  • % Done changed from 100 to 90
  • Status changed from Review to WIP

Alexandru Lungu wrote:

Anything left to review?

Not at this time.

I'm leaving this open for the discussion about nonEmptyTableFlags and isTableDefinitelyEmpty(). We need a proper fix for this also - maybe also FWD-H2 improvement.

#30 Updated by Constantin Asofiei 6 months ago

Branch 10937b was merged to trunk rev 16396 and archived.

What remains is to decide how to improve the nonEmptyFlags and isTableDefinitelyEmpty - Ovidiu, please work on this.

#31 Updated by Teodor Gorghe 5 months ago

  • Assignee changed from Ovidiu Maxiniuc to Teodor Gorghe

#32 Updated by Ovidiu Maxiniuc 5 months ago

Teodor,
Although 10937b was already merged into trunk, there are some changes in 10937a which might be useful. What you should consider:
  • method TemporaryBuffer.rollback(boolean transaction). Clearly, the local.nonEmptyTableFlags set should be updated. But there are two kind of changes on undo:
    • if the original operation was a CREATE, the respective table was removed from this set. Do we add it now? We do not have the information it was there before to add it back.
    • if the original operation was a DELETE, the respective table might be deleted now. Do we remove it now?
In a rollback/undo operation both of the above case may happen, for different tables. The rollback() method does not have any information for the above situations, so from my PoV, this is not the right location for managing nonEmptyTableFlags content.
  • method TemporaryBuffer.isTableDefinitelyEmpty(). In trunk, relies solely on local.nonEmptyTableFlags to decide the result. As noted above, this is not reliable. Even with the most pessimistic approach, we need to go to the lowest level and 'ask' directly the H2 layer using fastHasRecords(). Reaching SQL is a bit costly, but is the only way to get the correct result.
  • the other changes in that branch are just cosmetic: small javadoc improvements and code style updates.

#33 Updated by Teodor Gorghe 5 months ago

Ovidiu Maxiniuc wrote:

Teodor,
Although 10937b was already merged into trunk, there are some changes in 10937a which might be useful. What you should consider:
  • method TemporaryBuffer.rollback(boolean transaction). Clearly, the local.nonEmptyTableFlags set should be updated. But there are two kind of changes on undo:
    • if the original operation was a CREATE, the respective table was removed from this set. Do we add it now? We do not have the information it was there before to add it back.
    • if the original operation was a DELETE, the respective table might be deleted now. Do we remove it now?

And why do you think that the information is not enough? We can't just save local.nonEmptyTableFlags at the start of transaction and then restore it when rollback is being called?

#34 Updated by Teodor Gorghe 5 months ago

I have done some performance tests with and without nonEmptyTableFlags and I can confirm that the performance is better without RoaringBitmap.

#35 Updated by Constantin Asofiei 5 months ago

Teodor Gorghe wrote:

I have done some performance tests with and without nonEmptyTableFlags and I can confirm that the performance is better without RoaringBitmap.

Lets confirm this with performance testing of other applications (long-running reports, performance harness/testing, etc). If it confirms that it does not regress performance, we will remove nonEmptyTableFlags.

#36 Updated by Teodor Gorghe 5 months ago

  • Status changed from WIP to Internal Test

Committed revision 16320 on task branch 10937a:
- Replaced nonEmptyTableFlags with H2 fastHasRecords.

#37 Updated by Constantin Asofiei 3 months ago

Teodor, what testing is left here? If ETF, ChUI and unit tests for other apps passed, then is OK to merge.

#38 Updated by Teodor Gorghe 3 months ago

They passed and the performance for the remaining apps is the same.

#39 Updated by Constantin Asofiei 3 months ago

  • % Done changed from 90 to 100
  • Status changed from Internal Test to Merge Pending

Please merge now.

#40 Updated by Teodor Gorghe 3 months ago

  • Status changed from Merge Pending to Test

Branch 10937a was merged into trunk as rev. 16518 and archived.

Also available in: Atom PDF