Question Life after Death (DELETE) - is it a bug or miracle?

The session connects remotely to the database, deletes some records, commits the transaction after each record deletion, and finally re-reads some of the recently deleted records. The size of the records matters!

Explanation of “miracle”: if we don’t use NO-PREFETCH option then Progress fetches several records at a time and sends them to the client in one network message. Client stores the received messages in its memory while processing the records one by one. Documentation does not explain how many network messages can be cached by client. In ancient times there was the -Nb parameters called “Network Buffers” but most likely it’s obsolete now. As we know the size of the messages is defined by the -Mm parameter.

The code below deletes each records followed the records with the even keys. In other words, it deletes the records that are already stored in a current network message and that is not yet processed by the client. Obviously the client can’t re-read the deleted records using any locks other than NO-LOCK.

It’s happened with every sixth record. Let’s create only 6 records and check tabanalys:
Code:
                     -Record Size (B)-          ---Fragments--- Scatter
Table          Records    Size   Min   Max  Mean   Count Factor  Factor
PUB.theTable         6   15.8K  2704  2704  2704       6    1.0     2.3

The total size of the records is 15.8K. The tests were run with the -Mm 16K. Some number of bytes in network message is taken up by the service information. I guess the client got two network messages. If I would decrease a record size just by one byte the session will not read the deleted records. NO-PREFETCH option also solves the issue.

What do you think? Is it a Progress bug?

Result:
Code:
Key Can lock exclusively
  1 yes
  3 yes
  5 yes
  6 no
  7 yes
  9 yes
 11 yes
 13 yes
 15 yes
 16 no
 17 yes
 19 yes
 21 yes
 23 yes
 25 yes
 26 no

Procedure to reproduce the issue (note: "MeatSize"=2683+ is for -Mm 16384):
Code:
RUN CreateRecords(26, 2683).
RUN DeleteRecords(2).

/* ------------------------------------------------------------------------- */
PROCEDURE CreateRecords:

  DEFINE INPUT PARAMETER ipRecords  AS INTEGER NO-UNDO.
  DEFINE INPUT PARAMETER ipMeatSize AS INTEGER NO-UNDO.
 
  DEFINE VARIABLE vMeat AS CHARACTER NO-UNDO.
  DEFINE VARIABLE vKey  AS INTEGER   NO-UNDO.
 
  FOR EACH theTable EXCLUSIVE-LOCK:
    DELETE theTable.
  END. /* theTable */

  ASSIGN vMeat = FILL("x", ipMeatSize).

  DO vKey = 1 TO ipRecords
  TRANSACTION:
    CREATE theTable.
    ASSIGN theTable.TableKey  = vKey
           theTable.TableMeat = vMeat
    . /* ASSIGN */
  END. /* TRANSACTION */
END PROCEDURE. /* CreateRecords */

/* ------------------------------------------------------------------------- */
PROCEDURE DeleteRecords:

  DEFINE INPUT PARAMETER ipBaseKey AS INTEGER NO-UNDO.
 
  DEFINE BUFFER bufTable FOR theTable.

  DEFINE VARIABLE vKey   AS INTEGER NO-UNDO.
  DEFINE VARIABLE vRowid AS ROWID   NO-UNDO.

  FOR EACH theTable NO-LOCK:
    DISPLAY theTable.TableKey LABEL "Key".
  
    ASSIGN vRowid = ROWID(theTable)
           vKey   = theTable.TableKey
    . /* ASSIGN */
  
    DO TRANSACTION:
      FIND FIRST bufTable EXCLUSIVE-LOCK WHERE ROWID(bufTable) EQ vRowid
      NO-WAIT NO-ERROR.
    
      DISPLAY AVAILABLE(bufTable).
  
      IF AVAILABLE(bufTable) AND theTable.TableKey MODULO ipBaseKey EQ 1 THEN
      FOR FIRST bufTable EXCLUSIVE-LOCK
          WHERE bufTable.TableKey EQ vKey + 1:
        DELETE  bufTable.
      END. /* bufTable */
    END. /* TRANSACTION */
  END. /* theTable */
