Project

General

Profile

Bug #10026

A multi-table AdaptiveQuery running off-end is still updating the cursor

Added by Alexandru Lungu over 1 year ago. Updated 7 days ago.

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

100%

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

10026.patch Magnifier (1.77 KB) Alexandru Lungu, 05/21/2025 04:34 AM


Related issues

Related to Database - Bug #12094: NO-WAIT support for GET CURRENT and the GET methods (lock conflicts) New

History

#1 Updated by Alexandru Lungu over 1 year ago

  • Assignee set to Stefanel Pezamosca
  • File 10026.patchMagnifier added

From #10000:

First and foremost, there is Eduard's change in trunk rev. 15701 where buffers from components 2 onward are not released on off-end. This is a correct fix from #9538.
  • However, this exposes a limitation in AdaptiveQuery where the cursor.addResultFirst(getCurrentIds()); is called unconditionally. It should have checked whether the query actually run off-end and then call cursor.addResultFirst(null);. [...] we need a better synchronization between results and cursor: the cursor is empty when the results are empty and cursor has records when the results actually fetched something.

I am attaching my attempt.

This covers the example in #9538-69 - but scrolling variant. Debug it and check that after running off-end, the row is also added to the cursor instead of null to signal last entry.
With my attempt, FIND CURRENT becomes broken, because not updating the cursor will let is eventually empty. A FIND CURRENT on an example like #10000-34 will yield an error.

Goals:
  • Extend the FIND CURRENT test-case suite to cover: scrolling, non-scrolling queries + browsed queries + multi-table queries (with optimized components or not).
    • run some nexts and run FIND CURRENT.
    • run some nexts and then some prevs + run FIND CURRENT (the buffers should have been loaded by loadByValue now).
    • etc.
  • Check if there are other places where the query should decide whether updating the cursor with ids or null is right. There may be other places apart from the ones in my patch.
  • Fix GET CURRENT to work even if that component didn't find anything. It is possible to have a record loaded using loadByValues, but the results and cursor still be empty (and the query off-end). Maybe calling the CURRENT from CompoundQuery should by-pass such checks. The CURRENT solely relies on buffer contents, so it is irrelevant if the query is off-end.
  • Extra: please check in OE what happens if buffers are loaded with something else before a CURRENT. Will the CURRENT honor whatever is already loaded or reload the queries with the last entries found?

Mind that some symptoms of this issue were seen in #10000 and fixed properly. However, this issue has the potential to generate logical mismatches which may be hard to understand with the debugger.

#2 Updated by Stefanel Pezamosca over 1 year ago

  • Status changed from New to WIP

#3 Updated by Stefanel Pezamosca over 1 year ago

Alexandru Lungu wrote:

  • Extra: please check in OE what happens if buffers are loaded with something else before a CURRENT. Will the CURRENT honor whatever is already loaded or reload the queries with the last entries found?

I have observed that for a scrolling query. GET-CURRENT will load previously found records by the query. For non-scrolling case the CURRENT will honor the already loaded records.

Fix GET CURRENT to work even if that component didn't find anything. It is possible to have a record loaded using loadByValues, but the results and cursor still be empty (and the query off-end). Maybe calling the CURRENT from CompoundQuery should by-pass such checks. The CURRENT solely relies on buffer contents, so it is irrelevant if the query is off-end.

Do you have an example/testcase that shows this? At the moment I don't understand how this case should look like such that GET CURRENT would not work.

#4 Updated by Alexandru Lungu over 1 year ago

Do you have an example/testcase that shows this? At the moment I don't understand how this case should look like such that GET CURRENT would not work.

I get provide some pseudo-code:

  • Run a compound query with no optimized components EACH/EACH. The results found should be (1, 2), (1, 3), (2, 3) (so a x < y condition should do the trick).
  • Go through all rows with next till off-end (including off-end).
  • The first AQ is at record 3; the second AQ has no records
  • Run get first. This will call loadByValues and run load on both queries with (1) and (2).
    • Thus, the first AQ is re-positioned to row 1. Second AQ is not re-positioned .... its buffer is simply loaded with 2.
  • Run get current. First AQ will reload row 1. Second AQ will reload the buffer; however, a call to getRow from CompoundQuery will end up in AQ.getRow that checks whether it has any results. It doesn't ... so this fails.

I fixed this in #10000 by not calling getRow for scrolling queries (that are prone to loadByValues), but rather take input from buffers directly to bypass the checking whether the query has results. It doesn't ... but it was loaded with loadByValues previously.

#6 Updated by Stefanel Pezamosca over 1 year ago

I made a testcase:

The output in OE is:
(1,2)
(1,3)
(2,3)
(?,?)
(1,2)
(1,2)
(3,3)
(3,3)
======Scrolling======
(1,2)
(1,3)
(2,3)
(?,?)
(1,2)
(1,2)
(3,3)
Warning: A buffer in query qh_scroll contains a different record than the current query result list entry. (4108)
Warning: A buffer in query qh_scroll contains a different record than the current query result list entry. (4108)
(1,2)
The output in FWD is this:
(1,2)
(1,3)
(2,3)
(?,?)
(1,2)
(1,2)
(3,3)
(3,3)
======Scrolling======
(1,2)
(1,3)
(2,3)
(?,?)
(1,2)
No query record is available. (4114)
(1,2)
(3,3)
(3,3)
There are clearly some differences that should be fixed.

#7 Updated by Stefanel Pezamosca over 1 year ago

