/**
   * Tests that incremental metadata changes made on the master are visible (refreshed) when needed
   * on the replicas. Incremental metadata changes occur when not all metadata is known to the DPL
   * initially when the store is opened, and additional metadata is discovered as entities are
   * written. This is scenario A-1 in the [#16655] SR.
   *
   * <p>This is not actually a schema upgrade test, but is conveniently tested here using the
   * upgrade test framework.
   */
  @Test
  public void testIncrementalMetadataChanges() throws Exception {

    open();

    /* Master writes and reads Data entity. */
    masterApp.writeData(0);
    masterApp.readData(0);

    /* Replicas read Data entity. */
    replicaApp1.readData(0);
    replicaApp2.readData(0);

    /* Master writes DataA (subclass), causing a metadata update. */
    masterApp.writeDataA(1);
    masterApp.readDataA(1);

    /* Replicas read DataA and must refresh metadata. */
    replicaApp1.readDataA(1);
    replicaApp2.readDataA(1);

    /* Read Data again for good measure. */
    masterApp.readData(0);
    replicaApp1.readData(0);
    replicaApp2.readData(0);

    close();
  }
  /**
   * Tests that when a replica having stale metadata is elected master, the first metadata update on
   * the new master causes refresh of the stale metadata before the new metadata is written. This is
   * scenario A-2 in the [#16655] SR.
   *
   * <p>This is not actually a schema upgrade test, but is conveniently tested here using the
   * upgrade test framework.
   */
  @Test
  public void testElectedMasterWithStaleMetadata() throws Exception {

    open();

    /* Master writes and reads Data entity. */
    masterApp.writeData(0);
    masterApp.readData(0);

    /* Replicas read Data entity. */
    replicaApp1.readData(0);
    replicaApp2.readData(0);

    /* Master writes DataA (subclass), causing a metadata update. */
    masterApp.writeDataA(1);
    masterApp.readDataA(1);

    /*
     * Master is bounced (but not upgraded), replica1 switches roles with
     * master.
     */
    bounceMaster(0);

    /*
     * Master writes DataB, which requires a metadata change.  Before this
     * new metadata change, it must refresh metadata from disk to get the
     * definition of DataA.
     */
    masterApp.writeDataB(2);

    /*
     * Reading DataA would cause a ClassCastException if refresh did not
     * occur above, because the format ID for DataA would be incorrect.
     */
    masterApp.readDataA(1);

    /* Read all again for good measure. */
    masterApp.readData(0);
    masterApp.readDataA(1);
    masterApp.readDataB(2);
    replicaApp1.readData(0);
    replicaApp1.readDataA(1);
    replicaApp1.readDataB(2);
    replicaApp2.readData(0);
    replicaApp2.readDataA(1);
    replicaApp2.readDataB(2);

    close();
  }
  /**
   * Tests that a reasonable exception occurs when an upgraded node is elected Master *before* all
   * other nodes have been upgraded. This is a user error, since the Master election should occur
   * last, but it cannot always be avoided, for example, when an unexpected failover occurs during
   * the upgrade process.
   *
   * <p>There are two cases: (1) when the non-upgraded Replica node is already running when an
   * upgraded node becomes Master, and (2) when the non-upgraded node is brought up as a Replica in
   * a group with an upgraded Master. However, implementation-wise case (1) becomes case (2),
   * because in case (1) the Replica will attempt to refresh metadata, which is internally the same
   * as bringing up the Replica from scratch. In both case we instantiate a new PersistCatalog
   * internally, and run class evolution. This should result in an IncompatibleClassException.
   */
  @Test
  public void testPrematureUpgradedMaster() throws Exception {

    open();

    /* Master writes v0 entity, all nodes read. */
    masterApp.writeData(0);
    masterApp.readData(0);
    replicaApp1.readData(0);
    replicaApp2.readData(0);

    /* Replica2 is upgraded to v1, then Master is upgraded to v1. */
    bounceReplica2(1);
    bounceMaster(1);

    /* Replica2 and Replica1 were swapped when the Master was bounced. */
    assertEquals(1, masterApp.getVersion());
    assertEquals(1, replicaApp1.getVersion());
    assertEquals(0, replicaApp2.getVersion());

    /* Write a v1 entity on the Master. */
    try {
      masterApp.writeData(10);
      fail();
    } catch (DatabasePreemptedException expected) {
      masterApp.close();
      masterApp.open(masterEnv);
      masterApp.writeData(10);
    }

    /* The upgraded replica can read the v1 entity. */
    try {
      replicaApp1.readData(10);
      fail();
    } catch (DatabasePreemptedException expected) {
      replicaApp1.close();
      replicaApp1.open(replicaEnv1);
      replicaApp1.readData(10);
    }

    /* The non-upgraded replica will get IncompatibleClassException. */
    try {
      replicaApp2.readData(10);
      fail();
    } catch (DatabasePreemptedException expected) {
      replicaApp2.close();
      try {
        replicaApp2.open(replicaEnv2);
        fail();
      } catch (IncompatibleClassException expected2) {
      }
    }

    /* When finally upgraded, the replica can read the v1 entity. */
    bounceReplica2(1);
    replicaApp2.readData(10);

    /* Read all for good measure. */
    masterApp.readData(0);
    masterApp.readData(10);
    replicaApp1.readData(0);
    replicaApp1.readData(10);
    replicaApp2.readData(0);
    replicaApp2.readData(10);

    close();
  }
  /**
   * Ensure that when the master is bounced, the first write will refresh metadata. The testUpgrade
   * method, OTOH, ensures that metadata is refreshed when new indexes are requested.
   */
  @Test
  public void testRefreshAfterFirstWrite() throws Exception {

    open();

    /* Master writes v0 entity, all nodes read. */
    masterApp.writeData(0);
    masterApp.readData(0);
    replicaApp1.readData(0);
    replicaApp2.readData(0);

    /* Replica1 is upgraded to v1, upgrades metadata in memory. */
    bounceReplica1(1);

    /* Upgraded replica1 reads v0 entities, can't get new index. */
    try {
      replicaApp1.readData(0);
      fail();
    } catch (IndexNotAvailableException e) {
    }

    /* Replica2 (not yet upgraded) reads v0 entity without errors. */
    replicaApp2.readData(0);

    /* Replica2 is upgraded to v1, upgrades metadata in memory. */
    bounceReplica2(1);

    /* Read again on master for good measure. */
    masterApp.readData(0);

    /* Master is upgraded to v1, replica1 switches roles with master. */
    bounceMaster(1);

    /* Metadata is refreshed on first write. */
    try {
      masterApp.writeData(10);
      fail();
    } catch (DatabasePreemptedException expected) {
      masterApp.close();
      masterApp.open(masterEnv);
      masterApp.writeData(10);
    }

    /* Replicas also get DatabasePreemptedException on first read. */
    try {
      replicaApp1.readData(0);
      fail();
    } catch (DatabasePreemptedException expected) {
      replicaApp1.close();
      replicaApp1.open(replicaEnv1);
      replicaApp1.readData(0);
    }
    try {
      replicaApp2.readData(0);
      fail();
    } catch (DatabasePreemptedException expected) {
      replicaApp2.close();
      replicaApp2.open(replicaEnv2);
      replicaApp2.readData(0);
    }

    /* All reads now work. */
    masterApp.readData(0);
    masterApp.readData(10);
    replicaApp1.readData(0);
    replicaApp1.readData(10);
    replicaApp2.readData(0);
    replicaApp2.readData(10);

    close();
  }
  /** Tests scenarios B-1 and B-2 in the [#16655] SR. */
  @Test
  public void testUpgrade() throws Exception {

    open();

    /* Master writes and reads v0 entities. */
    masterApp.writeData(0);
    masterApp.writeDataA(1);
    masterApp.writeDataB(2);
    masterApp.readData(0);
    masterApp.readDataA(1);
    masterApp.readDataB(2);

    /* Replicas read v0 entities. */
    replicaApp1.readData(0);
    replicaApp1.readDataA(1);
    replicaApp1.readDataB(2);
    replicaApp2.readData(0);
    replicaApp2.readDataA(1);
    replicaApp2.readDataB(2);

    /* Replica1 is upgraded to v1, upgrades metadata in memory. */
    bounceReplica1(1);

    /* Upgraded replica1 reads v0 entities, can't get new index. */
    try {
      replicaApp1.readData(0);
      fail();
    } catch (IndexNotAvailableException e) {
    }
    try {
      replicaApp1.readDataB(2);
      fail();
    } catch (IndexNotAvailableException e) {
    }

    /* Upgraded replica1 can't get index for new entity NewData2. */
    try {
      replicaApp1.readData2(14);
      fail();
    } catch (IndexNotAvailableException e) {
    }

    /* Replica1 can read v0 DataA, because it has no new indexes. */
    replicaApp1.readDataA(1);

    /* Replica2 (not yet upgraded) reads v0 entities without errors. */
    replicaApp2.readData(0);
    replicaApp2.readDataA(1);
    replicaApp2.readDataB(2);

    /* Replica2 is upgraded to v1, upgrades metadata in memory. */
    bounceReplica2(1);

    /* Upgraded replicas read v0 entities, can't get new index. */
    try {
      replicaApp1.readData(0);
      fail();
    } catch (IndexNotAvailableException e) {
    }
    try {
      replicaApp1.readDataB(2);
      fail();
    } catch (IndexNotAvailableException e) {
    }
    try {
      replicaApp2.readData(0);
      fail();
    } catch (IndexNotAvailableException e) {
    }
    try {
      replicaApp2.readDataB(2);
      fail();
    } catch (IndexNotAvailableException e) {
    }

    /* Upgraded replicas can't get index for new entity NewData2. */
    try {
      replicaApp1.readData2(14);
      fail();
    } catch (IndexNotAvailableException e) {
    }
    try {
      replicaApp2.readData2(14);
      fail();
    } catch (IndexNotAvailableException e) {
    }

    /* Upgraded replicas can read v0 DataA, it has no new indexes. */
    replicaApp1.readDataA(1);
    replicaApp2.readDataA(1);

    /* Read again on master for good measure. */
    masterApp.readData(0);
    masterApp.readDataA(1);
    masterApp.readDataB(2);

    /* Master is upgraded to v1, replica1 switches roles with master. */
    bounceMaster(1);

    /* Metadata is refreshed when new indexes are requested. */
    try {
      masterApp.readData(0);
      fail();
    } catch (DatabasePreemptedException expected) {
      masterApp.close();
      masterApp.open(masterEnv);
      masterApp.readData(0);
    }
    masterApp.readDataA(1);
    masterApp.readDataB(2);
    try {
      replicaApp1.readData(0);
      fail();
    } catch (DatabasePreemptedException expected) {
      replicaApp1.close();
      replicaApp1.open(replicaEnv1);
      replicaApp1.readData(0);
    }
    replicaApp1.readDataA(1);
    replicaApp1.readDataB(2);
    try {
      replicaApp2.readData(0);
      fail();
    } catch (DatabasePreemptedException expected) {
      replicaApp2.close();
      replicaApp2.open(replicaEnv2);
      replicaApp2.readData(0);
    }
    replicaApp2.readDataA(1);
    replicaApp2.readDataB(2);

    /* Master writes v1 entities. */
    masterApp.writeData(10);
    masterApp.writeDataA(11);
    masterApp.writeDataB(12);

    /* Master reads v0 and v1 entities. */
    masterApp.readData(0);
    masterApp.readData(10);
    masterApp.readDataA(1);
    masterApp.readDataA(11);
    masterApp.readDataB(2);
    masterApp.readDataB(12);

    /* Replicas read v0 and v1 entities. */
    replicaApp1.readData(0);
    replicaApp1.readData(10);
    replicaApp1.readDataA(1);
    replicaApp1.readDataA(11);
    replicaApp1.readDataB(2);
    replicaApp1.readDataB(12);
    replicaApp2.readData(0);
    replicaApp2.readData(10);
    replicaApp2.readDataA(1);
    replicaApp2.readDataA(11);
    replicaApp2.readDataB(2);
    replicaApp2.readDataB(12);

    /* Master writes new NewDataC subclass, all can read. */
    masterApp.writeDataC(13);
    masterApp.readDataC(13);
    replicaApp1.readDataC(13);
    replicaApp2.readDataC(13);

    /* Master writes new NewData2 entity class, all can read. */
    masterApp.writeData2(14);
    masterApp.readData2(14);
    replicaApp1.readData2(14);
    replicaApp2.readData2(14);

    close();
  }