Unexpected lifetime of dataset member (REFERENCE-ONLY)

Posted by dbeavon on 19-Oct-2018 12:47

I have a dataset that is a member of a logic class, and the dataset is listed as REFERENCE-ONLY.  The intention is that, for performance sake and for memory conservation, I want the class rely on an instance that is created elsewhere and is given to the class for reference.

Here is the dataset.  It uses compilation parameters:


1 for REFERENCE-ONLY
2 for PRIVATE
3 for INSTANCE QUALIFIER

DEFINE {2} TEMP-TABLE TT{3}_BomHeader NO-UNDO {1}

   FIELD OurRecId AS CHARACTER
 
   FIELD BomCode AS CHARACTER   .
         
DEFINE {2} DATASET DS{3}_AsmBom {1} FOR 
   TT{3}_BomHeader .

It is used in my BomLogic class like so.  Note that this makes the dataset into a reference-only member.

   {app/Production/Maintenance/Bom/Assembly/AsmBomData.i REFERENCE-ONLY}
   

The goal is that once the dataset reference is provided to the class, then the class will be able to continue maintaining that data indefinitely.  However, this is not always working as expected.  In some scenarios the dataset reference will become invalidated and will no longer be available anymore even though it had been previously used.

The error I get is : Attempt to reference uninitialized temp-table. (12378)

I'm not fully understanding why the AVM would pull my dataset reference away from this class after it had been previously available.

All Replies

Posted by jquerijero on 19-Oct-2018 12:59

I think this is an expected behavior if the object or procedure that holds the true copy goes away.

Posted by dbeavon on 19-Oct-2018 13:07

I don't think my question is clear so I will give a more detailed repro.  Lets say I have a business logic class "BomLogic" and a testing harness that interacts with it "TestClient".  If BomLogic independently BIND's its own reference to a dataset into the reference-only member, then that dataset will be available to use indefinitely.  But if TestClient sends in the instance of the data as a parameter, then the dataset will be pulled away and won't be available for use when BomLogic does subsequent work. 

Here is BomLogic.  Note that InitDataMethod1 will bind the dataset reference independently.  But TestMethod1 will allow the dataset reference to be provided as a parameter:

USING Progress.Lang.*.

BLOCK-LEVEL ON ERROR UNDO, THROW.

CLASS app.Production.Maintenance.Bom.Assembly.Testing.BomLogic: 


   
   {app/Production/Maintenance/Bom/Assembly/AsmBomData.i REFERENCE-ONLY}
   
   
   /* ********************************************************************* */
   /* Init the reference only data from BIND                                */
   /* ********************************************************************* */
   METHOD PUBLIC VOID InitDataMethod1():

      DEFINE VARIABLE v_Init AS app.Production.Maintenance.Bom.Assembly.ReadOnlyHelpers.DataUtil.DataInitializerForBom NO-UNDO.
      v_Init = app.Production.Maintenance.Bom.Assembly.ReadOnlyHelpers.DataUtil.DataInitializerForBom:NewInitializerInstance().
      v_Init:BindNewBomData(OUTPUT DATASET DS_AsmBom BIND).  
      
      CREATE TT_BomHeader.
      TT_BomHeader.BomCode = "A".
      TT_BomHeader.OurRecId = "B".
      
   END METHOD.            
             
   /* ********************************************************************* */
   /* Init the reference only data from input                               */
   /* ********************************************************************* */
   METHOD PUBLIC VOID TestMethod1(INPUT DATASET DS_AsmBom):
      
      FOR EACH TT_BomHeader NO-LOCK:
         DISPLAY TT_BomHeader.BomCode.
         END.
          
   END METHOD. 
      
      
   /* ********************************************************************* */
   /* Secondary method after data is available                              */
   /* ********************************************************************* */
   METHOD PUBLIC VOID TestMethod2():
   
    
      MESSAGE "HERE {&FILE-NAME} {&LINE-NUMBER} IN TEST METHOD 2".
      PAUSE. 
          
      FOR EACH TT_BomHeader NO-LOCK:
         DISPLAY TT_BomHeader.BomCode.
         END.
          
      MESSAGE "HERE {&FILE-NAME} {&LINE-NUMBER} DONE!".
      PAUSE. 
            
   END METHOD.
      
END CLASS.

Given that BomLogic class, here is the test harness that uses it:

 
USING Progress.Lang.*.
USING app.Production.Maintenance.Bom.Assembly.Testing.*.


BLOCK-LEVEL ON ERROR UNDO, THROW.