I made some more experiments related with Scrolling/Non-Scrolling Queries.
For this testcase:

I get the following result:
I also found some relevant tests in xfer/testcases/abl/tests/query/adaptive/ that I could look into, as currently there are some failures. I'm currently investigating and experimenting with some changes.

#8 Updated by Stefanel Pezamosca about 1 month ago

  • % Done changed from 0 to 90

Branch 10026a (trunk r16766) fixes GET CURRENT / GET-CURRENT() for compound and single-table queries. All behavior was checked against OpenEdge 12.8.

Changes:
  • CURRENT on a scrolling query reloads the current result-list entry: warning 4108 for each buffer that holds a different record, 4114 when there is no row (off-end, between rows after REPOSITION, deleted row).
  • CURRENT on a non-scrolling query refetches the records in its buffers, with 4114 only when no buffer holds a record. GET NEXT / GET PREV on a non-scrolling query stays off-end.
  • 4114, 4108 and 3164 follow the 4GL error mode: shown in a plain block, recorded in ERROR-STATUS under NO-ERROR, raised under CATCH or BLOCK-/ROUTINE-LEVEL ON ERROR UNDO, THROW. GET-CURRENT() no longer swallows the raised error.
  • REPOSITION on any non-scrolling query reports 3164, not only on FORWARD-ONLY ones.
  • CompoundQuery component loading was refactored (r16767), and AdaptiveQuery no longer adds a cursor row when nothing was found.
  • Regression fix in FQLPreprocessor: the "or is null" rewrite of ? < field and ? <= field keeps the operand order.
Testcases: the testcases from the previous notes will be merged into tests/query/TestGetCurrent.cls (19 tests, all passing).
Results:
  • Both testcases from the previous notes and the browse probe (Swing and web client) give the same output as OpenEdge.
  • tests/query and tests/persistence: no regressions (2365 passed, 412 failed; the same failures as before the change).
Still open:
  • PRESELECT and presorted queries do not reload the result-list entry on CURRENT (OpenEdge warns 4108), and REPOSITION on a non-scrolling PRESELECT query does not report 3164.
  • With several stale buffers under CATCH, OpenEdge raises one error carrying every 4108; FWD raises after the first.
  • OUTER-JOIN cases will be handled in #11576.

#10 Updated by Stefanel Pezamosca 28 days ago

  • reviewer Alexandru Lungu added

I'm working on fixing some issues flagged by a self-review. Then I will clean-up some things and set it to review status.

#11 Updated by Stefanel Pezamosca 28 days ago

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

I fixed some issues, and I think 10026a is ready for a proper review.

10026a(based on trunk r16766) r16769 to r16771:
  • GET CURRENT on a scrolling query between rows reports 4114 and keeps the buffers, for all query types.
  • A join keeps the buffers of the components whose records still exist when one record of the current entry is gone.
  • A lock conflict while reloading the entry is now reported, no longer swallowed (4108 followed by locking the wrong record).
  • When the entry was fully loaded, GET CURRENT no longer reloads every buffer a second time.
  • GET-CURRENT(lock) on a closed query reports "not open" again, instead of a Java NPE on an unknown lock.
  • No 3164 on REPOSITION when FORWARD-ONLY was assigned FALSE, and no 4114 after a 4108.
  • A non-scrolling reposition no longer recurses, and first/last no longer cache a stale row when nothing is found.

#12 Updated by Alexandru Lungu 25 days ago

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

Review of 10026a:

-               else
-               {
-                  try
-                  {
-                     available = super.first(lockType);
-                  }
-                  catch (QueryOffEndException e)
-                  {
-                     cursor.addResultFirst(null);
-                     throw e;
-                  }
-               }

-               cursor.addResultFirst(getCurrentIds());
+               available = cacheFirst(lockType);
  • This doesn't look right as the logic that is now in cacheFirst is no longer wrapped in the else block and is run unconditionally. All other places that compress the super.first logic ported to cacheFirst are not within else blocks.
  • Make noQueryRecord return false after ErrorManager work. The users should return noQueryRecord this will allow us to easier short-circuit work after noQueryRecord by simply running return noQueryRecord. Also, the method naming should be a verb, for example throwNoRecordAvailable or failWithNoRecordAvailable or other suitable verb name.
  • Please use {@code true} instead of .
+      if (entry == null)
+      {
+         return 0;
+      }
  • Is this a legal state or a safe check? If it is a safe check, please add a log. Same applies to if (!isScrolling() || cursor null || _isOffEnd() || cursor.isBetweenRows()). In some circumstances, calling reloadResultListEntry is wrong rather then expecting 0. So I prefer to know if we have non-scrolling queries that call reloadResultListEntry instead of silently fallbacking to 0.
    • IMHO, this hides a potential problem from logs or diagnosis data.

- super.reposition(row);
+ super.reposition(row, internal);

  • I still need to assess this
  • Please try to compress history entries to 1/2 lines. If they are too big, it means that information stored there is verbose enough to be included in javadocs instead.
+      else if (!iterating && OffEnd.BACK.equals(getOffEnd()))
+      {
+         // a non-scrolling query stays off-end instead of restarting
+         releaseBuffers();
+         
+         return false;
+      }
  • Can you point me to a test for this logic. I am inclined to say the logic is right, but from my experience, CQ is not always realasing all buffers under all circumstances. If I recall correctly, in an INDEXED-REPOSITION multi-table query with both EACH and FIRST, only EACH buffers are released. There may be other cases (maybe after REPOSITION and then going off-end using NEXT).
  • staleBuffers I think it makes more sense to return 0 if there is no stale buffer and a positive number instead. I find it hard to associate -1 buffers being stale as "no buffers" being stale. This confusion manifested in:
