Project

General

Profile

Feature #1594

Feature #1592: implement additional database built-in functions

add conversion and runtime support for CURRENT-CHANGED() built-in function

Added by Eric Faulhaber over 11 years ago. Updated over 7 years ago.

Status:
Closed
Priority:
Normal
Start date:
10/17/2012
Due date:
07/22/2013
% Done:

100%

Estimated time:
96.00 h
billable:
No
vendor_id:
GCD
version_reported:
version_resolved:

CC_research_A.p Magnifier - never changed (1.21 KB) Ovidiu Maxiniuc, 10/30/2012 12:12 PM

CC_research_B.p Magnifier - user b will change the data (333 Bytes) Ovidiu Maxiniuc, 10/30/2012 12:12 PM

x3current-changed_3f.p Magnifier - my testcase that sees the change (2.12 KB) Ovidiu Maxiniuc, 10/30/2012 12:12 PM

om_upd20130129a.zip (78 KB) Ovidiu Maxiniuc, 01/29/2013 08:28 AM

om_upd20130829a.zip (178 KB) Ovidiu Maxiniuc, 08/29/2013 02:37 PM

om_upd20130911b.zip (198 KB) Ovidiu Maxiniuc, 09/12/2013 03:22 PM

om_upd20130912e.zip (193 KB) Ovidiu Maxiniuc, 09/13/2013 04:26 AM


Related issues

Related to Database - Feature #2175: add support for -rereadnolock startup parameter Closed

History

#1 Updated by Eric Faulhaber over 11 years ago

  • Assignee set to Ovidiu Maxiniuc
  • Parent task set to #1592

Will require conversion support in builtin_functions.rules and runtime support in RecordBuffer. Possibly will require use of ChangeBroker, but I'm not sure how the 4GL function works. This will require custom test cases to determine the exact, original behavior.

#2 Updated by Eric Faulhaber over 11 years ago

  • Estimated time set to 16.00

#3 Updated by Ovidiu Maxiniuc over 11 years ago

  • Status changed from New to WIP

#4 Updated by Ovidiu Maxiniuc over 11 years ago