CLASS app.Production.Maintenance.Bom.Assembly.Testing.TestClient: 
   
   
   /* References datasets */
   {app/Production/Maintenance/Bom/Assembly/AsmBomData.i REFERENCE-ONLY}
   
    
      
   /* ********************************************************************* */
   /* Creates dataset and sends it to BomLogic class, TestMethod2 fails.    */
   /* ********************************************************************* */
   METHOD PUBLIC VOID ClientTestMethod1():
      
      DEFINE VARIABLE v_Init AS app.Production.Maintenance.Bom.Assembly.ReadOnlyHelpers.DataUtil.DataInitializerForBom NO-UNDO.
      v_Init = app.Production.Maintenance.Bom.Assembly.ReadOnlyHelpers.DataUtil.DataInitializerForBom:NewInitializerInstance().
      v_Init:BindNewBomData(OUTPUT DATASET DS_AsmBom BIND).  
         
      
      CREATE TT_BomHeader.
      TT_BomHeader.BomCode = "A".
      TT_BomHeader.OurRecId = "B".
      FOR EACH TT_BomHeader NO-LOCK:
         DISPLAY TT_BomHeader.BomCode.
      END.
      
      DEFINE VARIABLE v_RefObj AS BomLogic NO-UNDO.
      v_RefObj = NEW BomLogic().
      v_RefObj:TestMethod1(INPUT DATASET DS_AsmBom BY-REFERENCE ).
      v_RefObj:TestMethod2().
         
             
   END METHOD.
         
      
      
         
         
   /* ********************************************************************* */
   /* Allows BomLogic to create its own data, TestMethod2 succeeds          */
   /* ********************************************************************* */
   METHOD PUBLIC VOID ClientTestMethod2():
      
      DEFINE VARIABLE v_RefObj AS BomLogic NO-UNDO.
      v_RefObj = NEW BomLogic().
      v_RefObj:InitDataMethod1().
      v_RefObj:TestMethod2().
      
   END METHOD.

         
END CLASS.

Finally for reference purposes, here is a simple utility class where the dataset is actually instantiated.  All this does is provide a way to create the instance of the dataset.  That instance can be used by any other class which only has a "BY-REFERENCE" member.

USING Progress.Lang.*.
USING app.Production.Maintenance.Bom.Assembly.ReadOnlyHelpers.DataUtil.*.

BLOCK-LEVEL ON ERROR UNDO, THROW.

CLASS app.Production.Maintenance.Bom.Assembly.ReadOnlyHelpers.DataUtil.DataInitializerForBom : 
   
   /* Data has actual instance.  NOT only a reference */
   {app/Production/Maintenance/Bom/Assembly/AsmBomData.i}
  
	CONSTRUCTOR PUBLIC DataInitializerForBom (  ):
		SUPER ().
	END CONSTRUCTOR.


   METHOD PUBLIC STATIC DataInitializerForBom NewInitializerInstance():
      RETURN NEW DataInitializerForBom().
   END METHOD. 

   
   METHOD PUBLIC VOID BindNewBomData(OUTPUT DATASET FOR DS_AsmBom BIND) :
      DATASET DS_AsmBom:EMPTY-DATASET ().
   END METHOD.
             
  
   
END CLASS.

So given the BomLogic and the TestClient, why does running the TestClient:ClientTestMethod1() fail with "attempt to reference uninitialized temp-table"?  But TestClient:ClientTestMethod2() will succeed and will not pull away the reference to the dataset that the BomLogic relies on.

This is behavior that is unpredictable and hard to understand.  I thought I knew how to work with REFERENCE-ONLY datasets.  I thought they worked like memory references (pointers to an instance of a DATASET that had been instantiated elsewhere).  What I didn't expect is that the memory reference would be taken away from me after it had been previously available for use.

Any help would be appreciated.  I'd really like a "sticky" reference that wouldn't be removed once it had been assigned  (even if the reference comes in as an input parameter).

Posted by dbeavon on 19-Oct-2018 13:20

jquerijero: In my case both classes are still active. Neither has gone out of scope. The dataset instance still exists, but is just not available to the BomLogic class because it had been pulled away for some strange reason.

While I understand that (in many other languages) the nature of a parameter is something that is short-lived and goes out of scope when the method exits, this is not quite the same. In my example the dataset is first and foremost a member on the class and should be treated as such. (The fact that the dataset reference is assigned via an input parameter is just water under the bridge.)

Posted by dbeavon on 19-Oct-2018 13:53

Wow, I was able to get ClientTestMethod1 to work by changing BomLogic TestMethod1 as follows below (note the BIND qualifier that is now in the input parameter):

   /* ********************************************************************* */
   /* Init the reference only data from input                               */
   /* ********************************************************************* */
   METHOD PUBLIC VOID TestMethod1(INPUT DATASET DS_AsmBom BIND):
       
      FOR EACH TT_BomHeader NO-LOCK:
         DISPLAY TT_BomHeader.BomCode.
         END.
           
   END METHOD. 
       

I also had to change the caller so that it specified "BIND" instead of using the "BY-REFERENCE" qualifier on the INPUT dataset.

The original example should have behaved the exact same way as this.  Not sure why the change made a difference in how "sticky" of a dataset I would send to BomLogic.  As a matter of course I always try to avoid "BIND" since the scenarios where it is required are fairly unusual.  I was hoping this would not be one of them, but it appears to be.

The BY-REFERENCE and BIND keywords both pass along a dataset reference to the class via TestMethod1.  Given the similarities between the two, are there any diagnostic features in OE to generate output that distinguishes the fact that one sends a "sticky" reference and one sends a reference that is not sticky and will be pulled away again when the method ends?  This behavior is pretty obscure and would be nice if I could turn on some logging that explained how my data is being treated at the boundaries of my  method calls.