+      int stale = staleBuffers(entry, buffers);
+      if (stale < 0)
+      {
+         return 0;
+      }
The check should have been stale <= 0 for that matter. Anyway, please return 0 is no buffer is stale and this check should be 0.
  • Please double-check if the lockType of the reloadResultListEntry governs the locking system instead of the "per-query-components" locking. Also check if lockType is missing, the query component locking is honored instead.
  • I don't think the changes in FQLPreprocessor are in scope.
  • public default int reloadResultListEntry(LockType lockType) is made default to return 0. Shouldn't we consider implementations instead to ensure the fix is complete: PreselectQuery, PresortQuery, etc. same for isBetweenRows. I usually prefer to force queries to have this even with a default value, but stress the developer to think about it. A new query won't be forced to think about these 2 new entries.
  • CURRENT_REFETCH, CURRENT_NONE and CURRENT_LOADED seem reasonable for an enum. This also forces beforeCurrent to return a well scoped enum value (e.g. not -1).
  • Why is forwardOnlyAssigned needed; isn't forwardOnly enough? AFAIK, a query wrapper can be neither scrolling nor forwardOnly. It can be forwardOnly only if assigned. Correct me if I am wrong.
  • beforeCurrent should be renamed to a verb (e.g. isBeforeCurrent). In fact, I think this method actually check the disposition in case of a CURRENT, so the name shall be getDispositionForCurrent which returns the enum in question.
  • currentRecords to canGetCurrentRecords.
  • empty = (((BufferReference) buffers.get(i)).buffer().getCurrentRecord() == null); this is the available check. Use available API. empty should be in fact allBuffersLoaded which seems quite opposite to empty. It is initialized with true and becomes false when encountering at least one non available buffer.
    • Check the behavior for outer-join. A buffer can be "semi-"unavailable, aka it was last used in an outer-join query and it has no record, but it is in unknownMode

#13 Updated by Alexandru Lungu 25 days ago

AI Review:

  • PreselectQuery.isBetweenRows is not overridden. The new "scrolling query between rows reports 4114" rule only works for FOR EACH queries. PRESELECT, presort and dynamic PRESELECT queries skip it. For example, REPOSITION q TO ROW 3 followed by GET CURRENT q on a PRESELECT query silently re-reads the old row-1 record and returns TRUE. The fix is to override it as isScrolling() && betweenRows && !_isOffEnd(). The isScrolling() guard is needed because betweenRows starts out true.

Indeed, this comes from the default interface impl.

  • QueryWrapper.getCurrent: the new entry reload runs outside the try/catch that turns errors into FALSE. A lock conflict such as GET-CURRENT on a stale entry now raises ERROR instead of returning FALSE.
  • QueryWrapper.beforeCurrent: when it reloads a stale entry it skips the refetch, so CURRENT-CHANGED stays FALSE even if another session updated the record.
  • QueryWrapper.beforeCurrent javadoc does not match what the code does. GET-CURRENT returning TRUE together with 4114 when the query isn't off-end is correct. Only the text needs fixing.
  • AbstractQuery.staleBuffers javadoc says only buffers holding a different record are counted, but empty buffers are counted too. The code looks intentional, so this is a documentation fix.
  • Two blank lines were removed before return statements: in AdaptiveQuery.first and in CompoundQuery.processComponent.

#14 Updated by Stefanel Pezamosca 21 days ago

Thanks for the review. Answers below; I'm working on the agreed fixes now.

