Project

General

Profile

Bug #9914

Investigate why ListenerSet implementation is not properly working

Added by Alexandru Lungu over 1 year ago. Updated about 1 year ago.

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

100%

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

start.p Magnifier (1.69 KB) Razvan-Nicolae Chichirau, 04/22/2025 05:36 AM

History

#1 Updated by Alexandru Lungu over 1 year ago

  • Priority changed from Normal to High
  • Assignee set to Razvan-Nicolae Chichirau

According to Eric, ListenerSet from ChangeBroker should have worked like a linked list which references other deeper scopes and so it should be enough to iterate with it all scopes for interested listeners.

In #9722, the ListenerSet implementation is ignored and all scopes are visited, but this may represent a performance drawback because all scopes are now iterated. I the ListenerSet implementation, some links are followed to iterate only the right scopes. This seems to be broken for #9914-15.

In trunk, there is a fix from #9722 that is functionally correct, but seems like a work-around now, considering that ListenerSet should have taken care of that already. This is a High priority!

#3 Updated by Razvan-Nicolae Chichirau over 1 year ago

So, what we need to do is to replace the workaround of just combining all listeners sets and instead use the ListenerSet's iterator?

#4 Updated by Alexandru Lungu over 1 year ago

Razvan-Nicolae Chichirau wrote:

So, what we need to do is to replace the workaround of just combining all listeners sets and instead use the ListenerSet's iterator?

Drop the workaround and find why the ListenerSet$Iter is not doing its job right.

#5 Updated by Razvan-Nicolae Chichirau over 1 year ago

Given this situation:

DEFINE VARIABLE q AS HANDLE NO-UNDO.
PROCEDURE openQuery:
...
CREATE QUERY q.
q:QUERY-OPEN().
...
END PROCEDURE.

ON CHOOSE OF btn DO:
    ghBuffer:BUFFER-DELETE(). 
    q:QUERY-CLOSE(). 
    q:QUERY-OPEN().
END.

// Main block
RUN openQuery.
wait-for ...

  1. The openQuery procedure is invoked. This will trigger the 2 listener sets containing TemporaryBuffer and PreselectQuery to be linked.
  2. Procedure is finished, TransactionManager@TransactionHelper.popScope() is called which leads to ChangeBroker.scopeFinished() that unlinks all listener sets returned by listeners.getDictionaryAtScope(0, false), including the set with PreselectQuery.
  3. On choosing the button, .deleteRecord() is invoked, leading to the for-each which uses ListenerSet$Iter, but the 2 listener sets are still unlinked, so only one gets picked up.
  4. Query open executes again, linking TemporaryBuffer with PreselectQuery, but now it's too late.

#6 Updated by Razvan-Nicolae Chichirau about 1 year ago

Attached the original testcase.

There are 2 important phases while opening the query in createWidgets:
  1. Initializing, which adds the PreselectQuery as a listener to the local scope and links it to the next outer scope, which in our case is the one holding the buffer.
  2. Opening, which adds the query as a listener in ChangeBroker.addListener() with depth = 1. This will make the following if statement to be skipped, thus ignoring the linking:
    if (set.nextOuter == null && depth > 1)
    {
       ListenerSet nextOuter = listeners.lookup(dmoIface, depth - 1);
       if (nextOuter != null)
       {
          set.link(nextOuter);
       }
    }

So, I don't think the problem comes from the unlinking of PreselectQuery after popping the procedure local scope, but rather why the listener set from the first scope is not linked with the rest. Maybe it's from the fact that the query should be considered global, but its depth is 1 instead of 0?

#7 Updated by Razvan-Nicolae Chichirau about 1 year ago

  • Status changed from New to WIP
  • % Done changed from 0 to 100
  • reviewer Alexandru Lungu added

Commited to 9914a/rev. 15868.

Alex: Please review.

#8 Updated by Razvan-Nicolae Chichirau about 1 year ago

  • Status changed from WIP to Review

#9 Updated by Alexandru Lungu about 1 year ago