END PROCEDURE. /* DeleteRecords */

DF file:
Code:
ADD TABLE "theTable"
  AREA "Data Area"
  DUMP-NAME "theTable"

ADD FIELD "TableKey" OF "theTable" AS integer
  FORMAT ">>9"
  INITIAL "0"
  LABEL "Key"
  MAX-WIDTH 4
  ORDER 10

ADD FIELD "TableMeat" OF "theTable" AS character
  FORMAT "x(40)"
  INITIAL ""
  LABEL "Meat"
  MAX-WIDTH 16384
  ORDER 20

ADD INDEX "key" ON "theTable"
  AREA "Idx Area"
  PRIMARY
  INDEX-FIELD "TableKey" ASCENDING

.
PSC
cpstream=undefined
.
0000000427
 
Last edited:
The forum died and was resurrected, and this gave me time to study how the server and client communicate. I changed my mind - the behavior described in the first post is NOT a bug.

When client initiates the FOR EACH NO-LOCK query it sends two messages to its server:
First one is a description of query. The size of the message is 220 bytes.
Second one is the a "gimme more records" request. Its size is 108 bytes.

A server sends one message back to the client with one or the -prefetchNumRecs (64 by default in the recent Progress versions) records based on the -prefetchDelay parameter.

Client stores only one network message per query.
Network message includes the directory of the records.
When a client receives a new network message, it merges the directory from the message with the local buffer directory (-l). That is why if we delete a record that is not yet proceeded by our query we will not read the record as a part of query. That is why we did not see all deleted records as the phantom records.

When a server sends a network message to a client it immediately starts reading the next bunch of records until their total size exceed the -Mm. It will keep these records in its own cache until the next a "gimme more records" request from a client. The cache also stores the previously found records that didn't fit into the previous network message. The records in the server's cache can be larger than the -Mm.

Server does not know If any of the records in its cache are be deleted or updated after it read them. So it will send the outdated records to the clients. It’s what is going on in the example from the first post.

In most cases, the likelihood of such a scenario is negligible - the size of records under the risk is one or two -Mm. The time window is determined by how quickly the client will process the current network message. But program in the first post deletes the right records in the right time.



During my tests I used the _ActServer VST table. The main VST rule is "VST fields may return the values that mean something other than what the fields promise". It’s very true for _ActServer.

_Server-QryRec Queries received
In fact, it’s the number of the client’s “gimme more records" requests.

_Server-MsgSent Messages sent
Equates to _Server-QryRec

_Server-MsgRec Messages received
The difference between _Server-MsgRec and _Server-QryRec is the number of the messages with query descriptions.

_Server-RecSent Records sent
In fact, It’s the number of the records that server reads into the cache after it sent another message to a client. It’s not the number of the records sent to a client if a query is not yet finished.

Note: the reads of VST records is not included into _ActRecord or in any other “record reads” fields but they are counted in _Server-RecSent.

_Server-ByteSent Bytes sent
Total size of the records in the messages plus a few extra bytes:
16 bytes per record for tiny records;
30 bytes per record for records of about 3K in size.
I did not test the very large records.

_Server-ByteRec Bytes received
_Server-RecRec Records received
Records created by a client and sent to its server.

Note that It is a remote client that creates the recovery notes and each note is sent to a server as a separate network message. That is why it’s not efficient to update a database remotely.
 
Just in case if someone would like to run the tests.
You need to create a database with theTable and to create its records.
E.g. RUN CreateRecords(1000, 1) where CreateRecords is in the first post.

Connect the database twice from the same session:
once remotely (-H/-S) using the "rmt" logical name;
second connection through shared memory using the "shm" logical name.

Then run program below.
The simplified code:
Code:
FOR EACH rmt.theTable NO-LOCK:
  PAUSE 0.1.
  //check shm._ActServer
END.