#10026-12
  • cacheFirst outside else: the behaviour is unchanged. The if branch returns next(lockType), so the code after it only runs on the former else path.
  • noQueryRecord: agreed. I will rename it to failNoQueryRecord() and make it return false, so callers can return failNoQueryRecord();.
  • {@code true} instead of <code>: agreed, for the new and changed javadoc.
  • Guards in reloadResultListEntry: !isScrolling() and _isOffEnd() are legal states. beforeCurrent calls it for every delegate: a non-scrolling FOR EACH honours its buffers, and off-end is reported by the delegate (scrolling) or keeps the buffers (non-scrolling). In AdaptiveQuery, cursor.isBetweenRows() is already handled by beforeCurrent before the call. cursor null and entry null are defensive checks for states not expected on a scrolling query positioned on a row, so I will log them as WARNING.
  • super.reposition(row, internal): this fixes a recursion. For a non-scrolling AQ, super.reposition(row) resolved to P2JQuery.reposition(int), which calls this.reposition(row, false) and lands back in AdaptiveQuery.reposition(int, boolean). That path is reachable after FORWARD-ONLY = FALSE and ends in a StackOverflowError. The fix also passes internal instead of dropping it.
  • History entries: I will compress them.
  • Non-scrolling query staying off-end: there are tests for an EACH/EACH compound query. After running off-end, a further GET NEXT returns FALSE, stays off-end with empty buffers, and a GET PREV then returns the last row. After running off the front, a further GET PREV stays off-end and a GET NEXT returns the first row. The OE baseline for this covers only a single-table query. The INDEXED-REPOSITION case does not apply, since a non-scrolling query cannot reposition (3164). I will add multi-table EACH/EACH, EACH/FIRST and "FIND between off-end and NEXT" cases and check in OE whether the second NEXT should touch the buffers at all.
  • staleBuffers: agreed. The -1 vs 0 split comes from an OE-verified case: a scrolling OUTER-JOIN query is on an entry with no inner record, a FIND loads a record into the inner buffer, and GET CURRENT clears that buffer again without 4108. I will split it into matchesEntry(..) and staleBuffers(..) (0 = none stale).
  • lockType: verified. An explicit lock overrides every component (PreselectQuery.coreFetch, RandomAccessQuery.load, and CompoundQuery.loadQuery passes it through). With null, each component uses its own lock, the same as the delegate's current().
  • FQLPreprocessor: agreed, it is not in scope. It is an existing trunk bug found while running the query and persistence tests. Should I move this into another task ?
  • Default reloadResultListEntry / isBetweenRows: agreed. I will make them abstract and implement them in PreselectQuery (covers AQ and presort), CompoundQuery (covers presort compound), RandomAccessQuery and QueryWrapper (including its null query). The missing PreselectQuery.isBetweenRows below is exactly this gap.
  • CURRENT_* enum, and renaming beforeCurrent to getDispositionForCurrent: agreed.
  • forwardOnlyAssigned: it is needed. In OE, a static query defined without SCROLLING reports 3164 on REPOSITION, but the same query repositions fine once FORWARD-ONLY = FALSE is assigned. Both states have forwardOnly == false. The alternative is a tri-state Boolean, if you prefer it.
  • currentRecords: it performs the GET-CURRENT rather than checking whether it can, so I propose getCurrentRecords instead of canGetCurrentRecords.
  • empty: it means "no buffer holds a record"; the loop stops at the first available buffer. That is the opposite of allBuffersLoaded. I will invert it into anyAvailable using isAvailable(). An OUTER-JOIN buffer in unknownMode has no record, so it counts as not available, matching 4GL AVAILABLE. The remaining OUTER-JOIN cases go to #11576.
#10026-13
  • PreselectQuery.isBetweenRows: valid. I will override it as isScrolling() && betweenRows && !_isOffEnd() and add a test for REPOSITION followed by GET CURRENT on PRESELECT and presorted queries.
  • Lock conflict in GET-CURRENT(): valid. The entry reload runs in QueryWrapper, outside the delegate's try/catch that turns errors into FALSE. I will apply the same error handling to the reload. I will also test GET-CURRENT(EXCLUSIVE-LOCK, NO-WAIT) when another session locks the entry's record and the buffer holds another record; so far only the GET CURRENT statement was checked in OE.
  • CURRENT-CHANGED: valid for a join where only some buffers are stale. The buffers that already held their entry record are reloaded without the CURRENT-CHANGED arming, and the refetch is then skipped. I will arm/update around the reload and check in OE what the stale buffer itself should report.
  • beforeCurrent and staleBuffers javadoc: valid; documentation fix only. Counting empty buffers is intended: in OE, GET CURRENT on a scrolling join after both buffers were released gives 4108 twice.
  • The two removed blank lines will be restored.

#15 Updated by Stefanel Pezamosca 20 days ago

  • % Done changed from 90 to 100
  • Status changed from WIP to Review
Committed the review fixes for #10026-12/13 in 10026a r16772:
  • reloadResultListEntry and isBetweenRows are abstract and implemented for every query type.
  • PRESELECT and presort queries report 4114 between rows and right after OPEN.
  • A non-scrolling join that is already off-end releases only the outer buffer on another NEXT/PREV.
  • FORWARD-ONLY = FALSE makes the query scrolling, as in OE.
  • A NO-WAIT lock conflict on CURRENT reads the record NO-LOCK and sets LOCKED.
  • A reloaded stale buffer sets CURRENT-CHANGED.
  • The FQLPreprocessor fix moved to #10729, #11992.

Issues found while testing 10026a that are outside this branch

# Issue OE FWD
1 QUERY q:GET-CURRENT(EXCLUSIVE-LOCK, NO-WAIT) is converted without NO-WAIT no wait, LOCKED = yes blocks until the lock is released
4 Scrolling FOR EACH, on row 2 of 3: REPOSITION-FORWARD(1) buffer empty buffer keeps row 2
5 Scrolling PRESELECT, same case as 4 QUERY-OFF-END = no QUERY-OFF-END = yes
6 Scrolling two-table PRESELECT/presort: GET FIRST, REPOSITION-TO-ROW(2) (1,?), only the inner buffer released (?,?)
Notes:
  • 4: with 10026a, FORWARD-ONLY = FALSE makes a query scrolling, as it does in OE. Because of #4, TestForwardOnlySet3#testnoscrolling36 now fails; it passed before only because that query took the non-scrolling path.

