Skip to content

Committed empty generation is treated as No Data, breaking non-empty-to-empty RTR transitions #170

Description

@wangxin1115

Summary

During local testing of StayRTR, I found that a legitimate transition from a non-empty RTR dataset to an empty dataset is not represented coherently to RTR clients.

StayRTR successfully commits the empty generation and advances its internal serial, but both Reset Query and Serial Query then return Error Report: No Data (code 2) instead of serving the empty generation or the withdrawals required to transition from the previous non-empty state.

The observed sequence is:

G0: { VRP_A }
        |
        | normal update
        v
G1: empty
        |
        | G1 committed
        | serial advances from 0 to 1
        v
Reset Query  -> No Data
Serial Query -> No Data
No withdrawal for VRP_A

In a small stateful RTR client used as a local oracle, the previously learned VRP_A remained retained after the transition.

Tested revision

Commit:       4bad96343e87df9c8acbcb1d983d224c5dc114b6
Git describe: v0.6.4-1-g4bad963

All testing used valid RTR messages only. No StayRTR source modification was required.

Test scenario

The initial generation contained one VRP:

G0

ASN:       AS64510
Prefix:    192.0.2.0/24
MaxLength: 24

The next generation was a valid empty RTR dataset:

G1

VRPs:        0
Router Keys: 0
ASPAs:       0

The transition was therefore:

G0 = { VRP_A }
       |
       v
G1 = {}

Expected behavior

A current generation containing zero RTR objects should remain distinguishable from the state where no current generation exists.

For a fresh client, an empty current generation should be represented coherently.

For a client previously synchronized to G0, the transition to G1 should allow the previously learned VRP_A to be removed, for example through the corresponding withdrawal semantics.

Conceptually:

No current generation
        !=
Current generation exists, but contains zero objects

Actual behavior

1. True no-current-state control

Before any current generation was installed, a valid Reset Query returned:

Error Report
Error code: 2
Meaning: No Data

This established the baseline behavior for a genuine no-current-state condition.

2. Non-empty G0 works normally

After installing G0, a valid Reset Query returned:

Cache Response
IPv4 Prefix announcement:
  AS64510 192.0.2.0/24 maxLength 24
End Of Data:
  serial = 0

So the normal non-empty path worked correctly.

3. Empty G1 is committed

Without restarting StayRTR, I then performed a normal update from G0 to the empty G1.

The empty generation was committed successfully:

Object count:    0
Session:         unchanged
Internal serial: 1

The serial had therefore advanced from 0 to 1, while the current dataset became empty.

At this point, StayRTR internally had a committed current generation:

Current generation: G1
Object count:       0
Serial:             1
Session:            valid

4. Fresh Reset Query against G1 returns No Data

A fresh valid Reset Query against the committed empty G1 returned:

Error Report
Error code: 2
Meaning: No Data

No Cache Response was returned.

No End Of Data was returned.

Therefore, these two internally different states were externally treated the same way:

A. No current generation exists

B. A current generation exists,
   has a valid session,
   has an advanced serial,
   but contains zero RTR objects

5. Serial Query from the previous G0 state also returns No Data

A client previously synchronized to G0 had:

Session: current StayRTR session
Serial:  0
State:   { VRP_A }

After StayRTR committed empty G1 with internal serial 1, the client sent a valid Serial Query using the previous session and serial.

StayRTR returned:

Error Report
Error code: 2
Meaning: No Data

No withdrawal for VRP_A was emitted.

No coherent G0 -> G1 transition was provided.

Source-level cause

The issue appears to come from conflating current-state existence with current object count.

An empty generation can be committed into sdCurrent, and the internal serial can advance, but GetCurrentSerial() determines whether current data is valid using a condition equivalent to:

len(sdCurrent) > 0

This means the implementation does not correctly distinguish:

State A:
  No current generation exists

from:

State B:
  Current generation exists
  Valid session exists
  Serial exists and has advanced
  Current object count == 0

Once State B is reached:

G0 non-empty
    |
    v
G1 empty is accepted
    |
    v
G1 becomes current
    |
    v
serial advances from N to N+1
    |
    v
len(sdCurrent) == 0
    |
    v
GetCurrentSerial() reports no valid current data
    |
    +--> Reset Query  -> No Data
    |
    +--> Serial Query -> No Data

The empty current generation therefore exists internally but cannot be represented coherently through the RTR query paths.

Stateful client observation

I also used a small local stateful RTR client as an oracle.

Before the transition, the client successfully synchronized to G0 and held:

{ VRP_A }

StayRTR then transitioned normally to empty G1.

Because the subsequent Serial Query returned No Data and no withdrawal was emitted, the local stateful client still held:

{ VRP_A }

To be precise:

The old-VRP retention was directly observed in the local stateful client used for this test. Production downstream behavior may vary by RTR client implementation, because different clients may react differently to an Error Report carrying No Data.

The server-side behavior does not depend on that client-specific consequence:

  • G1 is committed;
  • the serial advances;
  • G1 contains zero objects;
  • Reset Query returns No Data;
  • Serial Query returns No Data;
  • no withdrawal is emitted.

Minimal reproduction

  1. Start StayRTR with a valid non-empty dataset containing one VRP.

  2. Send a valid Reset Query and confirm:

    Cache Response
    -> VRP_A announcement
    -> End Of Data(serial = N)
    
  3. Record the current session and serial N.

  4. Update StayRTR normally to a valid empty dataset.

  5. Confirm that:

    object count = 0
    internal serial = N + 1
    session remains valid
    
  6. Send a fresh valid Reset Query.

  7. Observe:

    Error Report: No Data(code=2)
    no Cache Response
    no End Of Data
    
  8. Send a valid Serial Query using the previous session and serial N.

  9. Observe:

    Error Report: No Data(code=2)
    no withdrawal for VRP_A
    

Why this matters

The core issue is that these two states are semantically different:

NO_CURRENT_STATE

and:

CURRENT_STATE_EXISTS_BUT_EMPTY

In the tested behavior, StayRTR has already committed the empty generation and advanced its serial, but valid RTR queries cannot retrieve a coherent representation of that generation.

For a client previously synchronized to a non-empty generation, the server also fails to provide the withdrawal transition needed to reach the empty state.

The directly demonstrated server-side impact is:

A legitimate non-empty-to-empty RTR state transition cannot be represented coherently after the empty generation has been committed.

The directly observed local downstream consequence is:

The previously learned VRP remained retained because the server returned No Data and emitted no withdrawal.

Production downstream behavior may vary by RTR client implementation.

Controls

Case Current state Object count Serial Reset Query Serial Query Withdrawal
P0 No current generation N/A N/A No Data N/A N/A
P1 Non-empty G0 1 0 Normal full response Normal N/A
Target Committed empty G1 0 1 No Data No Data None

The important differential is that the target behaves externally like the true no-current-state control even though G1 has been committed and the internal serial has advanced.

The directly demonstrated issue is the inability to represent a committed empty current generation coherently through Reset Query and Serial Query.

Possible fix direction

One possible direction would be to track current-generation existence independently from the number of RTR objects in sdCurrent.

A predicate such as:

len(sdCurrent) > 0

does not distinguish:

No current generation exists

from:

A valid current generation exists and contains zero objects

A regression test could use:

G0:
  one VRP_A

G1:
  empty

and verify that:

  1. G1 is committed.
  2. The serial advances.
  3. A fresh Reset Query can retrieve a coherent empty current generation.
  4. A Serial Query from G0 can reach G1 coherently.
  5. The previous VRP is withdrawn.
  6. The empty generation is not reported as No Data solely because its object count is zero.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions