package org.epics.archiverappliance.reshard;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.File;
import java.io.StringWriter;
import java.net.URLEncoder;
import java.sql.Timestamp;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import org.epics.archiverappliance.Event;
import org.epics.archiverappliance.EventStream;
import org.epics.archiverappliance.SIOCSetup;
import org.epics.archiverappliance.StoragePlugin;
import org.epics.archiverappliance.TomcatSetup;
import org.epics.archiverappliance.common.BasicContext;
import org.epics.archiverappliance.common.TimeUtils;
import org.epics.archiverappliance.config.ArchDBRTypes;
import org.epics.archiverappliance.config.ConfigServiceForTests;
import org.epics.archiverappliance.config.PVTypeInfo;
import org.epics.archiverappliance.config.StoragePluginURLParser;
import org.epics.archiverappliance.data.ScalarValue;
import org.epics.archiverappliance.engine.membuf.ArrayListEventStream;
import org.epics.archiverappliance.retrieval.RemotableEventStreamDesc;
import org.epics.archiverappliance.retrieval.client.RawDataRetrievalAsEventStream;
import org.epics.archiverappliance.utils.simulation.SimulationEvent;
import org.epics.archiverappliance.utils.ui.GetUrlContent;
import org.epics.archiverappliance.utils.ui.JSONDecoder;
import org.epics.archiverappliance.utils.ui.JSONEncoder;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
/**
* Simple test to test resharding a PV from one appliance to another...
* <ul>
* <li>Bring up a cluster of two appliances.</li>
* <li>Archive the PV and wait for it to connect etc.</li>
* <li>Determine the appliance for the PV.</li>
* <li>Generate data for a PV making sure we have more than one data source and more than one chunk.</li>
* <li>Pause the PV.</li>
* <li>Reshard to the other appliance. </li>
* <li>Resume the PV.</li>
* <li>Check for data loss and resumption of archiving etc,</li>
* </ul>
*
* This test will probably fail at the beginning of the year; we generate data into MTS and LTS and if there is an overlap we get an incorrect number of events.
*
* @author mshankar
*
*/
public class BasicReshardingTest {
private static Logger logger = Logger.getLogger(BasicReshardingTest.class.getName());
private String pvName = "UnitTestNoNamingConvention:sine";
private ConfigServiceForTests configService;
TomcatSetup tomcatSetup = new TomcatSetup();
SIOCSetup siocSetup = new SIOCSetup();
WebDriver driver;
String folderSTS = ConfigServiceForTests.getDefaultShortTermFolder() + File.separator + "reshardSTS";
String folderMTS = ConfigServiceForTests.getDefaultPBTestFolder() + File.separator + "reshardMTS";
String folderLTS = ConfigServiceForTests.getDefaultPBTestFolder() + File.separator + "reshardLTS";
@Before
public void setUp() throws Exception {
configService = new ConfigServiceForTests(new File("./bin"));
System.getProperties().put("ARCHAPPL_SHORT_TERM_FOLDER", folderSTS);
System.getProperties().put("ARCHAPPL_MEDIUM_TERM_FOLDER", folderMTS);
System.getProperties().put("ARCHAPPL_LONG_TERM_FOLDER", folderLTS);
FileUtils.deleteDirectory(new File(folderSTS));
FileUtils.deleteDirectory(new File(folderMTS));
FileUtils.deleteDirectory(new File(folderLTS));
siocSetup.startSIOCWithDefaultDB();
tomcatSetup.setUpClusterWithWebApps(this.getClass().getSimpleName(), 2);
driver = new FirefoxDriver();
}
@After
public void tearDown() throws Exception {
driver.quit();
tomcatSetup.tearDown();
siocSetup.stopSIOC();
FileUtils.deleteDirectory(new File(folderSTS));
FileUtils.deleteDirectory(new File(folderMTS));
FileUtils.deleteDirectory(new File(folderLTS));
}
@Test
public void testReshardPV() throws Exception {
// This section is straight from the ArchivePVTest
// Let's archive the PV and wait for it to connect.
driver.get("http://localhost:17665/mgmt/ui/index.html");
WebElement pvstextarea = driver.findElement(By.id("archstatpVNames"));
pvstextarea.sendKeys(pvName);
WebElement archiveButton = driver.findElement(By.id("archstatArchive"));
logger.debug("About to submit");
archiveButton.click();
// We have to wait for some time here as it does take a while for the workflow to complete.
Thread.sleep(4*60*1000);
WebElement checkStatusButton = driver.findElement(By.id("archstatCheckStatus"));
checkStatusButton.click();
Thread.sleep(2*1000);
WebElement statusPVName = driver.findElement(By.cssSelector("#archstatsdiv_table tr:nth-child(1) td:nth-child(1)"));
String pvNameObtainedFromTable = statusPVName.getText();
assertTrue("PV Name is not " + pvName + "; instead we get " + pvNameObtainedFromTable, pvName.equals(pvNameObtainedFromTable));
WebElement statusPVStatus = driver.findElement(By.cssSelector("#archstatsdiv_table tr:nth-child(1) td:nth-child(2)"));
String pvArchiveStatusObtainedFromTable = statusPVStatus.getText();
String expectedPVStatus = "Being archived";
assertTrue("Expecting PV archive status to be " + expectedPVStatus + "; instead it is " + pvArchiveStatusObtainedFromTable, expectedPVStatus.equals(pvArchiveStatusObtainedFromTable));
PVTypeInfo typeInfoBeforePausing = getPVTypeInfo();
// We determine the appliance for the PV by getting it's typeInfo.
String applianceIdentity = typeInfoBeforePausing.getApplianceIdentity();
assertTrue("Cannot determine appliance identity for pv from typeinfo ", applianceIdentity != null);
// We use the PV's PVTypeInfo creation date for moving data. This PVTypeInfo was just created.
// We need to fake this to an old value so that the data is moved correctly.
// The LTS data spans 2 years, so we set a creation time of about 4 years ago.
typeInfoBeforePausing.setCreationTime(TimeUtils.getStartOfYear(TimeUtils.getCurrentYear() - 4));
String updatePVTypeInfoURL = "http://localhost:17665/mgmt/bpl/putPVTypeInfo?pv=" + URLEncoder.encode(pvName, "UTF-8") + "&override=true";
GetUrlContent.postObjectAndGetContentAsJSONObject(updatePVTypeInfoURL, JSONEncoder.getEncoder(PVTypeInfo.class).encode(typeInfoBeforePausing));
Timestamp beforeReshardingCreationTimedstamp = typeInfoBeforePausing.getCreationTime();
// Generate some data into the MTS and LTS
String[] dataStores = typeInfoBeforePausing.getDataStores();
assertTrue("Data stores is null or empty for pv from typeinfo ", dataStores != null && dataStores.length > 1);
for(String dataStore : dataStores) {
logger.info("Data store for pv " + dataStore);
StoragePlugin plugin = StoragePluginURLParser.parseStoragePlugin(dataStore, configService);
String name = plugin.getName();
if(name.equals("MTS")) {
// For the MTS we generate a couple of days worth of data
Timestamp startOfMtsData = TimeUtils.minusDays(TimeUtils.now(), 3);
long startOfMtsDataSecs = TimeUtils.convertToEpochSeconds(startOfMtsData);
ArrayListEventStream strm = new ArrayListEventStream(0, new RemotableEventStreamDesc(ArchDBRTypes.DBR_SCALAR_DOUBLE, pvName, TimeUtils.convertToYearSecondTimestamp(startOfMtsDataSecs).getYear()));
for(long offsetSecs = 0; offsetSecs < 2*24*60*60; offsetSecs += 60) {
strm.add(new SimulationEvent(TimeUtils.convertToYearSecondTimestamp(startOfMtsDataSecs + offsetSecs), ArchDBRTypes.DBR_SCALAR_DOUBLE, new ScalarValue<Double>((double)offsetSecs)));
}
try(BasicContext context = new BasicContext()) {
plugin.appendData(context, pvName, strm);
}
} else if(name.equals("LTS")) {
// For the LTS we generate a couple of years worth of data
long startofLtsDataSecs = TimeUtils.getStartOfYearInSeconds(TimeUtils.getCurrentYear() - 2);
ArrayListEventStream strm = new ArrayListEventStream(0, new RemotableEventStreamDesc(ArchDBRTypes.DBR_SCALAR_DOUBLE, pvName, TimeUtils.convertToYearSecondTimestamp(startofLtsDataSecs).getYear()));
for(long offsetSecs = 0; offsetSecs < 2*365*24*60*60; offsetSecs += 24*60*60) {
strm.add(new SimulationEvent(TimeUtils.convertToYearSecondTimestamp(startofLtsDataSecs + offsetSecs), ArchDBRTypes.DBR_SCALAR_DOUBLE, new ScalarValue<Double>((double)offsetSecs)));
}
try(BasicContext context = new BasicContext()) {
plugin.appendData(context, pvName, strm);
}
}
}
logger.info("Done generating data. Now making sure the setup is correct by fetching some data.");
// Get the number of events before resharding...
long eventCount = getNumberOfEvents();
long expectedMinEventCount = 2*24*60 + 2*365;
logger.info("Got " + eventCount + " events");
assertTrue("Expecting at least " + expectedMinEventCount + " got " + eventCount + " for ", eventCount >= expectedMinEventCount);
String otherAppliance = "appliance1";
if(applianceIdentity.equals(otherAppliance)) {
otherAppliance = "appliance0";
}
// Let's pause the PV.
String pausePVURL = "http://localhost:17665/mgmt/bpl/pauseArchivingPV?pv=" + URLEncoder.encode(pvName, "UTF-8");
JSONObject pauseStatus = GetUrlContent.getURLContentAsJSONObject(pausePVURL);
assertTrue("Cannot pause PV", pauseStatus.containsKey("status") && pauseStatus.get("status").equals("ok"));
Thread.sleep(1000);
logger.info("Successfully paused the PV; other appliance is " + otherAppliance);
driver.get("http://localhost:17665/mgmt/ui/pvdetails.html?pv=" + pvName);
Thread.sleep(2*1000);
WebElement reshardPVButton = driver.findElement(By.id("pvDetailsReshardPV"));
logger.info("About to click on reshard button.");
reshardPVButton.click();
WebElement dialogOkButton = driver.findElement(By.id("pvReshardOk"));
logger.info("About to click on reshard ok button");
dialogOkButton.click();
Thread.sleep(5*60*1000);
WebElement pvDetailsTable = driver.findElement(By.id("pvDetailsTable"));
List<WebElement> pvDetailsTableRows = pvDetailsTable.findElements(By.cssSelector("tbody tr"));
for(WebElement pvDetailsTableRow : pvDetailsTableRows) {
WebElement pvDetailsTableFirstCol = pvDetailsTableRow.findElement(By.cssSelector("td:nth-child(1)"));
if(pvDetailsTableFirstCol.getText().contains("Instance archiving PV")) {
WebElement pvDetailsTableSecondCol = pvDetailsTableRow.findElement(By.cssSelector("td:nth-child(2)"));
String obtainedAppliance = pvDetailsTableSecondCol.getText();
String expectedAppliance = otherAppliance;
assertTrue("Expecting appliance to be " + expectedAppliance + "; instead it is " + obtainedAppliance, expectedAppliance.equals(obtainedAppliance));
break;
}
}
logger.info("Resharding UI is done.");
PVTypeInfo typeInfoAfterResharding = getPVTypeInfo();
String afterReshardingAppliance = typeInfoAfterResharding.getApplianceIdentity();
assertTrue("Invalid appliance identity after resharding " + afterReshardingAppliance, afterReshardingAppliance != null && afterReshardingAppliance.equals(otherAppliance));
Timestamp afterReshardingCreationTimedstamp = typeInfoAfterResharding.getCreationTime();
// Let's resume the PV.
String resumePVURL = "http://localhost:17665/mgmt/bpl/resumeArchivingPV?pv=" + URLEncoder.encode(pvName, "UTF-8");
JSONObject resumeStatus = GetUrlContent.getURLContentAsJSONObject(resumePVURL);
assertTrue("Cannot resume PV", resumeStatus.containsKey("status") && resumeStatus.get("status").equals("ok"));
long postReshardEventCount = getNumberOfEvents();
logger.info("After resharding, got " + postReshardEventCount + " events");
assertTrue("Expecting at least " + expectedMinEventCount + " got " + postReshardEventCount + " for ", postReshardEventCount >= expectedMinEventCount);
checkRemnantShardPVs();
// Make sure the creation timestamps are ok. If we have external integration, these play a part and you can not serve data because the creation timestamp is off
assertTrue("Creation timestamps before "
+ TimeUtils.convertToHumanReadableString(beforeReshardingCreationTimedstamp)
+ " and after "
+ TimeUtils.convertToHumanReadableString(afterReshardingCreationTimedstamp)
+ " should be the same", beforeReshardingCreationTimedstamp.equals(afterReshardingCreationTimedstamp));
}
private void checkRemnantShardPVs() {
// Make sure we do not have any temporary PV's present.
String tempReshardPVs = "http://localhost:17665/mgmt/bpl/getAllPVs?pv=*_reshard_*";
JSONArray reshardPVs = GetUrlContent.getURLContentAsJSONArray(tempReshardPVs);
StringWriter buf = new StringWriter();
for(Object reshardPV : reshardPVs) {
buf.append(reshardPV.toString());
buf.append(",");
}
assertTrue("We seem to have some reshard temporary PV's present " + buf.toString(), reshardPVs.size() == 0);
}
private PVTypeInfo getPVTypeInfo() throws Exception {
String getPVTypeInfoURL = "http://localhost:17665/mgmt/bpl/getPVTypeInfo?pv=" + URLEncoder.encode(pvName, "UTF-8");
JSONObject typeInfoJSON = GetUrlContent.getURLContentAsJSONObject(getPVTypeInfoURL);
assertTrue("Cannot get typeinfo for pv using " + getPVTypeInfoURL, typeInfoJSON != null);
PVTypeInfo unmarshalledTypeInfo = new PVTypeInfo();
JSONDecoder<PVTypeInfo> typeInfoDecoder = JSONDecoder.getDecoder(PVTypeInfo.class);
typeInfoDecoder.decode((JSONObject) typeInfoJSON, unmarshalledTypeInfo);
return unmarshalledTypeInfo;
}
private long getNumberOfEvents() throws Exception {
Timestamp start = TimeUtils.convertFromEpochSeconds(TimeUtils.getStartOfYearInSeconds(TimeUtils.getCurrentYear() - 2), 0);
Timestamp end = TimeUtils.now();
RawDataRetrievalAsEventStream rawDataRetrieval = new RawDataRetrievalAsEventStream("http://localhost:" + ConfigServiceForTests.RETRIEVAL_TEST_PORT+ "/retrieval/data/getData.raw");
Timestamp obtainedFirstSample = null;
long eventCount = 0;
try(EventStream stream = rawDataRetrieval.getDataForPVS(new String[] { pvName }, start, end, null)) {
if(stream != null) {
for(Event e : stream) {
if(obtainedFirstSample == null) {
obtainedFirstSample = e.getEventTimeStamp();
}
logger.debug("Sample from " + TimeUtils.convertToHumanReadableString(e.getEventTimeStamp()));
eventCount++;
}
} else {
fail("Stream is null when retrieving data.");
}
}
return eventCount;
}
}