#16 Updated by Alexandru Lungu 19 days ago

  • [MAJOR] functional RandomAccessQuery.current: The method now calls buffer.reloadCurrent(lockType, isErrorIfNull()), which turns a NO-WAIT lock conflict into a NO-LOCK read with LOCKED set and no error. FindQuery overrides current() / current(LockType) only to call initializeQuery() and then super.current(...), and database_access.rules maps FIND CURRENT to current, or to silentCurrent under NO-ERROR. Trigger: FIND CURRENT customer EXCLUSIVE-LOCK NO-WAIT [NO-ERROR] while another session holds the record. Before, RecordBuffer.load caught LockUnavailableException and called recordOrThrowError, so ERROR was raised (or ERROR-STATUS:ERROR was set under NO-ERROR). Now lockForCurrent swallows the exception and the statement completes silently with a NO-LOCK copy; outside a transaction any SHARE lock already held is given up. The dynamic BUFFER:FIND-CURRENT() still goes through BufferImpl._current → recBuf.reload, so static and dynamic FIND CURRENT now behave differently. This also contradicts the reloadCurrent javadoc ("Unlike FIND CURRENT ..."). Restrict the NO-LOCK fallback to the query GET CURRENT path, for example by having FindQuery.current keep the old armCurrentChanged / reload / updateCurrentChanged sequence.\
  • [MINOR] functional CompoundQuery.reloadResultListEntry: When lockType == null (plain GET CURRENT q / GET-CURRENT()), lockTypes stays null, so loadEntryForCurrent never runs a component's own NO-WAIT lock through lockForCurrent. The component load falls back to its own lock type, and RandomAccessQuery.loadByValue sends the LockUnavailableException to recordOrThrowError. CompoundQuery.loadQuery rethrows the resulting ConditionException, and loadEntryForCurrent does not catch it. Trigger: scrolling OPEN QUERY q FOR EACH a EXCLUSIVE-LOCK NO-WAIT, EACH b outside a transaction, GET NEXT, a FIND on a that makes the buffer stale, another session locking the entry's a record, then GET CURRENT q. ERROR is raised (with NO-ERROR the buffer is left empty and LOCKED is not set). PreselectQuery.reloadEntry and RecordBuffer.reloadCurrent read the record NO-LOCK and set LOCKED in the same situation. Build lockTypes from each component query's lock type when lockType is null.

These 2 are related to locking.
I think this is the one you described in your last note, right? Is this a regression caused by 10026a or is it also in trunk?

  • [MAJOR] functional CompoundQuery.next: The new !iterating && OffEnd.BACK.equals(getOffEnd()) short-circuit reads component 0's off-end state, which nothing on the open/close path resets. CompoundQuery.open resets only its own fields, and the components' cleanup() leaves off-end alone (RecordBuffer.offEnd for RandomAccessQuery, the offEnd field for AdaptiveQuery / PreselectQuery). Components are reset lazily in processComponent, which runs after the short-circuit. Trigger: a non-scrolling multi-table dynamic query with a non-optimized join (QUERY-PREPARE("FOR EACH a, EACH b WHERE ...")) that is run off-end with GET-NEXT and then reopened with QUERY-OPEN(). QueryWrapper.queryOpen calls close() / open() on the same delegate and components. The first GET-NEXT() then returns FALSE and releases the outer buffer instead of restarting at the first row. previous has the same problem with OffEnd.FRONT. Also require that a retrieval happened since the open (e.g. counters != null / activeIndex >= 0), or reset the component off-end state in open().

This seems reasonable.

  • [MINOR] functional QueryWrapper.getCurrentRecords: getDispositionForCurrent (via reloadResultListEntry and loadEntryForCurrent) now runs before delegate.getCurrent, outside AbstractQuery.wrapCurrent's try/catch, which maps ErrorConditionException to FALSE for the GET-CURRENT method. The same applies to getCurrent() and its overloads. Trigger: q:GET-CURRENT() on a scrolling CompoundQuery defined with EXCLUSIVE-LOCK NO-WAIT, with a stale buffer and the entry record locked by another session (the path in the previous finding). The same happens with q:GET-CURRENT(EXCLUSIVE-LOCK) when a -lkwtmo lock timeout fires in loadByValue. ERROR escapes the method instead of FALSE being returned. Apply the wrapCurrent handling around the disposition step in the method variants: rethrow DeferredLegacyErrorException and map ErrorConditionException to FALSE.
  • [MINOR] functional QueryWrapper.getDispositionForCurrent: Each stale-buffer 4108 goes through its own ErrorManager.recordOrShowWarning call inside the loop. Under CATCH or BLOCK-/ROUTINE-LEVEL ON ERROR UNDO, THROW, recordOrShowError upgrades the warning and throws DeferredLegacyErrorException on the first iteration, so the error carries only one 4108. OE 12.8 raises one error carrying every 4108. Trigger: two-table SCROLLING query, GET FIRST, FIND LAST on both tables, then GET CURRENT inside a CATCH; OE gives NumMessages = 2 and FWD gives 1.

I think these are optional. It may be subject to the lack of proper structured error handling in FWD. If it is a quick fix, please do it, otherwise ignore it. Also, the first point is about locking.

Conclusion: if the locking limitation is also in trunk and not a regression feel free to defer it for now.

#17 Updated by Alexandru Lungu 19 days ago

Stefanel, are #11992 changes required in trunk to also have #10026? In other words, are they inter-dependent or simply additive one to the other?

#18 Updated by Stefanel Pezamosca 19 days ago

Alexandru Lungu wrote:

Stefanel, are #11992 changes required in trunk to also have #10026? In other words, are they inter-dependent or simply additive one to the other?

They are independent.

#19 Updated by Alexandru Lungu 15 days ago

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

Please assess #10026-16. If there is nothing to be dome, please revert back to review/100%

#20 Updated by Stefanel Pezamosca 15 days ago

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

