Skip to content

A replica which reconnects after a restore can be declared up to date and never sent the change it was restored past (ReSyncTest.testResyncAfterRestore) #963

Description

@vharseko

Describe the bug

ReSyncTest.testResyncAfterRestore failed in CI: the entry added between the backup and the
restore never came back, and the 30 s the test waits went by with nothing happening at all — no
error, no replay attempt, no message on the wire.

ReSyncTest.testResyncAfterRestore:167->ReplicationTestCase.getEntry:759
  Expected entry with dn "dc=fooUniqueName1,dc=example,dc=com" would exist expected [true] but found [false]

The whole sequence of the test fell inside one second (logs/errors of the failing run):

10:03:55 Backup task ... finished execution in the state Completed successfully
10:03:55 testResyncAfterRestore: backup done
10:03:55 testResyncAfterRestore: entry added
10:03:55 Restore task ... started execution
10:03:55 The backend userRoot is now taken offline
10:03:55 Restored backup file: 00000000.jdb (size 58002)
10:03:55 The database backend userRoot containing 3 entries has started
10:03:55 Directory server DS(123) has connected to replication server RS(104) for domain
         "dc=example,dc=com" at 127.0.0.1:65531 with generation ID 8969
10:03:55 testResyncAfterRestore: restore done
         <30 seconds of silence>
10:04:25 failed to wait for org.opends.server.util.TestTimer$SteppingTimer$1

The replica did announce a state behind the change. From logs/audit, the ds-sync-state of
dc=example,dc=com after the add and after the restore:

replace: ds-sync-state
ds-sync-state: 000001a0807967c2007b00000004   <- after the add of dc=fooUniqueName1
...
replace: ds-sync-state
ds-sync-state: 000001a080796545007b00000002   <- reloaded from the restored backend

So enable()loadDataState() did what it is meant to do, and DS(123) reconnected asking for
everything after seqnum 2 while the change it needed is seqnum 4.

The replication server had already decided there was nothing to send. The thread dump taken at
the failure:

"Replication server RS(104) writing to Replica DS(123) for domain "dc=example,dc=com" at localhost/127.0.0.1:56934"
  daemon TIMED_WAITING on org.opends.server.replication.server.MsgQueue@442cca58
	at org.opends.server.replication.server.MessageHandler.getNextMessage(MessageHandler.java:390)
	at org.opends.server.replication.server.ServerHandler.take(ServerHandler.java:991)
	at org.opends.server.replication.server.ServerWriter.run(ServerWriter.java:100)

MessageHandler.java:390 is the msgQueue.wait(500) inside if (following). The handler for the
just-reconnected replica was already following — i.e. it had read the changelog once, found
nothing after the announced state, and concluded the replica was up to date.

Why it matters

following is a one-way door for a change that is already in the changelog:

// MessageHandler.java:302
boolean queueContributesToDomainState = fillLateQueue();
if (lateQueue.isEmpty())
{
  // we could not find any messages in the changelog
  // so the remote server is not late anymore.
  synchronized (msgQueue)
  {
    if (isMsgQueueBelowThreshold())
    {
      following = true;
    }
  }
}

fillLateQueue() is the only reader of the changelog (getCursorFrom(serverState),
MessageHandler.java:429). Once following is set, the handler serves nothing but what add()
puts on its queue, and add() only ever carries changes published after the handler started
following. Nothing re-reads the changelog, so a change missed at that one moment is not sent again
— short of the queue overflowing, which is the single place that clears the flag
(MessageHandler.java:141).

This is not only a test problem. The same window is what a real replica depends on: a server that
restores a backup gets the changes it was restored past only if the changelog cursor, opened once
at the reconnect, yields them. If it comes back empty the replica silently stays short of those
changes, and the replication server considers it in sync.

What to look into

Why fillLateQueue() came back empty while the announced state was demonstrably behind. Two
candidates:

  1. The change was not yet visible to a cursor opened right then. The add, the backend going
    offline, the restore and the reconnect all happened inside the same second, so the cursor was
    opened on a changelog record written milliseconds earlier — worth checking what the file
    changelog guarantees between ReplicationServerDomain.put() and a subsequent
    getCursorFrom().
  2. The start position for the replica's own serverId in this path — the change to be resent
    here originates from the very server the handler serves, which is the unusual part of
    resync-after-restore compared to ordinary catch-up.

Whatever the cause turns out to be, treating "the changelog gave me nothing" as final is what makes
a transient miss permanent: a handler whose queue stays empty while the state it was given is
behind the domain state could read the changelog again instead of waiting forever.

Evidence

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugconcurrencyThread-safety / race-condition bugsdata-lossData integrity / loss of entriesreplicationtestsTest suites: fixing, enabling, un-disabling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions