Bug #10026
A multi-table AdaptiveQuery running off-end is still updating the cursor
100%
Related issues
History
#1 Updated by Alexandru Lungu over 1 year ago
- Assignee set to Stefanel Pezamosca
- File 10026.patch
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 callcursor.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.
- 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
loadByValuenow). - 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 fromCompoundQueryshould 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 callloadByValuesand runloadon 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 togetRowfrom CompoundQuery will end up inAQ.getRowthat 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: See 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: Scrolling & Non-Scrolling Query testcase
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.
- 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.
CompoundQuerycomponent loading was refactored (r16767), andAdaptiveQueryno longer adds a cursor row when nothing was found.- Regression fix in
FQLPreprocessor: the "or is null" rewrite of? < fieldand? <= fieldkeeps the operand order.
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/queryandtests/persistence: no regressions (2365 passed, 412 failed; the same failures as before the change).
- 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-ONLYwas assigned FALSE, and no 4114 after a 4108. - A non-scrolling
repositionno 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
cacheFirstis no longer wrapped in theelseblock and is run unconditionally. All other places that compress thesuper.firstlogic ported tocacheFirstare not withinelseblocks. - Make
noQueryRecordreturn false afterErrorManagerwork. The users should returnnoQueryRecordthis will allow us to easier short-circuit work afternoQueryRecordby simply runningreturn noQueryRecord. Also, the method naming should be a verb, for examplethrowNoRecordAvailableorfailWithNoRecordAvailableor 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, callingreloadResultListEntryis wrong rather then expecting0. So I prefer to know if we have non-scrolling queries that callreloadResultListEntryinstead 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).
staleBuffersI 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
lockTypeof thereloadResultListEntrygoverns the locking system instead of the "per-query-components" locking. Also check iflockTypeis missing, the query component locking is honored instead. - I don't think the changes in
FQLPreprocessorare 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 forisBetweenRows. 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_NONEandCURRENT_LOADEDseem reasonable for an enum. This also forcesbeforeCurrentto return a well scoped enum value (e.g. not -1).- Why is
forwardOnlyAssignedneeded; isn'tforwardOnlyenough? AFAIK, a query wrapper can be neither scrolling nor forwardOnly. It can beforwardOnlyonly if assigned. Correct me if I am wrong. beforeCurrentshould 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 begetDispositionForCurrentwhich returns the enum in question.currentRecordstocanGetCurrentRecords.empty = (((BufferReference) buffers.get(i)).buffer().getCurrentRecord() == null);this is the available check. UseavailableAPI.emptyshould be in factallBuffersLoadedwhich seems quite opposite toempty. 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
- 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
#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-12cacheFirstoutsideelse: the behaviour is unchanged. Theifbranch returnsnext(lockType), so the code after it only runs on the formerelsepath.noQueryRecord: agreed. I will rename it tofailNoQueryRecord()and make it returnfalse, so callers canreturn failNoQueryRecord();.{@code true}instead of<code>: agreed, for the new and changed javadoc.- Guards in
reloadResultListEntry:!isScrolling()and_isOffEnd()are legal states.beforeCurrentcalls 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). InAdaptiveQuery,cursor.isBetweenRows()is already handled bybeforeCurrentbefore the call.cursor nullandentry nullare 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 toP2JQuery.reposition(int), which callsthis.reposition(row, false)and lands back inAdaptiveQuery.reposition(int, boolean). That path is reachable afterFORWARD-ONLY = FALSEand ends in aStackOverflowError. The fix also passesinternalinstead 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 intomatchesEntry(..)andstaleBuffers(..)(0 = none stale).lockType: verified. An explicit lock overrides every component (PreselectQuery.coreFetch,RandomAccessQuery.load, andCompoundQuery.loadQuerypasses it through). Withnull, each component uses its own lock, the same as the delegate'scurrent().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 inPreselectQuery(covers AQ and presort),CompoundQuery(covers presort compound),RandomAccessQueryandQueryWrapper(including its null query). The missingPreselectQuery.isBetweenRowsbelow is exactly this gap. CURRENT_*enum, and renamingbeforeCurrenttogetDispositionForCurrent: agreed.forwardOnlyAssigned: it is needed. In OE, a static query defined without SCROLLING reports 3164 on REPOSITION, but the same query repositions fine onceFORWARD-ONLY = FALSEis assigned. Both states haveforwardOnly == false. The alternative is a tri-stateBoolean, if you prefer it.currentRecords: it performs the GET-CURRENT rather than checking whether it can, so I proposegetCurrentRecordsinstead ofcanGetCurrentRecords.empty: it means "no buffer holds a record"; the loop stops at the first available buffer. That is the opposite ofallBuffersLoaded. I will invert it intoanyAvailableusingisAvailable(). An OUTER-JOIN buffer inunknownModehas no record, so it counts as not available, matching 4GLAVAILABLE. The remaining OUTER-JOIN cases go to #11576.
PreselectQuery.isBetweenRows: valid. I will override it asisScrolling() && 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 inQueryWrapper, outside the delegate's try/catch that turns errors into FALSE. I will apply the same error handling to the reload. I will also testGET-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.
beforeCurrentandstaleBuffersjavadoc: 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
reloadResultListEntryandisBetweenRowsare 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 = FALSEmakes 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 |
(?,?) |
- 4: with 10026a,
FORWARD-ONLY = FALSEmakes a query scrolling, as it does in OE. Because of #4,TestForwardOnlySet3#testnoscrolling36now 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 callsbuffer.reloadCurrent(lockType, isErrorIfNull()), which turns a NO-WAIT lock conflict into a NO-LOCK read with LOCKED set and no error.FindQueryoverridescurrent()/current(LockType)only to callinitializeQuery()and thensuper.current(...), anddatabase_access.rulesmaps FIND CURRENT tocurrent, or tosilentCurrentunder NO-ERROR. Trigger:FIND CURRENT customer EXCLUSIVE-LOCK NO-WAIT [NO-ERROR]while another session holds the record. Before,RecordBuffer.loadcaughtLockUnavailableExceptionand calledrecordOrThrowError, so ERROR was raised (orERROR-STATUS:ERRORwas set under NO-ERROR). NowlockForCurrentswallows the exception and the statement completes silently with a NO-LOCK copy; outside a transaction any SHARE lock already held is given up. The dynamicBUFFER:FIND-CURRENT()still goes throughBufferImpl._current→recBuf.reload, so static and dynamic FIND CURRENT now behave differently. This also contradicts thereloadCurrentjavadoc ("Unlike FIND CURRENT ..."). Restrict the NO-LOCK fallback to the query GET CURRENT path, for example by havingFindQuery.currentkeep the oldarmCurrentChanged/reload/updateCurrentChangedsequence.\
- [MINOR] functional
CompoundQuery.reloadResultListEntry: WhenlockType == null(plainGET CURRENT q/GET-CURRENT()),lockTypesstaysnull, soloadEntryForCurrentnever runs a component's own NO-WAIT lock throughlockForCurrent. The component load falls back to its own lock type, andRandomAccessQuery.loadByValuesends theLockUnavailableExceptiontorecordOrThrowError.CompoundQuery.loadQueryrethrows the resultingConditionException, andloadEntryForCurrentdoes not catch it. Trigger: scrollingOPEN QUERY q FOR EACH a EXCLUSIVE-LOCK NO-WAIT, EACH boutside a transaction, GET NEXT, a FIND onathat makes the buffer stale, another session locking the entry'sarecord, then GET CURRENT q. ERROR is raised (with NO-ERROR the buffer is left empty and LOCKED is not set).PreselectQuery.reloadEntryandRecordBuffer.reloadCurrentread the record NO-LOCK and set LOCKED in the same situation. BuildlockTypesfrom each component query's lock type whenlockTypeis 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.openresets only its own fields, and the components'cleanup()leaves off-end alone (RecordBuffer.offEndforRandomAccessQuery, theoffEndfield forAdaptiveQuery/PreselectQuery). Components are reset lazily inprocessComponent, 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 withQUERY-OPEN().QueryWrapper.queryOpencallsclose()/open()on the same delegate and components. The firstGET-NEXT()then returns FALSE and releases the outer buffer instead of restarting at the first row.previoushas the same problem withOffEnd.FRONT. Also require that a retrieval happened since the open (e.g.counters != null/activeIndex >= 0), or reset the component off-end state inopen().
This seems reasonable.
- [MINOR] functional
QueryWrapper.getCurrentRecords:getDispositionForCurrent(viareloadResultListEntryandloadEntryForCurrent) now runs beforedelegate.getCurrent, outsideAbstractQuery.wrapCurrent's try/catch, which mapsErrorConditionExceptionto FALSE for the GET-CURRENT method. The same applies togetCurrent()and its overloads. Trigger:q:GET-CURRENT()on a scrollingCompoundQuerydefined withEXCLUSIVE-LOCK NO-WAIT, with a stale buffer and the entry record locked by another session (the path in the previous finding). The same happens withq:GET-CURRENT(EXCLUSIVE-LOCK)when a-lkwtmolock timeout fires inloadByValue. ERROR escapes the method instead of FALSE being returned. Apply thewrapCurrenthandling around the disposition step in the method variants: rethrowDeferredLegacyErrorExceptionand mapErrorConditionExceptionto FALSE. - [MINOR] functional
QueryWrapper.getDispositionForCurrent: Each stale-buffer 4108 goes through its ownErrorManager.recordOrShowWarningcall inside the loop. Under CATCH or BLOCK-/ROUTINE-LEVEL ON ERROR UNDO, THROW,recordOrShowErrorupgrades the warning and throwsDeferredLegacyErrorExceptionon 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 givesNumMessages= 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
#18 Updated by Stefanel Pezamosca 19 days ago
#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:
- [MAJOR]
FIND CURRENT ... NO-WAITreads NO-LOCK silently: fixed.FindQuery.refetchCurrentkeeps the old reload path andRecordBuffer.loadreports 445, so static and dynamic FIND CURRENT raise the lock conflict again, as in OE. - [MINOR]
CompoundQuery.reloadResultListEntryignores 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 dynamicQUERY-PREPARE), so the trigger is not valid 4GL. FWD'sQUERY-PREPAREreports the same 247. - [MAJOR] reopened non-scrolling
CompoundQuerystays off-end: not reproducible, no change needed. Component 0 keeps its off-end state throughclose(), as you say, but neither reopen path reuses it:QueryWrapper.queryOpen()runsnotifyQueryOpenListeners(true)betweenclose()andopen(), andDynamicQueryHelper's listener re-executes the prepared query, which resets the components; a staticOPEN QUERYbuilds a newCompoundQueryand new components on each open. Traced with a non-optimized join (CONTAINSin the predicate keeps FWD from joining server-side): dynamicQUERY-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. ARandomAccessQuerycomponent 0 cannot occur: OE rejects a FIRST outer table in a query with 3322. - [MINOR]
GET-CURRENT()raises ERROR from the entry reload: fixed.QueryWrapper.getCurrentRecordsmaps that ERROR to FALSE. - [MINOR] under CATCH only one 4108 is raised: fixed.
ErrorManager.recordOrShowWarningsraises 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 (inCompoundQuery, derive the lock types from the components, asPreselectQuery.reloadEntrydoes).- Optional: raise 247 at conversion time for NO-WAIT in an
OPEN QUERY,FORorPRESELECTrecord 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.AssertionFailedErroratHarnessApi.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, plusAssert:Equalsoverloads (14457). 13 are in progressive-results and 6 in current-changed (a table from thetstmultipledatabase, 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.mdon 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.clsis 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