Status of the #10026-16 review (10026a, based on trunk r16805) committed in revision 16812:

  1. [MAJOR] FIND CURRENT ... NO-WAIT reads NO-LOCK silently: fixed. FindQuery.refetchCurrent keeps the old reload path and RecordBuffer.load reports 445, so static and dynamic FIND CURRENT raise the lock conflict again, as in OE.
  2. [MINOR] CompoundQuery.reloadResultListEntry ignores a component's own NO-WAIT: no change needed. OE rejects NO-WAIT on any query component with 247 (FOR EACH, DO PRESELECT, OPEN QUERY, OPEN QUERY PRESELECT, static and dynamic QUERY-PREPARE), so the trigger is not valid 4GL. FWD's QUERY-PREPARE reports the same 247.
  3. [MAJOR] reopened non-scrolling CompoundQuery stays off-end: not reproducible, no change needed. Component 0 keeps its off-end state through close(), as you say, but neither reopen path reuses it: QueryWrapper.queryOpen() runs notifyQueryOpenListeners(true) between close() and open(), and DynamicQueryHelper's listener re-executes the prepared query, which resets the components; a static OPEN QUERY builds a new CompoundQuery and new components on each open. Traced with a non-optimized join (CONTAINS in the predicate keeps FWD from joining server-side): dynamic QUERY-OPEN(), QUERY-CLOSE() + QUERY-OPEN(), and a static query without SCROLLING reopened twice or run off the front, all restart at the first row, as in OE. A RandomAccessQuery component 0 cannot occur: OE rejects a FIRST outer table in a query with 3322.
  4. [MINOR] GET-CURRENT() raises ERROR from the entry reload: fixed. QueryWrapper.getCurrentRecords maps that ERROR to FALSE.
  5. [MINOR] under CATCH only one 4108 is raised: fixed. ErrorManager.recordOrShowWarnings raises one error carrying every 4108.

#21 Updated by Alexandru Lungu 13 days ago

I have only:

  • MAJOR: CompoundQuery.reloadResultListEntry: GET CURRENT q with no lock phrase can reload the wrong record. This happens on a scrolling client-side join opened with EXCLUSIVE-LOCK NO-WAIT, when another session locks the entry record and the buffer holds a different record. The lock conflict is swallowed, the old record is re-read and locked, and LOCKED is not set. PreselectQuery.reloadEntry already handles this case correctly. The fix is to derive the lock types from the components, as it does.
  • MINOR: RecordBuffer.reloadCurrent: outside a transaction, a failed EXCLUSIVE-LOCK NO-WAIT upgrade drops the SHARE lock the session already holds. lockForCurrent returns NONE, which relinquishes that lock. It should fall back to the pinned lock type.

I will start testing on my side. Let me know if you have the chance to look into these 2 items.
At the end, I will do a final manual review.

#22 Updated by Alexandru Lungu 13 days ago

  • Status changed from Review to Internal Test

#23 Updated by Stefanel Pezamosca 13 days ago

Alexandru Lungu wrote:

  • MAJOR: CompoundQuery.reloadResultListEntry: GET CURRENT q with no lock phrase can reload the wrong record. This happens on a scrolling client-side join opened with EXCLUSIVE-LOCK NO-WAIT, when another session locks the entry record and the buffer holds a different record. The lock conflict is swallowed, the old record is re-read and locked, and LOCKED is not set. PreselectQuery.reloadEntry already handles this case correctly. The fix is to derive the lock types from the components, as it does.
  • MINOR: RecordBuffer.reloadCurrent: outside a transaction, a failed EXCLUSIVE-LOCK NO-WAIT upgrade drops the SHARE lock the session already holds. lockForCurrent returns NONE, which relinquishes that lock. It should fall back to the pinned lock type.

I have a few findings related to these two, that seem to work wrong in trunk too, I'm waiting for the test results and then report.
In the end, I'll send an email to request this branch to be tested for more projects.

#24 Updated by Stefanel Pezamosca 13 days ago

I checked both review findings with two-session probes on OpenEdge, trunk (r16809) and this branch: session A runs the GET while session B holds a lock, then B reports which locks A holds.

Summary: neither finding can happen in valid 4GL, so neither proposed fix is needed. Each one points to a real gap, and every gap below already exists in trunk. The branch's conversion change for NO-WAIT on the lock methods is correct.

Finding 1: CompoundQuery.reloadResultListEntry

OpenEdge rejects NO-WAIT in any query record phrase (OPEN QUERY, FOR EACH, DO PRESELECT, QUERY-PREPARE) with error 247, so a query component never has a NO-WAIT lock. The closest valid code is GET CURRENT q NO-WAIT (no lock type), and both FWD versions get it wrong:

Stale buffer, B locks the entry record Result
OpenEdge 4108, reads the entry record, no wait, LOCKED = yes
Trunk no 4108, keeps the stale record and locks it, LOCKED = no
Branch 4108, waits for B's lock, LOCKED = no

Cause: the conversion drops a NO-WAIT that has no lock keyword and emits plain current() (rules/convert/database_access.rules).

Finding 2: RecordBuffer.reloadCurrent

OpenEdge never takes EXCLUSIVE-LOCK outside a transaction. A static GET makes its block the transaction, and the GET methods fail with error 7359 ("QUERY GET methods with exclusive lock requires a transaction"). FWD has no 7359:

A holds SHARE, GET-CURRENT, no transaction B holds SHARE Record free
OpenEdge 7359, A keeps SHARE, LOCKED = no 7359, A keeps SHARE
Trunk waits for B, then takes EXCLUSIVE takes EXCLUSIVE
Branch A loses its SHARE, LOCKED = yes takes EXCLUSIVE