Full version:
Code:
DEFINE VARIABLE vOutputFile AS CHARACTER NO-UNDO INITIAL "ActServer.txt".
&SCOPED-DEFINE Sep "~t":U
DEFINE VARIABLE vServerId  LIKE _ActServer._Server-Id        NO-UNDO.
DEFINE VARIABLE vMsgRec    LIKE _ActServer._Server-MsgRec    NO-UNDO EXTENT 3.
DEFINE VARIABLE vMsgSent   LIKE _ActServer._Server-MsgSent   NO-UNDO EXTENT 3.
DEFINE VARIABLE vByteRec   LIKE _ActServer._Server-ByteRec   NO-UNDO EXTENT 3.
DEFINE VARIABLE vByteSent  LIKE _ActServer._Server-ByteSent  NO-UNDO EXTENT 3.
DEFINE VARIABLE vRecSent   LIKE _ActServer._Server-RecSent   NO-UNDO EXTENT 3.
DEFINE VARIABLE vQryRec    LIKE _ActServer._Server-QryRec    NO-UNDO EXTENT 3.
DEFINE VARIABLE vTimeSlice LIKE _ActServer._Server-TimeSlice NO-UNDO EXTENT 3.
DEFINE VARIABLE vRecRead   LIKE _ActRecord._Record-RecRead   NO-UNDO EXTENT 3.
DEFINE VARIABLE vByteRead  LIKE _ActRecord._Record-BytesRead NO-UNDO EXTENT 3.
DEFINE VARIABLE vRecSize   AS INT64   NO-UNDO EXTENT 3.
DEFINE VARIABLE vByteAdd   AS INT64   NO-UNDO EXTENT 3.
DEFINE VARIABLE vRecCount  AS INT64   NO-UNDO.
DEFINE VARIABLE vPrev      AS INTEGER NO-UNDO.
DEFINE VARIABLE vCurr      AS INTEGER NO-UNDO.

DEFINE STREAM OutputFile.

FOR FIRST rmt._MyConnection NO-LOCK,

    FIRST shm._Connect NO-LOCK
    WHERE shm._Connect._Connect-Id EQ rmt._MyConnection._MyConn-UserId + 1,
    
    FIRST shm._Servers NO-LOCK
    WHERE shm._Servers._Server-Num EQ shm._Connect._Connect-Server
      AND shm._Servers._Server-Type EQ "auto":U,
      
    FIRST shm._ActRecord NO-LOCK,
    
    FIRST shm._ActServer NO-LOCK
    WHERE shm._ActServer._Server-Id EQ shm._Servers._Server-Id:
  ASSIGN vServerId  = shm._ActServer._Server-Id
         vMsgRec    = shm._ActServer._Server-MsgRec
         vMsgSent   = shm._ActServer._Server-MsgSent
         vByteRec   = shm._ActServer._Server-ByteRec
         vByteSent  = shm._ActServer._Server-ByteSent
         vRecSent   = shm._ActServer._Server-RecSent
         vQryRec    = shm._ActServer._Server-QryRec
         vTimeSlice = shm._ActServer._Server-TimeSlice
         vRecRead   = shm._ActRecord._Record-RecRead
         vByteRead  = shm._ActRecord._Record-BytesRead
         vByteAdd   = 0
         vRecSize   = 0
         vPrev      = 2
         vCurr      = 3
  . /* ASSIGN */
END.

OUTPUT STREAM OutputFile TO VALUE(vOutputFile).
PUT STREAM OutputFile UNFORMATTED
           "RecCount"
    {&Sep} "QryRec"    {&Sep} "Incr"
    {&Sep} "MsgRec"    {&Sep} "Incr"
    {&Sep} "ByteRec"   {&Sep} "Incr"
    {&Sep} "MsgSent"   {&Sep} "Incr"
    {&Sep} "RecRead"   {&Sep} "Incr"
    {&Sep} "RecSent"   {&Sep} "Incr"
    {&Sep} "TimeSlice" {&Sep} "Incr"
    {&Sep} "ByteRead"  {&Sep} "Incr"
    {&Sep} "ByteSent"  {&Sep} "Incr"
    {&Sep} "RecSize"   {&Sep} "Incr"
    {&Sep} "ByteAdd"   {&Sep} "Incr"