So, I don't think the problem comes from the unlinking of PreselectQuery after popping the procedure local scope, but rather why the listener set from the first scope is not linked with the rest.

I agree with this statement.

Committed to 9914a/rev. 15868.

The same code is inside CompoundQuery. This approach should be also applied there. However, I don't think the increasing of scope is the right thing to do here. It looks like you are registering the query at the first nested scope and the buffer at the second nested scope. I suppose the query won't be global anymore.

For the original problem, I tend to think that the global scope (-1) is populated before the local scope (0) in your case.

Note how ListenerSet nextInner = listeners.reverseLookup(dmoIface, 1); is linking the global scope with the first nested scope. However, ListenerSet nextOuter = listeners.lookup(dmoIface, depth - 1); is not ever linking the global scope with the nested scope. There is an invariant here that the listeners are always registered in increasing order of the scope and if the global scope is ever registered, it will be linked with the first nested scope. I think the bug you are seeing here is the invariant violation: a nested scoped set is created after the global scope.

Please confirm that this is the case. If so, please make the required changes to link the global scope if any to the fresh nested scope but only if there is no previous scope. An else to if (set.nextOuter == null && depth > 1) should look for listeners.getValueAtScope(dmoIface, -1); and if it exists, then link to it.

#10 Updated by Razvan-Nicolae Chichirau about 1 year ago

It looks like you are registering the query at the first nested scope and the buffer at the second nested scope.

The scope increasing was due to treating a 0-indexed value as 1-indexed. More exactly, in

int scope = BufferManager.get().findNearestExternal();
from PreselectQuery.open(), the value will be 1, whereas buffer.getScopeOpenDepth() will return 2, but they refer to the same scope. With my changes, both the query and the buffer will be registered at the same scope, which would be 2.

Alex: Is this difference in scope wanted? If it's indeed a bug, does #9914-9 still hold?

#11 Updated by Alexandru Lungu about 1 year ago

I can't tell in #9914-5 where the buffer is defined.

#12 Updated by Razvan-Nicolae Chichirau about 1 year ago

After converting start.p from #9914-6:

public void execute()
{
   externalProcedure(Start.this, new Block((Body) () -> 
   {
      fmainFrame.openScope();
      RecordBuffer.openScope(tt1);
      tt1.create();
      tt1.setF1(new character("a"));
      RUN_CALL_SITE_1.clone().run();
      apply(gwhBrowse, "entry");

      registerTrigger(new EventList("choose", fmainFrame.widgetBtnd()), Start.this, new Trigger((Body) () -> 
      {
         RUN_CALL_SITE_2.clone().run();
      }));

      fmainFrame.enable();

      LogicalTerminal.waitFor(null, new EventList("close", currentWindow()));
   }));
}

The place where the TemporaryBuffer is registered as a listener is RecordBuffer.openScope(tt1);, where tt1 is the default buffer for the temp-table, so I don't see any linking with the actual buffer variable. Anyway, it would be defined globally.

#13 Updated by Alexandru Lungu about 1 year ago

Anyway, it would be defined globally.

I am not sure that "globally" is right. It is within the scope of the external procedure, so I would expect to be a non-global scope. Because you run start.p as entry-point, it might get confusing. But technically, if you run a separate procedure from which you do RUN start.p, then tt1 buffer scope is even more nested. The idea is that after RUN finishes, the scope closes and the buffer scope is closed. Finally, this means that the scope is not global.

By "global" I would rather refer to buffers that are CREATEd and which should be deleted explicitly. These buffers persist beyond the scope of the procedure, so beyond RUN. The same applies to other resources like the QUERY.

If we are to consider indexing like 0 - global scope and 1, 2, 3 ... the nested scopes where 1 is the outer-most scope, then:
  • the buffer should be at scope 1.
  • the query should be at scope 0.
Please also do an experiment with RUN start.p. In that case:
  • the buffer should be at scope 2.
  • the query should be at scope 0.

#14 Updated by Razvan-Nicolae Chichirau about 1 year ago