Inside a transaction, the branch matches OpenEdge, while trunk raises "Could not acquire exclusive lock".

Branch conversion change: NO-WAIT on GET-*/FIND-* methods

Trunk drops a literal NO-WAIT (or 6090) from these methods, so they wait for the lock. The branch keeps it in every form tested (literal, 6090, variables, query and buffer handles). On the branch, GET-CURRENT and every FIND-* method then match OpenEdge. GET navigation does not:

GET-FIRST/LAST/NEXT/PREV EXCLUSIVE-LOCK NO-WAIT, B locks the record Method Statement
OpenEdge yes, record read NO-LOCK, LOCKED = yes same, no error
Trunk waits for the lock (NO-WAIT dropped) ERROR "Could not acquire exclusive lock", block undone
Branch no, "Could not acquire exclusive lock", LOCKED = no same as trunk

The statement shows the runtime gap is already in trunk; the branch only exposes it for the methods.

What needs to change

  • GET methods: raise 7359 when EXCLUSIVE-LOCK is requested without an active transaction.
  • GET navigation with NO-WAIT (statement and method): when another session holds the lock, read the record with NO-LOCK and set LOCKED instead of raising an error.
  • GET CURRENT q NO-WAIT: keep the NO-WAIT in conversion, and apply it to each component's own lock at runtime (in CompoundQuery, derive the lock types from the components, as PreselectQuery.reloadEntry does).
  • Optional: raise 247 at conversion time for NO-WAIT in an OPEN QUERY, FOR or PRESELECT record phrase.

Not checked: a zero NO-WAIT argument (both versions remove it, so the call waits).

#25 Updated by Alexandru Lungu 12 days ago

I reaches the same conclusions as #10026 so I can confirm them they are not relevant now. Please proceed with testing.

#26 Updated by Radu Apetrii 7 days ago

The two customer applications that I tested are both fine with 10026a. Nothing suspicious noted. ✅

#27 Updated by Alexandru Lungu 7 days ago

Branch regression: 10026a

Two tests in the lock battery pass at the fork (trunk r16766) and fail on the task branch (r16772), and both failures reproduced on a rerun. Apart from those two, the branch fixes 94 tests and regresses nothing else.

Regressions (2), both in tests/concurrency/get_current_lock

  • TestGetCurrentLockWriter.cls#GL08StaleJoinExclNoWaitNoErrorEscapes: OE pass, baseline pass, task fail. AssertionFailedError at HarnessApi.sendMessage(HarnessApi.java:401): the writer timed out after 120 s waiting for the reader.
  • TestGetCurrentLockWriter.cls#GL10StaleJoinGetCurrentNoWaitNoLockPhrase: OE pass, baseline pass, task fail. Same timeout.

What changed is the reader. At the fork, the GL08 and GL10 readers fail at once on a wrong value, and the writer finishes normally. On r16772 the reader blocks, apparently on a record lock: GL08 waits about 240 s, GL10 about 120 s and then fails its assertion. While the reader is blocked, the writer gives up waiting. So the branch turns a fast wrong answer into a hang in a NO-WAIT stale-join scenario. The battery's own notes predicted the GL10 hang on r16772, and the ticket says the branch is now at r16812; the newer revisions may have fixed this.

Conclusion

Considering that the 2 tests are OK on trunk, but failing with this branch, considering them regressions, we might expect seeing such exploit in customer application after merge. On the other hand, if these 2 tests were passing by chance and they are actually meant to be failing in trunk due to a lack of functionality, then I am ok porting them to a b branch. Stefanel, please investigate these 2 tests with urgent priority.

Fixed (94)

  • get-current: 34 tests, across 18 classes. Examples: the 11882 dynamic EACH/FIRST CURRENT after OPEN, 7 tests in the error-mode surroundings, compound CURRENT right after OPEN, and presort CURRENT between rows.
  • get-error-modes: 34 tests, in the CURRENT error-mode classes (empty query or join, off-end join, past end).
  • query-db: 16 tests, mostly get_current_join.
  • lock battery: 3 tests (GL04 and GL05 reader, GL05 writer).
  • Lyra browse suite: 2 tests.
  • Smaller suites: 2 in current-changed, 2 in off-end-state, 1 in reposition-past-last.

Notices

  • More to fix: 279 tests pass on OE but fail on both builds.
    • The ones that point at the branch's own bug: 38 in get-current, 10 in the lock battery, 13 in the Lyra browse suite and 10 in get-error-modes. Many get-current ones are from the later review notes (#10026-16/-20: reopen after off-end, several stale buffers under CATCH or THROW), so r16812 may cover them.
    • In the browse suite, FWD returns join rows reversed, for example (3,2) where OE has (2,3).
    • The rest (off-end-state, reposition-past-last, query-db) look like existing gaps, unchanged by the branch.
  • OE red: 105 tests fail on OE. 86 are in query-db, which was kept by choice: 56 from 10068 in reposition_to_rowid_outside_where, plus Assert:Equals overloads (14457). 13 are in progressive-results and 6 in current-changed (a table from the tstmultiple database, plus 14457).