It would also be nice if there was a way within TestMethod1 to tell the AVM that I want the dataset to be "sticky" after-the-fact (even though it didn't get passed in with BIND).  It would be nice to understand this stuff a little better.  I often spend a lot of time scratching my head when working with reference-only data in OE.

Posted by David Abdala on 22-Oct-2018 05:30

Well.. that's exactly the point with BIND.

BINDing a Temp-table/DataSet to a REF-ONLY equivalent, means "permanently" relating both references, into the same instance.

BY-REFERENCE means a temporary relation among the references.

The distinction is not so obvious, and where you should use them is not that simple either.

Having a REF-ONLY definition, means you need to "bind" it to a real instance (with obviously the same structure) before using this 'reference' (definition). You can do this in a "permanent" way, or in a "temporary" fashion.

The "permanent", which lasts up to the next binding, requires the BIND keyword.

The "temporary", which lasts up to the scope of the procedure/method, uses BY-REFERENCE.

So in cases where you need a REF-ONLY 'bindage' to persist "through scopes" you need to BIND it. When you only need it *IN* the actual scope, a BY-REFERENCE is enough.

Sorry for so many "", but I lack the language skill to clarify it better.

The documentation is (was hopefully) a little bit obscure in making this distinction.

Hope this brings some light into the matter.

David.

Posted by frank.meulblok on 22-Oct-2018 05:51

I think the distinction between BIND and BY-REFERENCE is reasonably well documented here: documentation.progress.com/.../index.html

So what happens in the problem scenario is actually expected:

In the ClientTestMethod1() in the test harnass, the initDataMethod1() doesn't get called, so the v_Init:BindNewBomData(OUTPUT DATASET DS_AsmBom BIND) never happens and the reference in your BomLogic stays unbound.

The BY-REFERENCE pass to TestMethod1()  will set up a temporary binding (which allows that call to complete) and will then unbind.

The next call to  TestMethod2() then fails, because that relies on the static reference which at that point is again an unbound reference.

In the current setup you also need to make sure your DataInitializerForBom class sticks in memory for the lifetime of the other classes, otherwise you will also see your dataset vanish at unexpected moments.

To me it doesn't seem right for a helper to be the actual owner of the data; helpers should be able to disappear when they've performed their task.

I'd suggest making the controlling class (in the example here that's the test harness) the owner of the actual dataset instance.

(Well, in my opinion the dataset should be a stand-alone object, and the controller should instantiate dataset and logic and link them up as desired. But until the ABL starts exposing built-in objects via OO syntax, building things like that gets messy fast.)

From there:

- Make the helper accept that instance as BY-REFERENCE, call that to re-initialize (fill/empty etc.) the dataset as needed.

- Between controller and BomLogic, use a BIND passing or use BY-REFERENCE on every method call to it. Which option you choose depends on how loose you want your coupling to be vs. perforamce trade-offs etc.

Posted by dbeavon on 22-Oct-2018 09:14

@David, even when passing a dataset "by-reference", the static data can be bound for quite a long time (eg. for the scope of multiple nested method calls).  In addition, for this period of time the AVM prevents you from binding ANOTHER dataset reference to the same ("temporary") static member of the class.  These factors can lead the programmer to think that the reference is not so "temporary" after all (ie. is a permanent fixture in the object).  This is why I was confused when the reference was mysteriously pulled away after the outer-most method lost scope.  It seems like the AVM is going out of its way to disrupt the data once the class has started to rely upon it.

Posted by dbeavon on 22-Oct-2018 09:39

@frank.meulblok I have never seen a case where I had to worry about the lifespan of the DataInitializer utility class.  It should probably be renamed, but it is basically a "factory" for static dataset instances.  Once another class (eg. logic class) has bound to the same dataset, the "factory" class should be able to lose scope without any unexpected consequences.  Are you certain that I will lose my dataset references, even if I have already used BIND to attach them to another class?  I have never observed that to be the case.

Please note that unlike the "DataInitializer" class, my regular logic classes deliberately AVOID having any datasets as members unless they are BY-REFERENCE (ie just pointers).  A logic class should NEVER be designed with "IS A" relationships against the data that it is dependent on.  This is because the dataset is considered a stand-alone object in its own right and may only be associated with the logic class for a particular purpose, after passing it in as a BY-REFERENCE parameter.  Once that purpose has been served, the entire logic class is removed or replaced with another.

The way OO classes interact with static ABL data is much too confusing.  Perhaps there should be another modifier on static data in a class to say that the data references should NOT be pulled away once they are assigned (eg. "REFERENCE-ONLY STICKY").  

In addition to that modifier, there needs to be some enhancements to the language to allow the UN-BINDING of static data and the TESTING of static data to detect whether it is bound.  At a minimum, these are some of the things a programmer would need to better interact with static data from OO classes (see KB's: knowledgebase.progress.com/.../000057279 and knowledgebase.progress.com/.../How-to-test-if-REFERENCE-ONLY-temp-table-is-already-bound)

This thread is closed