Bug #10026
A multi-table AdaptiveQuery running off-end is still updating the cursor
90%
History
#1 Updated by Alexandru Lungu over 1 year ago
- File 10026.patch
added - Assignee set to Stefanel Pezamosca
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 about 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 16 days 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 8 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 8 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 5 days ago
- % Done changed from 100 to 90
- Status changed from Review to WIP
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 5 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 about 12 hours 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.