Things that affect how to read this

  • Version: r16772 was tested, as on disk. The branch is now at r16812.
  • Lock battery: run one scenario per harness run (GL01 premise plus the scenario) on both FWD sides. A full run desynchronises after the first scenario that hangs (GL02), and every later scenario fails without running. On OE the full battery ran cleanly, 20/20.
  • Lyra: the 2 real-input scenarios use the OE verdict recorded in MAPPING.md on 2026-10-22; they were not rerun on OE. The 17 class ports were rerun on OE (prowin) and all pass.
  • Conversion failures: 25 files failed to convert and 13 had javac errors, the same on both sides (no rules/ change). They show up as err/not run in off-end-state and query-db.
  • Probe file: Probe10026Join.cls is a scratch probe by its own header and was left out of the frozen test copy.

Comparison

Suite Type Ring Tests OE pass/fail/err Baseline pass/fail/err Task pass/fail/err Regressed Fixed Still failing
get-current ablunit target 99 99 / 0 / 0 27 / 57 / 15 61 / 38 / 0 0 34 38
get-current-lock harness target 20 20 / 0 / 0 7 / 13 / 0 8 / 12 / 0 2 3 10
get-current-browse interactive target 19 19 / 0 / 0 4 / 15 / 0 6 / 13 / 0 0 2 13
get-error-modes ablunit neighbour 129 129 / 0 / 0 85 / 44 / 0 119 / 10 / 0 0 34 10
off-end-state ablunit neighbour 445 445 / 0 / 0 387 / 26 / 32 389 / 24 / 32 0 2 56
invalidation-off-end ablunit neighbour 18 18 / 0 / 0 18 / 0 / 0 18 / 0 / 0 0 0 0
progressive-results ablunit neighbour 579 566 / 0 / 13 517 / 52 / 10 517 / 52 / 10 0 0 49
query-db ablunit neighbour 240 154 / 0 / 86 167 / 45 / 28 183 / 42 / 15 0 16 40
reposition-past-last ablunit neighbour 106 106 / 0 / 0 75 / 30 / 1 76 / 29 / 1 0 1 30
ir-join-jump ablunit neighbour 116 116 / 0 / 0 83 / 27 / 6 83 / 27 / 6 0 0 33
current-changed ablunit neighbour 29 23 / 0 / 6 27 / 2 / 0 29 / 0 / 0 0 2 0
Total 1800 1695 / 0 / 105 1397 / 311 / 92 1489 / 247 / 64 2 94 279

err = runtime error, not run, conversion failure or skipped.

#28 Updated by Alexandru Lungu 7 days ago

PS: I am committing the tests just now.

#29 Updated by Alexandru Lungu 7 days ago

Done in rev. 1958.

#30 Updated by Stefanel Pezamosca 7 days ago

I'll check these tests out. I guess it it could be related to #10026-24 findings. There are two options here: Either I revert the NO-WAIT patches I made in 10026a to how trunk handled it or I should make a complete fix for the CURRENT NO-WAIT support.

#31 Updated by Stefanel Pezamosca 7 days ago

Sorry, my bad, I think I forgot to push --overwrite after I rebased the branch and applied other fixes.

Status of the two lock-battery regressions from #10026-27. That run tested r16772; 10026a is now at r16812.

I reproduced GL08 and GL10 with a pair of two-session probes, on trunk r16812 and on 10026a r16812. In every case the other session holds the outer record of the query entry with EXCLUSIVE-LOCK. I did not rerun the harness battery itself.

Case Trunk 10026a
GL08: q:GET-CURRENT(EXCLUSIVE-LOCK, NO-WAIT) on a stale join does not wait, but keeps the stale record and does not set LOCKED does not wait, loads the entry record and sets LOCKED, as in OE
GL10: GET CURRENT q NO-WAIT on a stale join does not wait, but keeps the stale record waits until the lock is released
GL10, with a buffer that is not stale waits until the lock is released waits until the lock is released

GL08 is fixed in r16812. In r16772, conversion dropped the NO-WAIT from GET-CURRENT(), so the reload waited. r16812 keeps it.

GL10 is not a new bug in 10026a. Conversion (in trunk as well) drops NO-WAIT when no lock keyword is given, so GET CURRENT q NO-WAIT becomes a plain current() and waits for the lock. Trunk returned at once in this case only because it did not reload the entry and kept the wrong record. When trunk does reload (last row), it waits too.

A full fix needs NO-WAIT kept in conversion and applied to each query component's lock at runtime. That touches the query API and four query classes, which is too much to change safely before the merge. I propose a follow-up task for GET CURRENT q NO-WAIT without a lock keyword. No change is needed in 10026a.

#32 Updated by Alexandru Lungu 7 days ago

Got it; thank you for investigation. I have no other objections.
Please let me if you are ready to merge 10026a.

#33 Updated by Stefanel Pezamosca 7 days ago

Alexandru Lungu wrote:

Got it; thank you for investigation. I have no other objections.
Please let me if you are ready to merge 10026a.

I finished testing on my side and also received confirmation from Serban and Artur that their tests were ok. I think 10026a is ready to be merged. Then, I guess I should create a followup task to fix the NO-WAIT support.

#34 Updated by Alexandru Lungu 7 days ago

  • Status changed from Internal Test to Merge Pending

Please merge 10026a to trunk now.

Then, I guess I should create a followup task to fix the NO-WAIT support.

Yes, please do.

#35 Updated by Stefanel Pezamosca 7 days ago

  • Status changed from Merge Pending to Test

Branch 10026a was merged into trunk as rev. 16818 and archived.

#36 Updated by Stefanel Pezamosca 6 days ago

  • Related to Bug #12094: NO-WAIT support for GET CURRENT and the GET methods (lock conflicts) added

Also available in: Atom PDF