Experimented with RUN custom.p from a start.p. Right before we should to add the query as a global listener, this is the state:
  • Scope 0 (global): null dictionary
  • Scope 1 (custom.p outer scope): null dictionary
  • Scope 2 (start.p outer scope): TemporaryBuffer as a listener
  • Scope 3 (start.p createWidgets procedure): local PreselectQuery as a listener

No matter how many nested RUN statements we have in this testcase, we always end up trying to register the query at the buffer's scope - 1, which is not equal to the global state (0). Why would we change the addListener implementation if we try to register the query at the wrong scope a.k.a. not the global scope?

What I'm trying to say is that the problem could be from the scope processing, which does not return 0 in this case.

Alex: Can a valid solution be the assigning of a 4GL dynamic query as a global scope listener, according to #9914-13? Note that static and dynamic query scopes are calculated in the same manner (min between the nearest external block and the outer-most record buffer of the query).

#15 Updated by Alexandru Lungu about 1 year ago

No matter how many nested RUN statements we have in this testcase, we always end up trying to register the query at the buffer's scope - 1, which is not equal to the global state (0). Why would we change the addListener implementation if we try to register the query at the wrong scope a.k.a. not the global scope?

I think this is correct for static queries. But I can agree that the query should be registered at scope 0 if dynamic.
Fun fact: a query that had a dynamic buffer (aka with its scope 0) will be automatically registered globally (due to the min operation).

Alex: Can a valid solution be the assigning of a 4GL dynamic query as a global scope listener, according to #9914-13? Note that static and dynamic query scopes are calculated in the same manner (min between the nearest external block and the outer-most record buffer of the query).

Well, if the query is static, the scoping from #9914-14 would be correct! But the issue will still reproduce (?).

I personally see two orthogonal issues, but most important is why some listeners are ignored. We can struggle to find examples with static or dynamic resources that will yield a problematic scoping where the listener set will still fail.
My bottom line is: if we change the scoping of the dynamic query to 0 (which will be correct), I guess we will only hide the issue. Other code can still encounter a malfunction of the listener sets: using static/dynamic resource / persistent procedures / etc. Am I misunderstanding?

#16 Updated by Razvan-Nicolae Chichirau about 1 year ago

Well, if the query is static, the scoping from #9914-14 would be correct! But the issue will still reproduce (?).

You're right. In the static case, the listener sets will not be linked. Commited to rev. 15869.

Alex: Please assess and review.

#17 Updated by Alexandru Lungu about 1 year ago

  • Status changed from Review to Internal Test
I am OK with the changes in 9914a.
However, we will need some extensive testing with it, eventually across multiple applications and unit tests. Please follow:
  • run smoke tests and include #9722 for the application you are handling
  • run ChUI regression testing
  • ask Radu to find somebody that can run ETF
If these work:
  • I will run unit tests and smoke tests of large customer application.
  • ask Danut to run unit tests and fwdtests for the application he is managing
  • ask Lorian to run harness

#18 Updated by Lorian Sandu about 1 year ago

ETF testing passed!

#19 Updated by Razvan-Nicolae Chichirau about 1 year ago

  • ChUI regression tests passed ✅
  • Smoke-test for a large customer app passed ✅
  • #9722 testing passed ✅

I think we can move on to the second testing phase.

#20 Updated by Alexandru Lungu about 1 year ago

I will run unit tests and smoke tests of large customer application.

Everything is OK here.

#21 Updated by Lorian Sandu about 1 year ago

  • ask Lorian to run harness

Harness passed ✅

#22 Updated by Dănuț Filimon about 1 year ago

  • ask Danut to run unit tests and fwdtests for the application he is managing

No issues found ✅

#23 Updated by Radu Apetrii about 1 year ago

Wonderful! I shall queue the branch up for merging.

#24 Updated by Radu Apetrii about 1 year ago

  • Status changed from Internal Test to Merge Pending

9914a can be merged after 5471a.

#25 Updated by Razvan-Nicolae Chichirau about 1 year ago

  • Status changed from Merge Pending to Test

Branch 9914a was merged into trunk as rev. 15893 and archived.

Also available in: Atom PDF