SKIP. 

PAUSE 0 BEFORE-HIDE.
FOR EACH rmt.theTable NO-LOCK:

  PAUSE 0.1 NO-MESSAGE.
 
/* Get stats: */ 
  FOR FIRST shm._ActRecord NO-LOCK,
      FIRST shm._ActServer NO-LOCK
      WHERE shm._ActServer._Server-Id EQ vServerId:
    ASSIGN vPrev = vCurr
           vCurr = 5 - vCurr
           vMsgRec   [vCurr] = shm._ActServer._Server-MsgRec
           vMsgSent  [vCurr] = shm._ActServer._Server-MsgSent
           vByteRec  [vCurr] = shm._ActServer._Server-ByteRec
           vByteSent [vCurr] = shm._ActServer._Server-ByteSent
           vRecSent  [vCurr] = shm._ActServer._Server-RecSent
           vQryRec   [vCurr] = shm._ActServer._Server-QryRec
           vTimeSlice[vCurr] = shm._ActServer._Server-TimeSlice
           vRecRead  [vCurr] = shm._ActRecord._Record-RecRead
           vByteRead [vCurr] = shm._ActRecord._Record-BytesRead
           vRecSize  [vCurr] = vRecSize [vPrev] + RECORD-LENGTH(rmt.theTable)
           vByteAdd  [vCurr] = vByteSent[vCurr] - vByteSent[1] - vRecSize[vCurr]
           vRecCount = vRecCount + 1
    . /* ASSIGN */
  END. /* shm._ActServer */

  PUT STREAM OutputFile UNFORMATTED
           vRecCount
    {&Sep} vQryRec   [vCurr] - vQryRec   [1] {&Sep} vQryRec   [vCurr] - vQryRec   [vPrev]
    {&Sep} vMsgRec   [vCurr] - vMsgRec   [1] {&Sep} vMsgRec   [vCurr] - vMsgRec   [vPrev]
    {&Sep} vByteRec  [vCurr] - vByteRec  [1] {&Sep} vByteRec  [vCurr] - vByteRec  [vPrev]
    {&Sep} vMsgSent  [vCurr] - vMsgSent  [1] {&Sep} vMsgSent  [vCurr] - vMsgSent  [vPrev]
    {&Sep} vRecRead  [vCurr] - vRecRead  [1] {&Sep} vRecRead  [vCurr] - vRecRead  [vPrev]
    {&Sep} vRecSent  [vCurr] - vRecSent  [1] {&Sep} vRecSent  [vCurr] - vRecSent  [vPrev]
    {&Sep} vByteRead [vCurr] - vByteRead [1] {&Sep} vByteRead [vCurr] - vByteRead [vPrev]
    {&Sep} vTimeSlice[vCurr] - vTimeSlice[1] {&Sep} vTimeSlice[vCurr] - vTimeSlice[vPrev]
    {&Sep} vByteSent [vCurr] - vByteSent [1] {&Sep} vByteSent [vCurr] - vByteSent [vPrev]
    {&Sep} vRecSize  [vCurr] - vRecSize  [1] {&Sep} vRecSize  [vCurr] - vRecSize  [vPrev]
    {&Sep} vByteAdd  [vCurr] - vByteAdd  [1] {&Sep} vByteAdd  [vCurr] - vByteAdd  [vPrev]
  SKIP.

  IF vRecCount MOD 10 EQ 0 THEN
  DISPLAY vRecCount FORMAT ">>>,>>9" LABEL "RecCount" WITH 1 DOWN SIDE-LABEL.

END. /* rmt.theTable */

OUTPUT STREAM OutputFile CLOSE.

ActServer.txt can be opened in Excel.
 
Back
Top