To emulate the OpenEdge behavior in P2J runtime I run a few tests. The test cases are a little tricky as they imply having at least two clients connected at the same database/table and editing the same record.
I observed that when testing for changed, OE in fact re-read the record (don't know yet if entire record or only affected fields) from database and compare it with values from memory buffer. There is no flag to mark a record as altered or listener mechanism for clients to be notified when a record changes.
So, while user A is editing a record, some other user (B) can change multiple times the same record and the CURRENT-CHANGED will return false as long as the last modification on the record will set the db record to have the same value as client A has in its memory buffer.

#5 Updated by Eric Faulhaber over 11 years ago

I observed that when testing for changed, OE in fact re-read the record (don't know yet if entire record or only affected fields) from database and compare it with values from memory buffer.

I expect it is re-reading the entire record, because the point of this function seems to be to avoid having session A commit updates to a record made stale by session B's changes while session A was holding the record with no lock. So even if a different field was changed by session A than the one(s) being edited by session B, session B would want to abort its changes. Otherwise, A's changes would be lost when B commits.

When creating test cases, try variations where you update only fields which participate in a table index and variations where you update only fields which do not participate in an index. Progress tends to act differently with respect to indexed fields and non-indexed fields. I wouldn't be surprised if that distinction makes a difference with the CURRENT-CHANGED function.

#6 Updated by Ovidiu Maxiniuc over 11 years ago

I spent some time doing investigations on this issue:
  • I created an index on an existing field
  • I added a new un-indexed field into the table
  • I split the test clients into two .p, each displaying and allowing the user to update different fields of the same record of the table.
    I opened in turns the same record in both clients and changed each field, indexed or not-indexed, which appear in both clients or only in one, changing back in forth to starting value.
    The process is a little complicated since each client must open the database in multiple user access and change alternatively one of the fields and this is the reason why there can not be written a scripting procedure for testing this (or at least I don't know how).
    Bottom line, OE does check the entire field, no matter what fields are displayed, and I found no evidence that it handles indexed fields differently that the ones not indexed.

#7 Updated by Eric Faulhaber over 11 years ago

OK, good research. A few more complications to throw into the mix before we can know the best way to implement...

Does CURRENT-CHANGED detect uncommitted changes in a record? In other words, consider this flow:
  1. user A FINDs/GETs a record with NO-LOCK
  2. user B begins a transaction
  3. user B loads the same record with EXCLUSIVE-LOCK
  4. user B makes a change to a field in the record (test separately for indexed and un-indexed fields), but does not commit the transaction yet (you can put a pause in to accomplish this delay)
  5. user A reloads the record with FIND/GET CURRENT, still with NO-LOCK
  6. user A calls CURRENT-CHANGED
  7. user B commits the transaction

My expectation is that CURRENT-CHANGED in this case would return yes if the changed field was indexed and no if the changed field was not indexed. This would be consistent with Progress' unusual (IMHO defective) transaction isolation.

Another thing we need to know is when Progress detects a change. Is it at the moment FIND/GET CURRENT is called or at the moment CURRENT-CHANGED is called? The former would be easier to implement, because the moment we have retrieved the record with FIND/GET CURRENT (but before we have loaded it into the buffer) would be the ideal time to make the check. At this moment we will have both the old copy of the record in the buffer, as well as the new version we just retrieved. A way to test this:
  1. user A loads a record with FIND NO-LOCK
  2. user B begins a transaction
  3. user A re-loads the record with FIND CURRENT NO-LOCK
  4. user B loads the same record with EXCLUSIVE-LOCK
  5. user B changes the record and commits its transaction
  6. user A calls CURRENT-CHANGED; if it reports yes, the check is done when CURRENT-CHANGED is called; if it reports no, the check is done when FIND CURRENT is called (and the result is cached and later returned by CURRENT-CHANGED)
Finally, does locking during FIND/GET CURRENT play any role? The test:
  1. user A loads a record with FIND NO-LOCK
  2. user B begins a transaction
  3. user B loads the same record with EXCLUSIVE-LOCK
  4. user B changes the record and commits its transaction
  5. user A re-loads the record with FIND/GET CURRENT; in one scenario with NO-LOCK, in the other with SHARE-LOCK or EXCLUSIVE-LOCK
  6. user A calls CURRENT-CHANGED; is there any difference between the result with or without locking in the previous step?

#8 Updated by Ovidiu Maxiniuc over 11 years ago

I started the investigation with two separate users A and B (see the attached CC_research_X files).
However, A never seems to detect changes. Not uncommitted, not committed, not within a transaction, nor without transaction, with or without exclusive LOCK. I don't know yet why, I'm a little tired right now.

On the other hand, the x3current-changed_3f.p does display interesting results:
  • 1st - uncommitted changes of the record are not detected.
  • 2nd - no matter if transaction is started or no, the changes are recognized.
  • 3rd - FIND statements by themselves, alter the result of CURRENT-CHANGED, but I did not find any difference between the two types of locks. Always, before calling FIND the change is invisible. You must call find on the record or CURRENT-CHANGED will return false. After that, the function behaves normally. However, if a second FIND is executed, the changed flag is somehow reset so CURRENT-CHANGED will return now false!

#9 Updated by Greg Shah over 11 years ago

  • Target version set to Milestone 7

#10 Updated by Ovidiu Maxiniuc over 11 years ago

To implement this we need to analyze the data requested from db when the FIND is called.

I see that the RandomAccessQuery.current(LockType), reloads the record from db by loading is as a Persistable object then calling RecordBuffer.setCurrentRecord(...). I believe this is the place where the test should be done (testing the current memory buffer with the dmo object just brought from db). If they differ, a changed flag should be set and the old data overwritten. The CURRENT_CHANGED implementation just returns this flag.
On a second invocation of FIND, if the data from memory buffer and the new dmo are the same, the flag is cleared, so CURRENT_CHANGED will return false as I observed.

#11 Updated by Eric Faulhaber over 11 years ago

Ovidiu Maxiniuc wrote:

To implement this we need to analyze the data requested from db when the FIND is called.

I see that the RandomAccessQuery.current(LockType), reloads the record from db by loading is as a Persistable object then calling RecordBuffer.setCurrentRecord(...). I believe this is the place where the test should be done (testing the current memory buffer with the dmo object just brought from db). If they differ, a changed flag should be set and the old data overwritten. The CURRENT_CHANGED implementation just returns this flag.
On a second invocation of FIND, if the data from memory buffer and the new dmo are the same, the flag is cleared, so CURRENT_CHANGED will return false as I observed.

To make sure I understand how you reached this understanding, please post the test case(s) (annotated with the observed output) that led you to this conclusion. It seems one way to confirm this hypothesis is to run 2 tests in the 4GL.

Test 1:
  1. FIND a record (session A)
  2. Edit the record (session B)
  3. FIND CURRENT the same record (session A)
  4. FIND CURRENT the same record again (session A)
  5. call CURRENT-CHANGED (session A)

If the check happens and the flag is set with FIND CURRENT, the CURRENT-CHANGED call should return false. If the check happens and the flag is set with CURRENT-CHANGED, the CURRENT-CHANGED call should return true.

Test 2:
  1. FIND a record (session A)
  2. Edit the record (session B)
  3. FIND CURRENT the same record (session A)
  4. call CURRENT-CHANGED (session A)
  5. call CURRENT-CHANGED (session A)

if the check happens and the flag is set with CURRENT-CHANGED, the first CURRENT-CHANGED call should return false and the second true. Otherwise, both should return true.

Is this similar to what you did?

#12 Updated by Eric Faulhaber over 11 years ago

Please update the Conversion Reference (builtin_database_functions_methods_attributes.odt chapter) to reflect the implementation of this built-in function.

#13 Updated by Ovidiu Maxiniuc over 11 years ago

Here are my 2 test files:

DEFINE VARIABLE a AS CHARACTER NO-UNDO FORMAT "x(64)".
DEFINE VARIABLE b AS CHARACTER NO-UNDO FORMAT "x(64)".
DEFINE VARIABLE c AS LOGICAL NO-UNDO.
DEFINE VARIABLE d AS LOGICAL NO-UNDO.

DISPLAY "A: observer" WITH SIDE-LABELS.
FIND FIRST Customer NO-LOCK.
a = Customer.Name + " " + STRING(Customer.Balance) + " " + Customer.Address.
DISPLAY a SKIP.
PAUSE MESSAGE  "Record found. Waiting for B to edit".

/*1*/ FIND CURRENT Customer.
c = CURRENT-CHANGED(Customer).

/*2*/ FIND CURRENT Customer.
d = CURRENT-CHANGED(Customer).

DISPLAY IF c = d THEN "as expected." ELSE "FAILED!" 
    FORMAT "x(20)" LABEL "2x call" SKIP.

IF c THEN
    DO:
        b = Customer.Name + " " + STRING(Customer.Balance) +
            " " + Customer.Address.
        DISPLAY "Customer changed: " SKIP b SKIP.
    END.
ELSE
    DISPLAY "No change detected." SKIP.

and the second one:
DISPLAY "B: changer" SKIP WITH SIDE-LABELS.

DO TRANSACTION:
    FIND FIRST Customer EXCLUSIVE-LOCK.
    UPDATE Customer.Name SKIP
           Customer.Balance SKIP
           Customer.Address SKIP.
END. /*transaction*/

DISPLAY "Record altered".

Please note the two lines marked with /*1*/ and /*2*/. I did 4 tests, by removing/commenting-out one, both or neither of them. There is no need to list the output of B, the important is that it will change the record and A will list it before and after B changes it.

1. 1 and 2 are disabled: (c and d are both false) A does not get notified about the change if no FIND is invoked:

┌───────────────────────────────────────────────────────────────────┐
│A: observer                                                        │
│a: Before test 0 aaaa                                              │
│2x call: as expected.                                              │
│No change detected.                                                │
│b: Before test 0 aaaa                                              │
└───────────────────────────────────────────────────────────────────┘

2. only 1 is disabled: (c is false but d is true) because c is evaluated before FIND it is false. However, d is evaluated after, so the "2x call" will normally fail. b is different however, the "No change detected." is displayed based on c, which is evaluated at a wrong time.
┌───────────────────────────────────────────────────────────────────┐
│A: observer                                                        │
│a: Before test 0 aaa                                               │
│2x call: FAILED!                                                   │
│No change detected.                                                │
│b: After test 2 0 2222                                             │
└───────────────────────────────────────────────────────────────────┘

3. only 2 is disabled: (c and d are both true) as I posted in my previous note, subsequent calls on the function will return the same positive result:
┌───────────────────────────────────────────────────────────────────┐
│A: observer                                                        │
│a: Before tests 0 aaaa                                             │
│2x call: as expected.                                              │
│Customer changed:                                                  │
│b: After test 3 0 333333                                           │
└───────────────────────────────────────────────────────────────────┘

4. both line are executing: (c is true but d is false) c sees the change, however, at the second call d is false because the second call to FIND has re-validated the memory buffer:
┌───────────────────────────────────────────────────────────────────┐
│A: observer                                                        │
│a: Before tests 0 aaaa                                             │
│2x call: FAILED!                                                   │
│Customer changed:                                                  │
│b: After test 4 0 44444                                            │
└───────────────────────────────────────────────────────────────────┘

#14 Updated by Ovidiu Maxiniuc over 11 years ago

When implementing as described above I got into the following issue:
B has committed the changes to db (I can see this using pgAdmin) and A is FIND ing the record to promote to exclusive mode and see if content has changed.
In com.goldencode.p2j.persist.Persistence.load() at line 1947, it calls org.hibernate.Session.get(). However, I get the old, unmodified value instead of the new one, committed by B. The javadoc for get:
Return the persistent instance of the given entity class with the given identifier, or null if there is no such persistent instance. (If the instance is already associated with the session, return that instance. This method never returns an uninitialized instance.)
The load javadocs explain better:
You should not use this method to determine if an instance exists (use get() instead).
Most probably, the record I request is cached by hibernate. I believe a refresh() is necessary to force hibernate to reload the content of the record, as simply calling get() will only check if the record has not been deleted.
My problem is, calling refresh() each time, wouldn't be a performance hit?

Later edit:
Just saw that the refresh is called a few lines below. My problem was/is that I get a proxy, in fact the same dmo I already have in RecordBuffer. To check if the record has changed, I need to have a second copy of the object or a heuristic approach like hashCode() to compare the record after get().

#15 Updated by Eric Faulhaber over 11 years ago

Ovidiu Maxiniuc wrote:

Later edit:
Just saw that the refresh is called a few lines below. My problem was/is that I get a proxy, in fact the same dmo I already have in RecordBuffer. To check if the record has changed, I need to have a second copy of the object or a heuristic approach like hashCode() to compare the record after get().

OK, that makes more sense, based on my recollection of how the Hibernate first and second level caching was working. If a DMO is loaded in any session, it should be the same object instance as a DMO with the same ID in any other session. Have a look at the snapshot code in RecordBuffer, at RecordBuffer.stateChanged(RecordChangeEvent), and at the ChangeBroker class. Is there anything useful you can tap into there?

#16 Updated by Eric Faulhaber over 11 years ago

Nevermind on ChangeBroker; I forgot that is just about changes across buffers in the same session, not across sessions. However, this brings up another point: we need to test CURRENT-CHANGED behavior when a buffer changes a record already held by another buffer in the same session. I would expect it behaves the same way as if the record had been changed by another session, but best to test this explicitly, since this is how our current implementation will behave.

#17 Updated by Eric Faulhaber over 11 years ago

With respect to the documentation update, please also update the "master" table of all supported built-in functions in the "Functions" section of the "Expressions" chapter of the Conversion Reference.

#18 Updated by Eric Faulhaber over 11 years ago

  • % Done changed from 0 to 60

#19 Updated by Ovidiu Maxiniuc over 11 years ago

To test two buffers within the same session I wrote the following test.
It FINDs the first record, then enables a BROWSE widget in which I manually alter the 1st record, and when user input ends, I find the 1st record again and check for change:

DEFINE QUERY q1 FOR Customer SCROLLING.
DEFINE BROWSE b1 QUERY q1
        DISPLAY Name Balance Address WIDTH 18
        ENABLE Name Balance
        WITH 10 DOWN SEPARATORS.
DEFINE BUTTON btn-Exit LABEL "Exit".
DEFINE FRAME f1
        b1 SKIP btn-Exit
        WITH SIDE-LABELS USE-TEXT CENTERED
        ROW 2 TITLE "Current-changed with 2 buffers".

FIND FIRST Customer NO-LOCK.
DEFINE VARIABLE c-before AS CHARACTER NO-UNDO FORMAT "x(64)".
c-before = Customer.Name + " " + STRING(Customer.Balance) + " " +
        Customer.Address.

OPEN QUERY q1 FOR EACH Customer NO-LOCK.
ENABLE ALL WITH FRAME f1.
WAIT-FOR CHOOSE OF btn-Exit.
CLOSE QUERY q1.

FIND FIRST Customer NO-LOCK.
DEFINE VARIABLE c-after AS CHARACTER NO-UNDO FORMAT "x(64)".
c-after = Customer.Name + " " + STRING(Customer.Balance) + " " +
        Customer.Address.

DISPLAY IF CURRENT-CHANGED Customer THEN "Changed!" 
        ELSE "Unchanged" LABEL "Ch Check" 
    WITH SIDE-LABELS.

DISPLAY SKIP c-before SKIP c-after.

As you can see the customer's fields are saved to a variable before and after the browse editing.
However, even if they are different the CURRENT-CHANGED returns false.
┌──────────────────────────────────────────────────────────────────────────┐
│Ch Check: Unchange                                                        │
│c-before: bbbbb 0 44444                                                   │
│c-after: aaa 0 44444                                                      │
└──────────────────────────────────────────────────────────────────────────┘

#20 Updated by Ovidiu Maxiniuc over 11 years ago

However, I was unable to run the converted code from my previous note. When moving from a field to another, the client receives the following error messages:

** Customer record has NO-LOCK status, update to field not allowed. (396)
** Unable to update customer Field. (142)

while the server console displays the following exceptions:
com.goldencode.p2j.NumberedException: Unable to update customer Field
        at com.goldencode.p2j.util.ErrorManager.recordOrThrowError(ErrorManager.java:606)
        at com.goldencode.p2j.persist.RecordBuffer$Handler.invoke(RecordBuffer.java:7339)
        at com.goldencode.p2j.persist.$Proxy1.setName(Unknown Source)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at com.goldencode.p2j.persist.FieldReference.set(FieldReference.java:540)
        at com.goldencode.p2j.ui.Element.set(Element.java:333)
        at com.goldencode.p2j.ui.BrowseColumnWidget.setValue(BrowseColumnWidget.java:261)
        at com.goldencode.p2j.ui.BrowseWidget.updateRow(BrowseWidget.java:1085)
        at com.goldencode.p2j.ui.LogicalTerminal.updateRow(LogicalTerminal.java:6735)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at com.goldencode.p2j.util.MethodInvoker.invoke(MethodInvoker.java:76)
        at com.goldencode.p2j.net.Dispatcher.processInbound(Dispatcher.java:675)
        at com.goldencode.p2j.net.Conversation.block(Conversation.java:316)
        at com.goldencode.p2j.net.Conversation.waitMessage(Conversation.java:254)
        at com.goldencode.p2j.net.Queue.transactImpl(Queue.java:1107)
        at com.goldencode.p2j.net.Queue.transact(Queue.java:575)
        at com.goldencode.p2j.net.BaseSession.transact(BaseSession.java:178)
        at com.goldencode.p2j.net.HighLevelObject.transact(HighLevelObject.java:163)
        at com.goldencode.p2j.net.RemoteObject$RemoteAccess.invokeCore(RemoteObject.java:1406)
        at com.goldencode.p2j.net.InvocationStub.invoke(InvocationStub.java:97)
        at $Proxy0.waitFor(Unknown Source)
        at com.goldencode.p2j.ui.LogicalTerminal.waitFor(LogicalTerminal.java:3671)
        at com.goldencode.p2j.ui.LogicalTerminal.waitFor(LogicalTerminal.java:3439)
        at com.goldencode.testcases.util.X3cc1session$1.body(X3cc1session.java:62)
        at com.goldencode.p2j.util.BlockManager.processBody(BlockManager.java:6983)
        at com.goldencode.p2j.util.BlockManager.topLevelBlock(BlockManager.java:6890)
        at com.goldencode.p2j.util.BlockManager.externalProcedure(BlockManager.java:194)
        at com.goldencode.p2j.util.BlockManager.externalProcedure(BlockManager.java:180)
        at com.goldencode.testcases.util.X3cc1session.execute(X3cc1session.java:35)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at com.goldencode.p2j.util.Utils.invoke(Utils.java:1153)
        at com.goldencode.p2j.main.StandardServer$MainInvoker.execute(StandardServer.java:1513)
        at com.goldencode.p2j.main.StandardServer.invoke(StandardServer.java:1019)
        at com.goldencode.p2j.main.StandardServer.standardEntry(StandardServer.java:320)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at com.goldencode.p2j.util.MethodInvoker.invoke(MethodInvoker.java:76)
        at com.goldencode.p2j.net.Dispatcher.processInbound(Dispatcher.java:675)
        at com.goldencode.p2j.net.Conversation.block(Conversation.java:316)
        at com.goldencode.p2j.net.Conversation.run(Conversation.java:158)
        at java.lang.Thread.run(Thread.java:662)
Caused by: com.goldencode.p2j.persist.PersistenceException: Customer record has NO-LOCK status, update to field not allowed
        at com.goldencode.p2j.persist.RecordBuffer.upgradeLock(RecordBuffer.java:4284)
        at com.goldencode.p2j.persist.RecordBuffer$Handler.invoke(RecordBuffer.java:7335)
        at com.goldencode.p2j.persist.$Proxy1.setName(Unknown Source)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at com.goldencode.p2j.persist.FieldReference.set(FieldReference.java:540)
        at com.goldencode.p2j.ui.Element.set(Element.java:333)
        at com.goldencode.p2j.ui.BrowseColumnWidget.setValue(BrowseColumnWidget.java:261)
        at com.goldencode.p2j.ui.BrowseWidget.updateRow(BrowseWidget.java:1085)
        at com.goldencode.p2j.ui.LogicalTerminal.updateRow(LogicalTerminal.java:6735)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at com.goldencode.p2j.util.MethodInvoker.invoke(MethodInvoker.java:76)
        at com.goldencode.p2j.net.Dispatcher.processInbound(Dispatcher.java:675)
        at com.goldencode.p2j.net.Conversation.block(Conversation.java:316)
        at com.goldencode.p2j.net.Conversation.waitMessage(Conversation.java:254)
        at com.goldencode.p2j.net.Queue.transactImpl(Queue.java:1107)
        at com.goldencode.p2j.net.Queue.transact(Queue.java:575)
        at com.goldencode.p2j.net.BaseSession.transact(BaseSession.java:178)
        at com.goldencode.p2j.net.HighLevelObject.transact(HighLevelObject.java:163)
        at com.goldencode.p2j.net.RemoteObject$RemoteAccess.invokeCore(RemoteObject.java:1406)
        at com.goldencode.p2j.net.InvocationStub.invoke(InvocationStub.java:97)
        at $Proxy0.waitFor(Unknown Source)
        at com.goldencode.p2j.ui.LogicalTerminal.waitFor(LogicalTerminal.java:3671)
        at com.goldencode.p2j.ui.LogicalTerminal.waitFor(LogicalTerminal.java:3439)
        at com.goldencode.testcases.util.X3cc1session$1.body(X3cc1session.java:62)
        at com.goldencode.p2j.util.BlockManager.processBody(BlockManager.java:6983)
        at com.goldencode.p2j.util.BlockManager.topLevelBlock(BlockManager.java:6890)
        at com.goldencode.p2j.util.BlockManager.externalProcedure(BlockManager.java:194)
        at com.goldencode.p2j.util.BlockManager.externalProcedure(BlockManager.java:180)
        at com.goldencode.testcases.util.X3cc1session.execute(X3cc1session.java:35)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at com.goldencode.p2j.util.Utils.invoke(Utils.java:1153)
        at com.goldencode.p2j.main.StandardServer$MainInvoker.execute(StandardServer.java:1513)
        at com.goldencode.p2j.main.StandardServer.invoke(StandardServer.java:1019)
        at com.goldencode.p2j.main.StandardServer.standardEntry(StandardServer.java:320)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at com.goldencode.p2j.util.MethodInvoker.invoke(MethodInvoker.java:76)
        at com.goldencode.p2j.net.Dispatcher.processInbound(Dispatcher.java:675)
        at com.goldencode.p2j.net.Conversation.block(Conversation.java:316)
        at com.goldencode.p2j.net.Conversation.run(Conversation.java:158)
        at java.lang.Thread.run(Thread.java:662)

#21 Updated by Eric Faulhaber over 11 years ago

It appears you have uncovered a bug in how we deal with default locking in a BROWSE. BTW, what database are you using for your testcases? You should only be using (and converting) data that we own for testcases (like the p2j_test database from p2j/testcases/uast/data). Please open a separate bug issue for this finding in the Database project, but unset the start date and do not assign it.

Back to the original matter: I don't think this testcase is doing what you meant it to. You are using the same buffer in your FINDs as in your browse query, and you are never invoking FIND CURRENT, just FIND FIRST twice. I wouldn't expect CURRENT-CHANGED to report a difference in this case. Let's take BROWSE out of the picture altogether; it adds unnecessary complexity. Try something simple, like:

DEFINE VAR old-pub like book.publisher.
DEFINE BUFFER x-book for book.

FIND FIRST book NO-LOCK.
FIND FIRST x-book EXCLUSIVE-LOCK.
old-pub = x-book.publisher.
x-book.publisher = "Whatever".
FIND CURRENT book NO-LOCK.

MESSAGE "book changed: " CURRENT-CHANGED(book).

x-book.publisher = old-pub.
I haven't run this, so you may need to tweak the syntax in case I mistyped something, but the idea is to minimize the complexity and to reduce it to the simplest case possible:
  1. FIND a record with no lock in the default buffer
  2. FIND the same record in a different buffer and modify it
  3. FIND CURRENT in the default buffer to re-load the record
  4. Invoke CURRENT-CHANGED on the default buffer and check the result
  5. Set the modified field back to its original value

#22 Updated by Ovidiu Maxiniuc over 11 years ago

I am using a very simple database (one table with a few records) that I created manually which is somehow similar to p2j/testcases/uast/data/p2j_test.df.

Here is my test procedure:
DEFINE VARIABLE old-name LIKE Customer.NAME.
DEFINE BUFFER x-cust FOR Customer.

FIND FIRST Customer NO-LOCK.
old-name = Customer.NAME.

FIND FIRST x-cust EXCLUSIVE-LOCK.
    x-cust.NAME = "After".
FIND FIRST x-cust NO-LOCK.

PAUSE MESSAGE "Hit space to continue...".
FIND CURRENT Customer NO-LOCK.

MESSAGE "Customer changed? " CURRENT-CHANGED(Customer) old-name Customer.NAME.

and here is the result:
Customer changed?  no Before After

As you can see, Progress does not 'see' the change through CURRENT-CHANGED function, but the new value of the field is visible.
I tried some variants here:
  • I changed the FIND CURRENT with FIND FIRST. There is no difference, the outcome is the same.
  • I have interchanged the buffers, altering the data in default buffer and testing for CURRENT-CHANGED in the 2nd buffer. Also no visible difference.
  • The 3rd choice was to confirm my findings by adding a second client that would change the data while the first one is held in the PAUSE MESSAGE.... This time I encounter an unexpected behavior. The second client could not UPDATE the record because it was in use by om on CON:. . It looks like some kind of LOCK remains on the record even though you can clearly see that the lock is released immediately after the assignment and just before the PAUSE statement. After hitting space key and terminating the 1st procedure the LOCK is really released and
    the second client has the opportunity to UPDATE the record.

#23 Updated by Ovidiu Maxiniuc over 11 years ago

A small problem regarding the implementation:
I could not find in the hibernate documentation a way to detect if a record has changed. I manged to detect some changes using hashes over the fields of dmo (Persistable) objects obtained by reflection. However, this is very heuristical, as collisions are rather possible so this is only useful for accelerating the change detection process. To exactly detect a change, a bit-by-bit comparison is necessary. For that I keep a deepCopy of the Persistable, and when hashes are equal I check field by field if it has changed when hibernate (re-)load the record from database.
Here is the psudo-code:

void updateCurrentChangedFlag(boolean reset) {
    int newHash = 0;
    if (this.currentRecord != null) {
        foreach (getMethod m of this.currentRecord) {
            Object field = m.invoke(this.currentRecord);
            if (field != null) {
               newHash ^= field.hashCode(); // also += should work
            }            
        }
    }
    boolean changeDetected = (this.currentHash != newHash); //heuristically by compare hashes
    if (!changeDetected) {
       //if changed not detected heuristically, try heavy duty:
       if (this.bgDeepCopy != null && !this.bgDeepCopy.equals(this.currentRecord)) {
            //change detected by checking bit-by-bit
            foreach (getMethod m of this.currentRecord) {
                Object field1 = m.invoke(this.currentRecord);
                Object field2 = m.invoke(this.bgDeepCopy);
                if (!field1.equals(field2)) {
                     changeDetected = true;
                     break;
                }            
           }
       }
   }

   this.isRecordChanged = (reset) ? false : changeDetected;

   if (changeDetected) {
      //prepare for next call (save newly computed hash and deep copy of new data):
      this.bgDeepCopy = (this.currentRecord == null) ? null : this.currentRecord.deepCopy();
      this.currentHash = newHash;
   }
}

My impression is that this approach is too expensive, I wonder if there isn't a better way to do it ...?

In addition, as Eric mentioned in mail, the method should be called only when the record is being re-loaded with FIND CURRENT, when it was previously held with NO-LOCK before that. This should reduce the performance hit by only doing the comparison when we are most likely to need it for a subsequent CURRENT-CHANGED call. This probably means an API change to RecordBuffer.setCurrentRecord, or an additional method specifically for this purpose, which is only called from RandomAccessQuery.current.

#24 Updated by Eric Faulhaber over 11 years ago

Please review the use of the snapshot instance variable in RecordBuffer. Can this variable (i.e., when it is a different object instance than the current record) be of any use in detecting a change?

#25 Updated by Ovidiu Maxiniuc over 11 years ago

Indeed, the snapshot seems to be used for a similar reason, but I see it's only used for undo-s, it only stores session-local changes and have no synchronization with db.

During the weekend and today I thought of a solution for a clean implementation, and I got the following idea:
Why not marking the Persistable object directly when Hibernate more intelligent ?
We add a bool flag named dirty (or changed) that will be set each time Hibernate calls one of the setters.
Then we add a getter for this flag and a reset method that will be called on local assign/updates.
This way no extra computation is necessary to detect the change, only check the dmo's flag.

#26 Updated by Eric Faulhaber over 11 years ago

This is an interesting idea, but it is pretty intrusive in the DMOs. Let's go with your first approach, limiting the cases where it is used as much as possible. If we find hard evidence that it creates a bottleneck, we have a backup approach to consider and refine.

#27 Updated by Ovidiu Maxiniuc over 11 years ago

I put aside all above and started with a fresh new idea. Please correct me if I'm wrong.

P2J is informed about changes of records db only using org.hibernate.Session.refresh() method while the fresh records are loaded from db using org.hibernate.Session.load().
The actual P2J code is smart enough to only call refresh() when needed (when the dmo is not null, not fresh, and the lock is upgraded from NO-LOCK) in Persistence.load(RecordBuffer, String, Object[], Type[], LockType, boolean, boolean) and Persistence.load(String, String, Serializable, LockType, boolean).

However I am not sure about the 3rd place: Persistence.quickLoad(RecordIdentifier, boolean).
For the first two occurrences I will add the code to check if dmo changes in refresh() (deepCopy before and text after the method call), but there is the need to propagate the result up the calling stack to the RecordBuffer by using an extra parameter, or keep a changed flag in dmo itself.
What do you say about this approach ?

#28 Updated by Eric Faulhaber over 11 years ago

Ovidiu, do you have any code in process that you have not attached to this issue? We at least need the conversion changes and runtime stubs for Milestone 4, even though we haven't settled on a runtime implementation yet.

#29 Updated by Ovidiu Maxiniuc over 11 years ago

I attached the code. I merged it with latest sources from bzr as it was a couple of months old.
The conversion runs fine, the runtime implementation works, but is not perfect.

#30 Updated by Eric Faulhaber over 11 years ago

After review, I added currentChanged and _currentChanged to the Buffer interface, and removed the static versions from RecordBuffer. I changed the conversion rules accordingly.

Regression testing revealed a problem with the runtime implementation, which I would like to revisit later. I have disabled the runtime implementation of RecordBuffer.updateCurrentChangedFlag, which causes the regression when used with a table with an extent field:

[01/29/2013 18:20:53 EST] (org.hibernate.LazyInitializationException:SEVERE) failed to lazily initialize a collection of role: aero.timco.majic.dmo._temp.impl.TempRecord14Impl.
composite10 - no session or session was closed
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: aero.timco.majic.dmo._temp.impl.TempRecord14Impl.composite10 - no session or sessio
n was closed
        at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:191)
        at org.hibernate.collection.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:183)
        at org.hibernate.collection.AbstractPersistentCollection.read(AbstractPersistentCollection.java:48)
        at org.hibernate.collection.PersistentList.get(PersistentList.java:219)
        at aero.timco.majic.dmo._temp.impl.TempRecord14Impl.getAmount(TempRecord14Impl.java:175)
        at aero.timco.majic.dmo._temp.impl.TempRecord14Impl.assign(TempRecord14Impl.java:97)
        at aero.timco.majic.dmo._temp.impl.TempRecord14Impl.deepCopy(TempRecord14Impl.java:75)
        at com.goldencode.p2j.persist.RecordBuffer.updateCurrentChangedFlag(RecordBuffer.java:6247)
        at com.goldencode.p2j.persist.RecordBuffer.setCurrentRecord(RecordBuffer.java:6166)
        at com.goldencode.p2j.persist.RecordBuffer.setRecord(RecordBuffer.java:4501)
        at com.goldencode.p2j.persist.RandomAccessQuery.updateBuffer(RandomAccessQuery.java:2029)
        at com.goldencode.p2j.persist.FindQuery.updateBuffer(FindQuery.java:604)
        at com.goldencode.p2j.persist.RandomAccessQuery.unique(RandomAccessQuery.java:1520)
        at com.goldencode.p2j.persist.RandomAccessQuery.unique(RandomAccessQuery.java:1433)
        ...

The full server log file from the regression test run is at #1981.

I am leaving the conversion changes in place, which are needed for milestone 4.

#31 Updated by Eric Faulhaber about 11 years ago

  • Estimated time changed from 16.00 to 96.00

#32 Updated by Eric Faulhaber about 11 years ago

  • Due date set to 07/15/2013

#33 Updated by Eric Faulhaber about 11 years ago

  • Due date changed from 07/15/2013 to 07/22/2013

#34 Updated by Ovidiu Maxiniuc almost 11 years ago

You were right about the implementation. I changed a little, I'm doing the check for changed field in a bracket in RandomAccessQuery.current().
However, I encountered a really nasty issue with Hibernate and its double cache implementation.
As documented in Persistence.load(), lines 2083 and 2354, I need a brute force to get the data back from db if I am on another context. Those conditional following those lines were specially written for upgrading the locktype from None to something higher. But in my case I need the fresh data and I do not always meet this condition.
I wonder.. is there something bad if I loosen them a little (like result != null && updateLock)? An excessive db access ?

As far as I can remember, I did test this 10 months ago and it was working (indeed, not in all cases). The new Hibernate might be more aggressive to cache the values ?

#35 Updated by Eric Faulhaber almost 11 years ago

I don't know if the caching behavior changed with the Hibernate upgrade. Regardless, I want to keep the calls to Session.refresh to an absolute minimum, since each one causes a round trip to the database. If we do this in more cases than necessary, it will affect performance. This is why I originally protected these calls in the most restricted manner I could, while fixing the stale data problem I had encountered.

If I understand correctly, you need a less restrictive version of the load methods, but only from certain call paths (hopefully not that commonly used). If this understanding is correct, please add worker variants of load which are package private, and which accept a flag parameter which governs whether the more restrictive check is done or not. The existing load methods can call these workers with the flag set to a default value, and you can call them with the flag set differently for only the specific call paths which you need to implement CURRENT-CHANGED properly. This way, all the other, existing calls to load will be unaffected, other than having one more method on the call stack.

Does this approach make sense, or are you saying that every call to Persistence.load must be changed to make CURRENT-CHANGED work? If so, it sounds like we will have a performance problem, since this effectively would negate most of the benefit of caching.

#36 Updated by Ovidiu Maxiniuc almost 11 years ago

I am forced to do the implementation as you suggested.
I really do not understand why my session of hibernate cannot 'see' the change on the same process that was committed (it's visible in db) on another session (in the p2j-context of other user).
So this was the question I tried to answer: shouldn't both sessions share same cache (2nd level)?

Going back and looking at the configuration of P2J I found a first answer why this is not happening. There is no second level cache in Hibernate. The directory.xml explicitly say: hibernate.cache.use_second_level_cache = FALSE. So even if the default hibernate use_second_level_cache is true, the 2nd level cache is not active.

I turned it on by setting the value to true and specifying the @hibernate.cache.region.factory_class = org.hibernate.cache.ehcache.EhCacheRegionFactory (this is the new factory for Hibernate 4). The changes from other thread failed to be visible again.

Then I checked the caching approach for each objects. The nonstrict-read-write from .hbm.xml seemed to be the inconvenient as the docs say: "strategy makes no guarantee that the item returned from the cache is the latest version available in the database". I have changed it to read-write but it didn't help either.

The last option: deeply dig into the Hibernate to find the problem. Doing this you will reach the DefaultLoadEventListener.doLoad() which explain why none if my previous attempts worked. It first looks for the entity into the session cache (loadFromSessionCache()). As the record was previously read by this context/session the record will be found and returned with the state it was grabbed earlier. The attempt to check with loadFromSecondLevelCache is only performed only if the entity wasn't found in the first attempt. This is not a read/write cache behavior, but rather a read-only one.

My conclusion is that this second level cache is not useful from CURRENT-CHANGED' s point of view and indeed, the new value of the record to be checked must be reloaded directly from db. In fact not only FIND CURRENT fails to refresh the data but any other forms of FIND (FIRST/NEXT/PREV/LAST). They cannot see the current value of a record after it was altered by other concurrent user (on the same server).

Now I could tell why my tests from 10 months ago "were" working. They were merely upgrading the lock type of none to exclusive-lock in the FIND CURRENT statement. This caused the if -s i mentioned in note 34 to evaluate to true to the data was re-fetched from db server. However, this is not always the case, my recent test cases ran on windev01 were not upgrading the lock level and Progress was returning the updated values for altered records.

#37 Updated by Eric Faulhaber almost 11 years ago

Ovidiu Maxiniuc wrote:

There is no second level cache in Hibernate. The directory.xml explicitly say: hibernate.cache.use_second_level_cache = FALSE.

In production use, this is normally enabled, but I guess we have it turned off by default in the test case environment. The CURRENT-CHANGED implementation in P2J has to work regardless of this setting, however, so we can't rely on this option being set a certain way.

#38 Updated by Ovidiu Maxiniuc almost 11 years ago

I know.
The important issue is that if you have 2 clients on the same P2J server and

Client A:

FIND FIRST customer.
DISPLAY name.
PAUSE. /* wait for B here */
FIND FIRST customer.
DISPLAY name.

Client B:

FIND FIRST customer.
ASSIGN name = "changed".

and they run concurrently (B executes while A pauses), A won't see the change B has done.

#39 Updated by Eric Faulhaber almost 11 years ago

How does this test case behave in the 4GL?

The customer table is a bit unusual in our p2j_test database in that it has no indexes defined. So, I wouldn't expect you would see a change to a non-indexed field before the transaction in session B was committed. In my work implementing the "dirty share" infrastructure, I found it was only the indexed fields whose changes were leaked across sessions from uncommitted transactions. Try the same test with the person table and the last-name field (which is the first field in the name index). I think you will see a different result.

Nevertheless, if your original test case using the customer table behaves differently between the 4GL and P2J, you have found a bug.

#40 Updated by Ovidiu Maxiniuc almost 11 years ago

Just re-checked with customer and person tables.
There is definitely a bug, in P2J the A client displays the same 'old' value in the second DISPLAY:
A: (no-Lock is needed to let B acquire write lock)

FIND FIRST person no-Lock.
DISPLAY "1: " + last-name.
PAUSE. /* wait for B here */
FIND FIRST person no-lock.
DISPLAY "2: " + last-name.

B:
FIND FIRST person.
ASSIGN last-name = "Smith".

#41 Updated by Eric Faulhaber almost 11 years ago

Oops, I misread your test case. This is not a dirty share issue, as I was thinking. Session B actually commits its change before session A re-FINDs the record, so it's not about leakage of uncommitted transaction data. Sorry for the confusion.

If the 4GL is seeing the update in session A, it sounds like the -rereadnolock startup parameter is in use. Normally (i.e., without the -rereadnolock parameter), you would see the same record in session A after the second FIND.

See http://knowledgebase.progress.com/articles/Article/P109264 and some of the related articles for a description of this parameter.

IIRC, the current implementation of P2J does not honor -rereadnolock. I will have to confirm whether it is in use in the current project. If so, we will have to add support.

#42 Updated by Eric Faulhaber almost 11 years ago

The -rereadnolock parameter is indeed in use in the server project. We will need to support this as a configurable setting, since some projects do not use it. I'll add a separate task for it.

Please check whether it is in use in your test case environment.

#43 Updated by Ovidiu Maxiniuc almost 11 years ago

The -rereadnolock is not activated and in 4GL this case is not taken into consideration.

In fact this is the second what we have here is the last sample from note 2 from #2175, with a single buffer. In this case P2J behaves like first case without the parameter (ie the data is not refreshed).
This is very weird. Evidently, P4GL had a bug and they tried to correct it. But they couldn't as probably there was some old code that was working because of the "feature". So they added the command-line parameter for fixing the issue.

#44 Updated by Ovidiu Maxiniuc almost 11 years ago

Update for review.
We need to update the current-changed each time P2JQuery.current() is called. Other first/next/last/prev are ignored, in fact they should invalidate (and they do this automatically in RecordBuffer.setCurrentRecord() when altering currentRecord, the recordChanged is set to false).
The algorithm is as follow:
  • in current() before calling RecordBuffer.reload() we save a copy of the currentRecord from buffer
  • during RecordBuffer.reload(), the currentRecord will eventually change and the recordChanged will be invalidated
  • now we have the old value saved and we only compare it to currentRecord in updateCurrentChanged() and then the intermediate object is set to null. The compare is done field-by-field (including extents) as the DMOs do not have any mechanism for checking if they have changed.

All Persistence.load() methods have now an extra boolean parameter that allows to force the refresh of the record from a higher level, probably directly from database. I found this more elegant than doubing in signatures. This will probably be used in the implementation of #2175 as well.
One important issue here is when to force this reload. For this issue, we need this only when FIND/GET CURRENT is called. The RecordBuffer.load() will do the load from persistence, but we need to identify only the cases that came from current(). I found useful the undo parameter (as documented in code) but on the other side at a later thought checking if shadowCopyRecord is not null is also a good (maybe better) way to force reload of the record from db.

#45 Updated by Ovidiu Maxiniuc almost 11 years ago

The initial tests had some unimportant fails in CTRL+C and quite a lot of fails in the main part:
  • 7 in gso_tests (gso_29 / 77, gso_281 / 31, gso_307 / 40, gso_394 / 10, gso_395 / 55, gso_414 / 20 and gso_422 / 38),
  • 5 in tc_tests (tc_inquiry_inventory_017 / 19, tc_job_002 / 40, tc_job_clock_005 / 8, tc_job_matlcron_002 / 23 and tc_po_012 / 28).

After investigations the conclusion was that most of the fails are caused by temp-tables which majic uses for intermediary results (like sums) before extracting the final report. Indeed, when working with temp tables, each user has its own instance of it and cannot touch other temp-tables than its own.
So it does not make sense to forcefully refresh from backing db a dmo obtained by session.get() in the eventuality other user has altered the data from my temp-table.

Attached there is the update with the exclusion of temp-tables from the forced refresh.
It passed all the above mentioned tests but failed another: gso_17 step 79.
This is really strange because the test-set does in fact two almost identical reports, the more detailed one passed in the step 78. The fail is caused by the addition of an extra employee with id = 1234 in gso_17.sum that is not visible in gso_17.txt.

#46 Updated by Ovidiu Maxiniuc almost 11 years ago

Yesterday my work was focused on fixing the queries for failing tests. Because of the large amounts of sql queries needed in a report, it was nearly impossible to directly pinpoint the cause. The suspected cause was the double call to RecordBuffer.load(); it was fixed.

The update contains the changes for issue #2175, too.

This update fixes the issue that caused test fails in previous updates. Also, because of the constant parameters, the signature of one Persistence.load() was reverted so less files are affected by this update.

It has passed the full regression test-set (only failed tests were the expected ones from CTRL+C and tc_job_002).
If the review is OK then I will commit the changes to bzr.

#47 Updated by Eric Faulhaber almost 11 years ago

Code review 20130911b/20130912e:

Changes look good, but please help me understand why the refresh code in the 2 main worker variants of Persistence.load is slightly different. On version contains an extra else if (crtRecBuffCount == 1) test.

#48 Updated by Ovidiu Maxiniuc almost 11 years ago

The crtRecBuffCount == 1 means that the respective record is referred by one buffer. In this case, there are two cases:
  • the buffer that refers the record is exactly the one into which the record is stored (and sent to this worker as buffer parameter) - that is a FIND FIRST buffer statement - this will end up by refreshing the record from DB, so forceRefresh is set to true.
  • the buffer is not holding a record or is holding a different one. In this case we let the forceRefresh unchanged. This is exactly as the same case as crtRecBuffCount >= 2, meaning that there is a copy of the record already cached somewhere in another buffer and should not be forced to be refreshed.
    Of course, these are happening in the local.isRereadNoLock() is false. If the parameter is detected the refresh is forced anyway, no matter in how many buffers the record is stored. And, if refreshed, all those buffers will reflect that.

On the other hand, the absence of crtRecBuffCount == 1 test in second method is normal as the method do have a destination buffer, so if the crtRecBuffCount > 0 we know that there is already a buffer storing the record and it will processed accordingly.

#49 Updated by Eric Faulhaber almost 11 years ago

OK, thanks. Please commit and distribute the update.

#50 Updated by Ovidiu Maxiniuc almost 11 years ago

The update was committed to bzr as revision 10381 and distributed by email.

#51 Updated by Eric Faulhaber almost 11 years ago

  • Status changed from WIP to Closed
  • % Done changed from 60 to 100

#52 Updated by Greg Shah over 7 years ago

  • Target version changed from Milestone 7 to Runtime Support for Server Features

Also available in: Atom PDF