/** * Bounce the master, causing replica1 to switch roles with the master. If a higher appVersion is * specified, the bounced node will also be upgraded. */ private void bounceMaster(final int appVersion) throws Exception { /* Disable updates to RepGroupDB due to LocalCBVLSN updates. */ LocalCBVLSNUpdater.setSuppressGroupDBUpdates(true); for (RepEnvInfo info : repEnvInfo) { if (info.getEnv() == masterEnv) { /* * Sync up the replication group so that node2 doesn't do * hard recovery. */ RepTestUtils.syncGroupToLastCommit(repEnvInfo, repEnvInfo.length); /* Disable replay on replicas. */ shutdownFeeder(info.getRepNode(), replicaEnv1); shutdownFeeder(info.getRepNode(), replicaEnv2); /* Close the master. */ masterApp.close(); masterApp = null; info.closeEnv(); masterEnv = null; /* Force repEnvInfo[2] to the master. */ WaitForMasterListener masterWaiter = new WaitForMasterListener(); replicaEnv2.setStateChangeListener(masterWaiter); RepNode repNode = repEnvInfo[2].getRepNode(); repNode.forceMaster(true); /* Enable the LocalCBVLSN updates. */ LocalCBVLSNUpdater.setSuppressGroupDBUpdates(false); masterWaiter.awaitMastership(); assertTrue(repNode.isMaster()); masterEnv = replicaEnv2; /* Replica2 was elected, swap names with replica1. */ final ReplicatedEnvironment tmpEnv = replicaEnv1; replicaEnv1 = replicaEnv2; replicaEnv2 = tmpEnv; final AppInterface tmpApp = replicaApp1; replicaApp1 = replicaApp2; replicaApp2 = tmpApp; /* Replica1 (or 2, see above) has been elected master. */ masterApp = newAppObject(appVersion); masterApp.adopt(replicaApp1); /* Former master (just upgraded) becomes replica1. */ replicaEnv1 = info.openEnv(); replicaApp1.open(replicaEnv1); break; } } assertNotNull(masterApp); assertSame(masterEnv.getState(), ReplicatedEnvironment.State.MASTER); }
/** * Creates an instance of the specified class that implements AppInterface, and returns a Proxy to * the instance. The returned Proxy invokes all methods of the target instance in the context of * the ClassLoader of the specified class. */ private AppInterface newAppObject(final int appVersion) throws Exception { AppInterface app = (AppInterface) Proxy.newProxyInstance( AppInterface.class.getClassLoader(), new Class[] {AppInterface.class}, new MyInvocationHandler(appClasses[appVersion].newInstance())); app.setVersion(appVersion); app.setInitDuringOpen(doInitDuringOpen); return app; }
/** * Bounce replica2. No election will take place. If a higher appVersion is specified, the bounced * node will also be upgraded. */ private void bounceReplica2(final int appVersion) throws Exception { replicaApp2.close(); replicaApp2 = null; for (RepEnvInfo info : repEnvInfo) { if (info.getEnv() == replicaEnv2) { info.closeEnv(); replicaEnv2 = info.openEnv(); replicaApp2 = newAppObject(appVersion); replicaApp2.open(replicaEnv2); break; } } assertNotNull(replicaApp2); assertSame(masterEnv.getState(), ReplicatedEnvironment.State.MASTER); }
private void close(boolean normalShutdown) { try { if (normalShutdown) { replicaApp1.close(); replicaApp2.close(); masterApp.close(); RepTestUtils.shutdownRepEnvs(repEnvInfo); } else { for (RepEnvInfo info : repEnvInfo) { info.abnormalCloseEnv(); } } } finally { repEnvInfo = null; masterEnv = null; replicaEnv1 = null; replicaEnv2 = null; masterApp = null; replicaApp1 = null; replicaApp2 = null; } }
/** Creates a 3 node group and initializes the app classes. */ private void open() throws Exception { /* * ReplicaAckPolicy.ALL is used to ensure that when a master operation * is committed, the change is immediately available on the replica for * testing -- no waiting in the test is needed. */ repEnvInfo = RepTestUtils.setupEnvInfos( envRoot, 3, RepTestUtils.createEnvConfig( new Durability( Durability.SyncPolicy.WRITE_NO_SYNC, Durability.SyncPolicy.WRITE_NO_SYNC, Durability.ReplicaAckPolicy.ALL)), new ReplicationConfig()); masterEnv = RepTestUtils.joinGroup(repEnvInfo); replicaEnv1 = repEnvInfo[1].getEnv(); replicaEnv2 = repEnvInfo[2].getEnv(); /* Load app classes with custom class loader. */ final File evolveParentDir = new File(System.getProperty("testevolvedir")); final ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader(); for (int i = 0; i < N_APP_VERSIONS; i += 1) { final ClassLoader myLoader = new SimpleClassLoader(parentClassLoader, new File(evolveParentDir, "dplUpgrade." + i)); appClasses[i] = Class.forName(APP_IMPL, true /*initialize*/, myLoader); } /* Open v0 app objects. */ masterApp = newAppObject(0); masterApp.open(masterEnv); replicaApp1 = newAppObject(0); replicaApp1.open(replicaEnv1); replicaApp2 = newAppObject(0); replicaApp2.open(replicaEnv2); }
/** * Tests a somewhat contrived upgrade scenario with persistent classes A and B that can be * embedded in an entity without changing the entity class definition or version. For example, if * a field of type Animal has subclasses Cat and Dog, these subclasses will not be known to the * DPL until an instance of them is stored. When a new subclass is defined in the app, a metadata * upgrade will not occur until an instance is stored. * * <p>The contrived scenario is: * * <p>+ App initially contains persistent class A, but no instance of A is stored. Neither the * Master nor Replica DPL is aware of class A. * * <p>+ App is modified to contain new persistent class B. * * <p>+ Replica is upgraded, but does not enter Replica Upgrade Mode because class A and B are not * yet used or referenced. * * <p>+ Master writes record containing instance of A, but Replica does not read it. Replica * metadata is stale. It so happens that A is assigned format ID 200 by the Master. * * <p>+ Master is bounced and former Replica is elected Master. * * <p>+ New Master attempts to write record with instances of B, and updates its metadata, and * happens to assign B format ID 200. If the metadata previously written by the old Master is * overwritten, then metadata corruption will occur. Class A and B will have been assigned the * same format ID. * * <p>+ When any node reads record containing A, some sort of exception will probably occur as a * result of the incorrect format ID, or perhaps incorrect data will be returned. * * <p>This problem should be avoided because the new Master should detect that its metadata is * stale, and refresh it prior to writing the new metadata. It detects this, even though it is not * in Replica Upgrade Mode, by reading the metadata before updating it, and checking to see if it * has changed since it was last read. See PersistCatalog.writeDataCheckStale. * * <p>The scenario is contrived in the sense that it is very unlikely that an instance of A will * happen to be written by the Master for the very first time during the upgrade process. But it * does provide a test case for the detection of metadata changes in PersistCatalog.writeData. */ @Test public void testRefreshBeforeWrite() throws Exception { doInitDuringOpen = false; open(); masterApp.insertNullAnimal(); replicaApp1.readNullAnimal(); replicaApp2.readNullAnimal(); /* Replicas upgraded to v1 but do not enter Replica Upgrade Mode. */ bounceReplica1(1); bounceReplica2(1); assertFalse(replicaApp1.getStore().isReplicaUpgradeMode()); assertFalse(replicaApp2.getStore().isReplicaUpgradeMode()); masterApp.insertDogAnimal(); bounceMaster(1); masterApp.insertCatAnimal(); masterApp.readDogAnimal(); masterApp.readCatAnimal(); replicaApp1.readDogAnimal(); replicaApp1.readCatAnimal(); replicaApp2.readDogAnimal(); replicaApp2.readCatAnimal(); 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(); }
/** * 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 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(); }