diff --git a/configuration/conf/config.properties b/configuration/conf/config.properties index b5750037d..e7c7e2a49 100644 --- a/configuration/conf/config.properties +++ b/configuration/conf/config.properties @@ -428,6 +428,9 @@ # + schema.txt 每一行解释每个Sensor的Type,如"d_0 s_0 3\n d_0 s_1 4" # IS_COPY_MODE=false +# Format of the real dataset for generateDataMode export and verification replay: CSV or TSFILE (TsFile 2.x table model) +REAL_DATASET_FORMAT=CSV + # IS_ADD_ANOMALY=false # ANOMALY_RATE=0.2 diff --git a/core/pom.xml b/core/pom.xml index eeaed16d1..2f45d2240 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -92,6 +92,11 @@ opencsv 5.5.2 + + org.apache.tsfile + tsfile + 2.2.1 + diff --git a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/client/generate/GenerateDataWriteClient.java b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/client/generate/GenerateDataWriteClient.java index 8fc654d76..c593aead3 100644 --- a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/client/generate/GenerateDataWriteClient.java +++ b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/client/generate/GenerateDataWriteClient.java @@ -28,7 +28,7 @@ /** This client is using by GenerateMode */ public class GenerateDataWriteClient extends GenerateBaseClient { - private DataWriter dataWriter = DataWriter.getDataWriter(); + private DataWriter dataWriter = DataWriter.getDataWriter(clientThreadId); public GenerateDataWriteClient( int id, CountDownLatch countDownLatch, CyclicBarrier barrier, TaskProgress taskProgress) { @@ -38,14 +38,18 @@ public GenerateDataWriteClient( /** Do Operations */ @Override protected void doTest() { - taskProgress.resetLoopIndex(); - for (; taskProgress.getLoopIndex() < config.getLOOP(); taskProgress.incrementLoopIndex()) { - if (!doGenerate()) { - break; - } - if (isStop.get()) { - break; + try { + taskProgress.resetLoopIndex(); + for (; taskProgress.getLoopIndex() < config.getLOOP(); taskProgress.incrementLoopIndex()) { + if (!doGenerate()) { + break; + } + if (isStop.get()) { + break; + } } + } finally { + dataWriter.close(); } } diff --git a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/conf/Config.java b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/conf/Config.java index 33b703c38..02ce83395 100644 --- a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/conf/Config.java +++ b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/conf/Config.java @@ -194,6 +194,12 @@ public class Config { /** The path of file */ private String FILE_PATH = "data/test"; + /** + * Format of the real dataset on disk: CSV or TSFILE (table model). Used by generate + + * verification. + */ + private RealDatasetFormat REAL_DATASET_FORMAT = RealDatasetFormat.CSV; + /** The size of Big Batch */ private int BIG_BATCH_SIZE = 100; @@ -1003,6 +1009,14 @@ public void setFILE_PATH(String FILE_PATH) { this.FILE_PATH = FILE_PATH; } + public RealDatasetFormat getREAL_DATASET_FORMAT() { + return REAL_DATASET_FORMAT; + } + + public void setREAL_DATASET_FORMAT(RealDatasetFormat REAL_DATASET_FORMAT) { + this.REAL_DATASET_FORMAT = REAL_DATASET_FORMAT; + } + public int getDEVICE_NUMBER() { return DEVICE_NUMBER; } @@ -2044,7 +2058,10 @@ public String toInfoText() { + "\nQUERY_SEED=" + QUERY_SEED + "\nWORKLOAD_BUFFER_SIZE=" - + WORKLOAD_BUFFER_SIZE; + + WORKLOAD_BUFFER_SIZE + + "\nREAL_DATASET_FORMAT='" + + REAL_DATASET_FORMAT + + '\''; } /** get properties from config, one property in one line. */ @@ -2169,6 +2186,7 @@ public ConfigProperties getAllConfigProperties() { configProperties.addProperty("Extern Param", "ANOMALY_RATE", this.ANOMALY_RATE); configProperties.addProperty("Extern Param", "ANOMALY_TIMES", this.ANOMALY_TIMES); configProperties.addProperty("Extern Param", "IS_COPY_MODE", this.IS_COPY_MODE); + configProperties.addProperty("Extern Param", "REAL_DATASET_FORMAT", this.REAL_DATASET_FORMAT); /* The config of schema */ configProperties.addProperty("Extern Param", "IS_CLIENT_BIND", this.IS_CLIENT_BIND); diff --git a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/conf/ConfigDescriptor.java b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/conf/ConfigDescriptor.java index d4c65162f..49f6ae659 100644 --- a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/conf/ConfigDescriptor.java +++ b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/conf/ConfigDescriptor.java @@ -458,6 +458,10 @@ private void loadProps() { config.setIS_COPY_MODE( Boolean.parseBoolean( properties.getProperty("IS_COPY_MODE", config.isIS_COPY_MODE() + ""))); + config.setREAL_DATASET_FORMAT( + RealDatasetFormat.getRealDatasetFormat( + properties.getProperty( + "REAL_DATASET_FORMAT", config.getREAL_DATASET_FORMAT().name()))); config.setIS_ADD_ANOMALY( Boolean.parseBoolean( properties.getProperty("IS_ADD_ANOMALY", config.isIS_ADD_ANOMALY() + ""))); diff --git a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/conf/RealDatasetFormat.java b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/conf/RealDatasetFormat.java new file mode 100644 index 000000000..a497f4b60 --- /dev/null +++ b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/conf/RealDatasetFormat.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.conf; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public enum RealDatasetFormat { + CSV, + TSFILE; + + private static final Logger LOGGER = LoggerFactory.getLogger(RealDatasetFormat.class); + + public static RealDatasetFormat getRealDatasetFormat(String value) { + if (value != null) { + for (RealDatasetFormat format : values()) { + if (format.name().equalsIgnoreCase(value.trim())) { + return format; + } + } + } + LOGGER.warn("Unknown REAL_DATASET_FORMAT '{}', falling back to CSV", value); + return CSV; + } +} diff --git a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/extern/DataWriter.java b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/extern/DataWriter.java index 62846e550..f3f0d474a 100644 --- a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/extern/DataWriter.java +++ b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/extern/DataWriter.java @@ -27,7 +27,11 @@ public abstract class DataWriter { protected static final Config config = ConfigDescriptor.getInstance().getConfig(); - public static DataWriter getDataWriter() { + public static DataWriter getDataWriter(int clientId) { + if (config.getREAL_DATASET_FORMAT() + == cn.edu.tsinghua.iot.benchmark.conf.RealDatasetFormat.TSFILE) { + return new TsFileDataWriter(clientId); + } return new CSVDataWriter(); } @@ -37,4 +41,7 @@ public static DataWriter getDataWriter() { * @param insertLoopIndex loop index of batch */ public abstract boolean writeBatch(IBatch batch, long insertLoopIndex) throws Exception; + + /** Flush and release any open resources. No-op for append-style writers (CSV). */ + public void close() {} } diff --git a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/extern/SchemaWriter.java b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/extern/SchemaWriter.java index b6bb042ba..2bbb688c1 100644 --- a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/extern/SchemaWriter.java +++ b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/extern/SchemaWriter.java @@ -32,6 +32,10 @@ public abstract class SchemaWriter { /** Get Basic Writer */ public static SchemaWriter getBasicWriter() { + if (config.getREAL_DATASET_FORMAT() + == cn.edu.tsinghua.iot.benchmark.conf.RealDatasetFormat.TSFILE) { + return new TsFileSchemaWriter(); + } return new CSVSchemaWriter(); } diff --git a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/extern/TsFileDataWriter.java b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/extern/TsFileDataWriter.java new file mode 100644 index 000000000..7b01418d3 --- /dev/null +++ b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/extern/TsFileDataWriter.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.extern; + +import cn.edu.tsinghua.iot.benchmark.entity.Batch.IBatch; +import cn.edu.tsinghua.iot.benchmark.entity.Record; +import cn.edu.tsinghua.iot.benchmark.entity.Sensor; +import cn.edu.tsinghua.iot.benchmark.schema.schemaImpl.DeviceSchema; +import cn.edu.tsinghua.iot.benchmark.source.TsFileTableModelMapping; +import cn.edu.tsinghua.iot.benchmark.utils.FileUtils; +import org.apache.tsfile.enums.ColumnCategory; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.file.metadata.TableSchema; +import org.apache.tsfile.write.record.Tablet; +import org.apache.tsfile.write.schema.IMeasurementSchema; +import org.apache.tsfile.write.schema.MeasurementSchema; +import org.apache.tsfile.write.v4.DeviceTableModelWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TsFileDataWriter extends DataWriter { + + private static final Logger LOGGER = LoggerFactory.getLogger(TsFileDataWriter.class); + private static final long MEM_THRESHOLD = 32 * 1024 * 1024L; + + private final int clientId; + private final Map writers = new HashMap<>(); + + public TsFileDataWriter(int clientId) { + this.clientId = clientId; + } + + @Override + public boolean writeBatch(IBatch batch, long insertLoopIndex) throws Exception { + while (true) { + DeviceSchema deviceSchema = batch.getDeviceSchema(); + try { + DeviceTableModelWriter writer = writerFor(deviceSchema); + writer.write(buildTablet(deviceSchema, batch.getRecords())); + } catch (Exception e) { + LOGGER.error("Failed to write batch for device {}", batch.getDeviceSchema().getDevice(), e); + return false; + } + if (!batch.hasNext()) { + break; + } + batch.next(); + } + batch.finishCheck(); + return true; + } + + private DeviceTableModelWriter writerFor(DeviceSchema deviceSchema) throws IOException { + String table = deviceSchema.getTable(); + DeviceTableModelWriter writer = writers.get(table); + if (writer == null) { + File dir = Paths.get(config.getFILE_PATH(), table).toFile(); + Files.createDirectories(dir.toPath()); + File file = new File(FileUtils.union(dir.getAbsolutePath(), "data_" + clientId + ".tsfile")); + Files.deleteIfExists(file.toPath()); + writer = new DeviceTableModelWriter(file, tableSchemaOf(deviceSchema), MEM_THRESHOLD); + writers.put(table, writer); + } + return writer; + } + + /** Column order: FIELD sensors, then device_id TAG, then each tag key as TAG. */ + private TableSchema tableSchemaOf(DeviceSchema deviceSchema) { + List cols = new ArrayList<>(); + List cats = new ArrayList<>(); + for (Sensor s : deviceSchema.getSensors()) { + cols.add( + new MeasurementSchema( + s.getName(), TsFileTableModelMapping.toTsDataType(s.getSensorType()))); + cats.add(ColumnCategory.FIELD); + } + cols.add(new MeasurementSchema(TsFileTableModelMapping.DEVICE_ID_COLUMN, TSDataType.STRING)); + cats.add(ColumnCategory.TAG); + for (String tagKey : deviceSchema.getTags().keySet()) { + cols.add(new MeasurementSchema(tagKey, TSDataType.STRING)); + cats.add(ColumnCategory.TAG); + } + return new TableSchema(deviceSchema.getTable(), cols, cats); + } + + private Tablet buildTablet(DeviceSchema deviceSchema, List records) { + List sensors = deviceSchema.getSensors(); + List names = new ArrayList<>(); + List types = new ArrayList<>(); + List cats = new ArrayList<>(); + for (Sensor s : sensors) { + names.add(s.getName()); + types.add(TsFileTableModelMapping.toTsDataType(s.getSensorType())); + cats.add(ColumnCategory.FIELD); + } + names.add(TsFileTableModelMapping.DEVICE_ID_COLUMN); + types.add(TSDataType.STRING); + cats.add(ColumnCategory.TAG); + List tagKeys = new ArrayList<>(deviceSchema.getTags().keySet()); + for (String tagKey : tagKeys) { + names.add(tagKey); + types.add(TSDataType.STRING); + cats.add(ColumnCategory.TAG); + } + + Tablet tablet = new Tablet(deviceSchema.getTable(), names, types, cats, records.size()); + for (int row = 0; row < records.size(); row++) { + Record record = records.get(row); + tablet.addTimestamp(row, record.getTimestamp()); + tablet.addValue(row, TsFileTableModelMapping.DEVICE_ID_COLUMN, deviceSchema.getDevice()); + for (String tagKey : tagKeys) { + tablet.addValue(row, tagKey, deviceSchema.getTags().get(tagKey)); + } + for (int i = 0; i < sensors.size(); i++) { + addFieldValue(tablet, row, sensors.get(i), record.getRecordDataValue().get(i)); + } + } + tablet.setRowSize(records.size()); + return tablet; + } + + private void addFieldValue(Tablet tablet, int row, Sensor sensor, Object value) { + if (value == null) { + return; // leave null + } + String col = sensor.getName(); + // value is Object; cast to the wrapper (not the primitive) before unboxing. + switch (sensor.getSensorType()) { + case BOOLEAN: + tablet.addValue(row, col, (Boolean) value); + break; + case INT32: + tablet.addValue(row, col, ((Number) value).intValue()); + break; + case INT64: + case TIMESTAMP: + tablet.addValue(row, col, ((Number) value).longValue()); + break; + case FLOAT: + tablet.addValue(row, col, ((Number) value).floatValue()); + break; + case DOUBLE: + tablet.addValue(row, col, ((Number) value).doubleValue()); + break; + default: + tablet.addValue(row, col, String.valueOf(value)); + } + } + + @Override + public void close() { + for (DeviceTableModelWriter writer : writers.values()) { + try { + writer.close(); + } catch (Exception e) { + LOGGER.error("Failed to close TsFile writer", e); + } + } + writers.clear(); + } +} diff --git a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/extern/TsFileSchemaWriter.java b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/extern/TsFileSchemaWriter.java new file mode 100644 index 000000000..9994f9c88 --- /dev/null +++ b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/extern/TsFileSchemaWriter.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.extern; + +import cn.edu.tsinghua.iot.benchmark.conf.Constants; +import cn.edu.tsinghua.iot.benchmark.schema.schemaImpl.DeviceSchema; +import cn.edu.tsinghua.iot.benchmark.utils.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +/** + * Generates a TsFile real dataset's metadata: clears FILE_PATH and writes info.txt (no schema.txt). + */ +public class TsFileSchemaWriter extends SchemaWriter { + + private static final Logger LOGGER = LoggerFactory.getLogger(TsFileSchemaWriter.class); + + @Override + public boolean writeSchema(List deviceSchemaList) { + try { + Path path = Paths.get(config.getFILE_PATH()); + if (Files.isDirectory(path)) { + try (Stream walk = Files.walk(path)) { + walk.sorted(Comparator.reverseOrder()) + .forEach( + subPath -> { + try { + Files.delete(subPath); + } catch (IOException e) { + LOGGER.error("Failed to delete {}", subPath, e); + } + }); + } + } else { + Files.deleteIfExists(path); + } + Files.createDirectories(path); + + Path infoPath = Paths.get(FileUtils.union(config.getFILE_PATH(), Constants.INFO_PATH)); + Files.createFile(infoPath); + Files.write(infoPath, config.toInfoText().getBytes(StandardCharsets.UTF_8)); + LOGGER.info("Finish writing TsFile dataset info to {}", infoPath); + return true; + } catch (IOException ioException) { + LOGGER.error( + "Failed to generate TsFile dataset metadata in {}", config.getFILE_PATH(), ioException); + return false; + } + } +} diff --git a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/schema/schemaImpl/RealMetaDataSchema.java b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/schema/schemaImpl/RealMetaDataSchema.java index 9dd82bf62..17d441243 100644 --- a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/schema/schemaImpl/RealMetaDataSchema.java +++ b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/schema/schemaImpl/RealMetaDataSchema.java @@ -22,11 +22,13 @@ import cn.edu.tsinghua.iot.benchmark.conf.Config; import cn.edu.tsinghua.iot.benchmark.conf.ConfigDescriptor; import cn.edu.tsinghua.iot.benchmark.conf.Constants; +import cn.edu.tsinghua.iot.benchmark.conf.RealDatasetFormat; import cn.edu.tsinghua.iot.benchmark.entity.Sensor; import cn.edu.tsinghua.iot.benchmark.schema.MetaDataSchema; import cn.edu.tsinghua.iot.benchmark.schema.MetaUtil; import cn.edu.tsinghua.iot.benchmark.source.CSVSchemaReader; import cn.edu.tsinghua.iot.benchmark.source.SchemaReader; +import cn.edu.tsinghua.iot.benchmark.source.TsFileSchemaReader; import cn.edu.tsinghua.iot.benchmark.utils.CommonAlgorithms; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,7 +46,8 @@ public class RealMetaDataSchema extends MetaDataSchema { @Override protected boolean createMetaDataSchema() { - SchemaReader schemaReader = new CSVSchemaReader(); + boolean isTsFile = config.getREAL_DATASET_FORMAT() == RealDatasetFormat.TSFILE; + SchemaReader schemaReader = isTsFile ? new TsFileSchemaReader() : new CSVSchemaReader(); String pathStr = config.getFILE_PATH(); Path path = Paths.get(pathStr); // Check the existence of dataset @@ -57,12 +60,8 @@ protected boolean createMetaDataSchema() { LOGGER.error("There are difference between benchmark and dataset"); return false; } - // Load file from dataset - Map files = new LinkedHashMap<>(); - getAllFiles(pathStr, files); - LOGGER.info("Total files: {}", files.size()); - // Load sensor type from dataset + // Load sensor type from dataset and register device schemas (common to both formats) Map> deviceSchemaMap = schemaReader.getDeviceSchemaList(); List deviceSchemaList = new ArrayList<>(); for (Map.Entry> device : deviceSchemaMap.entrySet()) { @@ -75,37 +74,52 @@ protected boolean createMetaDataSchema() { deviceSchemaList.add(deviceSchema); } - // Split into client And store Type + // Split device schemas into clients (common). Pre-create a bucket for EVERY client id so that + // clients with no devices (when client count > device count) get an empty list, not null — + // SchemaClient/DataClient look up by id and would otherwise NPE. Mirrors + // MetaUtil.distributeDevices. + for (int c = 0; c < config.getSCHEMA_CLIENT_NUMBER(); c++) { + SCHEMA_CLIENT_DATA_SCHEMA.put(c, new ArrayList<>()); + } + for (int c = 0; c < config.getDATA_CLIENT_NUMBER(); c++) { + DATA_CLIENT_DATA_SCHEMA.put(c, new ArrayList<>()); + } for (int i = 0; i < deviceSchemaList.size(); i++) { int schemaClientId = i % config.getSCHEMA_CLIENT_NUMBER(); int dataClientId = i % config.getDATA_CLIENT_NUMBER(); DeviceSchema deviceSchema = deviceSchemaList.get(i); - if (!SCHEMA_CLIENT_DATA_SCHEMA.containsKey(schemaClientId)) { - SCHEMA_CLIENT_DATA_SCHEMA.put(schemaClientId, new ArrayList<>()); - } - if (!DATA_CLIENT_DATA_SCHEMA.containsKey(dataClientId)) { - DATA_CLIENT_DATA_SCHEMA.put(dataClientId, new ArrayList<>()); - } SCHEMA_CLIENT_DATA_SCHEMA.get(schemaClientId).add(deviceSchema); DATA_CLIENT_DATA_SCHEMA.get(dataClientId).add(deviceSchema); } - // Split data files into data client + // Distribute data files to data clients List> clientFiles = new ArrayList<>(); for (int i = 0; i < config.getDATA_CLIENT_NUMBER(); i++) { clientFiles.add(new ArrayList<>()); } - Map deviceDistributionForDataClient = - CommonAlgorithms.distributeDevicesToClients( - config.getDEVICE_NUMBER(), config.getDATA_CLIENT_NUMBER()); - List deviceIds = MetaUtil.sortDeviceId(); - int index = 0; - for (int clientId = 0; clientId < config.getDATA_CLIENT_NUMBER(); clientId++) { - int fileNumber = deviceDistributionForDataClient.get(clientId); - for (int fileId = 0; fileId < fileNumber; fileId++, index++) { - String device = config.getDEVICE_NAME_PREFIX() + deviceIds.get(index); - String filePath = files.get(device); - clientFiles.get(clientId).add(filePath); + if (isTsFile) { + // file-level: round-robin every *.tsfile across data clients + List tsFiles = TsFileSchemaReader.listTsFiles(new File(pathStr)); + LOGGER.info("Total TsFiles: {}", tsFiles.size()); + for (int i = 0; i < tsFiles.size(); i++) { + clientFiles.get(i % config.getDATA_CLIENT_NUMBER()).add(tsFiles.get(i).getAbsolutePath()); + } + } else { + // CSV: device -> single file, distributed by device count (unchanged behavior) + Map files = new LinkedHashMap<>(); + getAllFiles(pathStr, files); + LOGGER.info("Total files: {}", files.size()); + Map deviceDistributionForDataClient = + CommonAlgorithms.distributeDevicesToClients( + config.getDEVICE_NUMBER(), config.getDATA_CLIENT_NUMBER()); + List deviceIds = MetaUtil.sortDeviceId(); + int index = 0; + for (int clientId = 0; clientId < config.getDATA_CLIENT_NUMBER(); clientId++) { + int fileNumber = deviceDistributionForDataClient.get(clientId); + for (int fileId = 0; fileId < fileNumber; fileId++, index++) { + String device = config.getDEVICE_NAME_PREFIX() + deviceIds.get(index); + clientFiles.get(clientId).add(files.get(device)); + } } } MetaUtil.setClientFiles(clientFiles); diff --git a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/CSVSchemaReader.java b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/CSVSchemaReader.java index 052add2c2..09ceb222f 100644 --- a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/CSVSchemaReader.java +++ b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/CSVSchemaReader.java @@ -47,37 +47,4 @@ public Map> getDeviceSchemaList() { } return result; } - - @Override - public boolean checkDataSet() { - Path path = Paths.get(config.getFILE_PATH(), Constants.INFO_PATH); - if (!Files.exists(path) || !Files.isRegularFile(path)) { - return false; - } - try { - List configs = Files.readAllLines(path); - List nowConfigs = new ArrayList<>(Arrays.asList(config.toInfoText().split("\n"))); - Map differs = new HashMap<>(); - for (int i = 0; i < nowConfigs.size(); i++) { - String configValue = configs.get(i); - String nowConfigValue = nowConfigs.get(i); - if (!nowConfigValue.equals(configValue)) { - differs.put(configValue, nowConfigValue); - } - } - for (Map.Entry differ : differs.entrySet()) { - LOGGER.error( - "The config in dataSet is " - + differ.getKey() - + " but now config is " - + differ.getValue()); - } - if (differs.size() != 0) { - return false; - } - } catch (IOException exception) { - LOGGER.error("Failed to check config"); - } - return true; - } } diff --git a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/DataReader.java b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/DataReader.java index a39efe0a8..be3c78f0f 100644 --- a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/DataReader.java +++ b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/DataReader.java @@ -33,6 +33,10 @@ public abstract class DataReader { protected String currentFileName; public static DataReader getInstance(List files) { + if (config.getREAL_DATASET_FORMAT() + == cn.edu.tsinghua.iot.benchmark.conf.RealDatasetFormat.TSFILE) { + return new TsFileDataReader(files); + } if (config.isIS_COPY_MODE()) { return new CopyDataReader(files); } else { diff --git a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/SchemaReader.java b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/SchemaReader.java index 8c8c3193f..e17e8f358 100644 --- a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/SchemaReader.java +++ b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/SchemaReader.java @@ -2,13 +2,24 @@ import cn.edu.tsinghua.iot.benchmark.conf.Config; import cn.edu.tsinghua.iot.benchmark.conf.ConfigDescriptor; +import cn.edu.tsinghua.iot.benchmark.conf.Constants; import cn.edu.tsinghua.iot.benchmark.entity.Sensor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; public abstract class SchemaReader { + private static final Logger LOGGER = LoggerFactory.getLogger(SchemaReader.class); protected static final Config config = ConfigDescriptor.getInstance().getConfig(); /** @@ -18,10 +29,38 @@ public abstract class SchemaReader { */ public abstract Map> getDeviceSchemaList(); - /** - * Check whether dataset is valid - * - * @return result - */ - public abstract boolean checkDataSet(); + /** Check whether the on-disk dataset's info.txt matches the current config. */ + public boolean checkDataSet() { + Path path = Paths.get(config.getFILE_PATH(), Constants.INFO_PATH); + if (!Files.exists(path) || !Files.isRegularFile(path)) { + return false; + } + try { + List configs = Files.readAllLines(path); + List nowConfigs = new ArrayList<>(Arrays.asList(config.toInfoText().split("\n"))); + Map differs = new HashMap<>(); + for (int i = 0; i < nowConfigs.size(); i++) { + // A shorter info.txt (e.g. from an older benchmark version with fewer config lines) must be + // reported as a difference, not crash with IndexOutOfBounds. + String configValue = i < configs.size() ? configs.get(i) : ""; + String nowConfigValue = nowConfigs.get(i); + if (!nowConfigValue.equals(configValue)) { + differs.put(configValue, nowConfigValue); + } + } + for (Map.Entry differ : differs.entrySet()) { + LOGGER.error( + "The config in dataSet is " + + differ.getKey() + + " but now config is " + + differ.getValue()); + } + if (differs.size() != 0) { + return false; + } + } catch (IOException exception) { + LOGGER.error("Failed to check config"); + } + return true; + } } diff --git a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/TsFileDataReader.java b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/TsFileDataReader.java new file mode 100644 index 000000000..61064a62c --- /dev/null +++ b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/TsFileDataReader.java @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.source; + +import cn.edu.tsinghua.iot.benchmark.entity.Batch.Batch; +import cn.edu.tsinghua.iot.benchmark.entity.Batch.IBatch; +import cn.edu.tsinghua.iot.benchmark.entity.Record; +import cn.edu.tsinghua.iot.benchmark.entity.Sensor; +import cn.edu.tsinghua.iot.benchmark.entity.enums.SensorType; +import cn.edu.tsinghua.iot.benchmark.schema.MetaUtil; +import cn.edu.tsinghua.iot.benchmark.schema.schemaImpl.DeviceSchema; +import org.apache.tsfile.file.metadata.TableSchema; +import org.apache.tsfile.read.query.dataset.ResultSet; +import org.apache.tsfile.read.v4.DeviceTableModelReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class TsFileDataReader extends DataReader { + + private static final Logger LOGGER = LoggerFactory.getLogger(TsFileDataReader.class); + + private DeviceTableModelReader currentReader; + private Iterator tableIterator; + private ResultSet currentRs; + private List currentSensors; // FIELD columns as benchmark Sensors, in order + private List currentFieldNames; // FIELD column names in order + private List currentTagNames; + private List currentFieldTypes; + + private Row pending; // one-row lookahead + + public TsFileDataReader(List files) { + super(files); + } + + /** A buffered row carries its table's sensor list so a batch built later stays consistent. */ + private static final class Row { + final String device; + final long timestamp; + final List values; + final List sensors; + + Row(String device, long timestamp, List values, List sensors) { + this.device = device; + this.timestamp = timestamp; + this.values = values; + this.sensors = sensors; + } + } + + @Override + public boolean hasNextBatch() { + if (pending == null) { + pending = advanceRow(); + } + return pending != null; + } + + @Override + public IBatch nextBatch() { + if (pending == null) { + pending = advanceRow(); + } + if (pending == null) { + return null; + } + String device = pending.device; + DeviceSchema deviceSchema = new DeviceSchema(device, pending.sensors, MetaUtil.getTags(device)); + + List records = new ArrayList<>(); + while (pending != null + && pending.device.equals(device) + && records.size() < config.getBATCH_SIZE_PER_WRITE()) { + records.add(new Record(pending.timestamp, pending.values)); + pending = advanceRow(); + } + return new Batch(deviceSchema, records); + } + + /** Pull the next row across tables/files; null when exhausted. */ + private Row advanceRow() { + try { + while (true) { + if (currentRs != null && currentRs.next()) { + String device = TsFileSchemaReader.deviceName(currentRs, currentTagNames); + if (device == null) { // malformed/external data: no usable device id — skip this row + LOGGER.warn("Skipping TsFile row with no usable device id in {}", currentFileName); + continue; + } + long time = currentRs.getLong(1); // column 1 == Time + List values = new ArrayList<>(currentFieldNames.size()); + for (int i = 0; i < currentFieldNames.size(); i++) { + values.add(readValue(currentRs, currentFieldNames.get(i), currentFieldTypes.get(i))); + } + return new Row(device, time, values, currentSensors); + } + if (!advanceTableOrFile()) { + return null; + } + } + } catch (Exception e) { + LOGGER.error("Failed to read TsFile rows", e); + return null; + } + } + + /** Move to the next table (within current file) or next file. Returns false when nothing left. */ + private boolean advanceTableOrFile() throws Exception { + if (currentRs != null) { + currentRs.close(); + currentRs = null; + } + while (true) { + if (tableIterator != null && tableIterator.hasNext()) { + TableSchema table = tableIterator.next(); + currentSensors = TsFileTableModelMapping.fieldSensors(table); + currentFieldNames = new ArrayList<>(); + currentFieldTypes = new ArrayList<>(); + for (Sensor s : currentSensors) { + currentFieldNames.add(s.getName()); + currentFieldTypes.add(s.getSensorType()); + } + currentTagNames = TsFileTableModelMapping.tagColumnNames(table); + List queryCols = new ArrayList<>(currentTagNames); + queryCols.addAll(currentFieldNames); + currentRs = + currentReader.query(table.getTableName(), queryCols, Long.MIN_VALUE, Long.MAX_VALUE); + return true; + } + if (!openNextFile()) { + return false; + } + } + } + + private boolean openNextFile() throws Exception { + if (currentReader != null) { + currentReader.close(); + currentReader = null; + tableIterator = null; + } + if (currentFileIndex >= files.size()) { + return false; + } + currentFileName = files.get(currentFileIndex++); + currentReader = new DeviceTableModelReader(new File(currentFileName)); + tableIterator = currentReader.getAllTableSchema().iterator(); + return true; + } + + private static Object readValue(ResultSet rs, String col, SensorType type) { + if (rs.isNull(col)) { + return null; + } + switch (type) { + case BOOLEAN: + return rs.getBoolean(col); + case INT32: + return rs.getInt(col); + case INT64: + case TIMESTAMP: + return rs.getLong(col); + case FLOAT: + return rs.getFloat(col); + case DOUBLE: + return rs.getDouble(col); + case DATE: + return rs.getDate(col); + case TEXT: + case STRING: + case BLOB: + default: + return rs.getString(col); + } + } +} diff --git a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/TsFileSchemaReader.java b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/TsFileSchemaReader.java new file mode 100644 index 000000000..286d7a915 --- /dev/null +++ b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/TsFileSchemaReader.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.source; + +import cn.edu.tsinghua.iot.benchmark.entity.Sensor; +import org.apache.tsfile.file.metadata.TableSchema; +import org.apache.tsfile.read.query.dataset.ResultSet; +import org.apache.tsfile.read.v4.DeviceTableModelReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TsFileSchemaReader extends SchemaReader { + + private static final Logger LOGGER = LoggerFactory.getLogger(TsFileSchemaReader.class); + + @Override + public Map> getDeviceSchemaList() { + Map> result = new LinkedHashMap<>(); + for (File file : listTsFiles(new File(config.getFILE_PATH()))) { + try (DeviceTableModelReader reader = new DeviceTableModelReader(file)) { + for (TableSchema table : reader.getAllTableSchema()) { + List sensors = TsFileTableModelMapping.fieldSensors(table); + for (String device : devicesOf(reader, table, sensors)) { + result.putIfAbsent(device, sensors); + } + } + } catch (Exception e) { + LOGGER.error("Failed to read schema from {}", file, e); + } + } + LOGGER.info("Total devices: {}", result.size()); + return result; + } + + /** Distinct device names (device_id TAG value) present in one table. */ + private List devicesOf( + DeviceTableModelReader reader, TableSchema table, List sensors) throws Exception { + List tagCols = TsFileTableModelMapping.tagColumnNames(table); + // The table query executor only emits rows when at least one FIELD column is selected, so + // include the first field sensor alongside the TAG columns we actually read for the device + // name. + List queryCols = new ArrayList<>(tagCols); + if (!sensors.isEmpty()) { + queryCols.add(sensors.get(0).getName()); + } + List devices = new ArrayList<>(); + String lastDevice = null; + boolean warnedNoId = false; + ResultSet rs = reader.query(table.getTableName(), queryCols, Long.MIN_VALUE, Long.MAX_VALUE); + try { + while (rs.next()) { + String device = deviceName(rs, tagCols); + if (device == null) { // malformed/external data: no usable device identifier + if (!warnedNoId) { + LOGGER.warn( + "Skipping rows with no usable device id in table {} (tag columns={})", + table.getTableName(), + tagCols); + warnedNoId = true; + } + continue; + } + if (!device.equals(lastDevice)) { // rows are grouped by device + if (!devices.contains(device)) { + devices.add(device); + } + lastDevice = device; + } + } + } finally { + rs.close(); + } + return devices; + } + + /** + * device_id TAG if present, else all TAG values joined by '.'. Returns {@code null} when no + * usable identifier exists (null device_id value, or a table with no TAG columns) so callers can + * skip the row instead of building a {@code DeviceSchema(null/"")}, which would NPE in + * device-name parsing. + */ + static String deviceName(ResultSet rs, List tagCols) { + String name; + if (tagCols.contains(TsFileTableModelMapping.DEVICE_ID_COLUMN)) { + name = rs.getString(TsFileTableModelMapping.DEVICE_ID_COLUMN); + } else { + List parts = new ArrayList<>(); + for (String tag : tagCols) { + parts.add(rs.getString(tag)); + } + name = String.join(".", parts); + } + return (name == null || name.isEmpty()) ? null : name; + } + + /** Recursively collect *.tsfile files under root (info.txt and others ignored). */ + public static List listTsFiles(File root) { + List out = new ArrayList<>(); + File[] children = root.listFiles(); + if (children == null) { + return out; + } + Arrays.sort(children); + for (File f : children) { + if (f.isDirectory()) { + out.addAll(listTsFiles(f)); + } else if (f.getName().endsWith(".tsfile")) { + out.add(f); + } + } + return out; + } +} diff --git a/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/TsFileTableModelMapping.java b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/TsFileTableModelMapping.java new file mode 100644 index 000000000..deb30c8b1 --- /dev/null +++ b/core/src/main/java/cn/edu/tsinghua/iot/benchmark/source/TsFileTableModelMapping.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.source; + +import cn.edu.tsinghua.iot.benchmark.entity.Sensor; +import cn.edu.tsinghua.iot.benchmark.entity.enums.SensorType; +import org.apache.tsfile.enums.ColumnCategory; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.file.metadata.TableSchema; +import org.apache.tsfile.write.schema.IMeasurementSchema; + +import java.util.ArrayList; +import java.util.List; + +/** Mapping helpers between benchmark entities and TsFile table-model columns. */ +public class TsFileTableModelMapping { + + /** Canonical TAG column that benchmark-generated TsFiles use to carry the device name. */ + public static final String DEVICE_ID_COLUMN = "device_id"; + + private TsFileTableModelMapping() {} + + public static SensorType toSensorType(TSDataType type) { + try { + return SensorType.valueOf(type.name()); + } catch (IllegalArgumentException e) { + return SensorType.TEXT; + } + } + + public static TSDataType toTsDataType(SensorType type) { + try { + return TSDataType.valueOf(type.name); + } catch (IllegalArgumentException e) { + return TSDataType.TEXT; + } + } + + /** FIELD columns of a table, in column order, as benchmark {@link Sensor}s. */ + public static List fieldSensors(TableSchema table) { + List cols = table.getColumnSchemas(); + List cats = table.getColumnTypes(); + List sensors = new ArrayList<>(); + for (int i = 0; i < cols.size(); i++) { + if (cats.get(i) == ColumnCategory.FIELD) { + sensors.add( + new Sensor( + cols.get(i).getMeasurementName(), + toSensorType(cols.get(i).getType()), + cn.edu.tsinghua.iot.benchmark.entity.enums.ColumnCategory.FIELD)); + } + } + return sensors; + } + + /** Names of TAG columns, in column order. */ + public static List tagColumnNames(TableSchema table) { + List cols = table.getColumnSchemas(); + List cats = table.getColumnTypes(); + List tags = new ArrayList<>(); + for (int i = 0; i < cols.size(); i++) { + if (cats.get(i) == ColumnCategory.TAG) { + tags.add(cols.get(i).getMeasurementName()); + } + } + return tags; + } +} diff --git a/core/src/test/java/cn/edu/tsinghua/iot/benchmark/conf/RealDatasetFormatTest.java b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/conf/RealDatasetFormatTest.java new file mode 100644 index 000000000..a58165003 --- /dev/null +++ b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/conf/RealDatasetFormatTest.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.conf; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class RealDatasetFormatTest { + + @Test + public void parsesKnownValuesCaseInsensitively() { + assertEquals(RealDatasetFormat.TSFILE, RealDatasetFormat.getRealDatasetFormat("TSFILE")); + assertEquals(RealDatasetFormat.TSFILE, RealDatasetFormat.getRealDatasetFormat("tsfile")); + assertEquals(RealDatasetFormat.CSV, RealDatasetFormat.getRealDatasetFormat("CSV")); + } + + @Test + public void unknownFallsBackToCsv() { + assertEquals(RealDatasetFormat.CSV, RealDatasetFormat.getRealDatasetFormat("parquet")); + assertEquals(RealDatasetFormat.CSV, RealDatasetFormat.getRealDatasetFormat(null)); + } +} diff --git a/core/src/test/java/cn/edu/tsinghua/iot/benchmark/extern/TsFileDataWriterTest.java b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/extern/TsFileDataWriterTest.java new file mode 100644 index 000000000..e7b20e2a6 --- /dev/null +++ b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/extern/TsFileDataWriterTest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.extern; + +import cn.edu.tsinghua.iot.benchmark.BenchmarkTestBase; +import cn.edu.tsinghua.iot.benchmark.conf.Config; +import cn.edu.tsinghua.iot.benchmark.conf.ConfigDescriptor; +import cn.edu.tsinghua.iot.benchmark.entity.Batch.Batch; +import cn.edu.tsinghua.iot.benchmark.entity.Record; +import cn.edu.tsinghua.iot.benchmark.entity.Sensor; +import cn.edu.tsinghua.iot.benchmark.entity.enums.SensorType; +import cn.edu.tsinghua.iot.benchmark.schema.schemaImpl.DeviceSchema; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import static org.junit.Assert.assertTrue; + +public class TsFileDataWriterTest extends BenchmarkTestBase { + + private static final Config config = ConfigDescriptor.getInstance().getConfig(); + + @Rule public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void writesReadableTsFilePerTable() throws Exception { + config.setFILE_PATH(folder.getRoot().getAbsolutePath()); + + List sensors = + Arrays.asList(new Sensor("s_0", SensorType.INT32), new Sensor("s_1", SensorType.DOUBLE)); + DeviceSchema d0 = new DeviceSchema("g_0", "t_0", "d_0", sensors, new HashMap<>()); + List recs = new ArrayList<>(); + recs.add(new Record(10L, Arrays.asList((Object) 1, 0.1))); + recs.add(new Record(20L, Arrays.asList((Object) 2, 0.2))); + + TsFileDataWriter writer = new TsFileDataWriter(0); + assertTrue(writer.writeBatch(new Batch(d0, recs), 0L)); + writer.close(); + + // dir name is the resolved table name (DeviceSchema applies the table-name prefix), not "t_0" + File tableDir = new File(folder.getRoot(), d0.getTable()); + File[] files = tableDir.listFiles((dir, name) -> name.endsWith(".tsfile")); + assertTrue(files != null && files.length == 1); + assertTrue(files[0].length() > 0); + } +} diff --git a/core/src/test/java/cn/edu/tsinghua/iot/benchmark/extern/TsFileSchemaWriterTest.java b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/extern/TsFileSchemaWriterTest.java new file mode 100644 index 000000000..23db7d5de --- /dev/null +++ b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/extern/TsFileSchemaWriterTest.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.extern; + +import cn.edu.tsinghua.iot.benchmark.BenchmarkTestBase; +import cn.edu.tsinghua.iot.benchmark.conf.Config; +import cn.edu.tsinghua.iot.benchmark.conf.ConfigDescriptor; +import cn.edu.tsinghua.iot.benchmark.conf.Constants; +import cn.edu.tsinghua.iot.benchmark.conf.RealDatasetFormat; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TsFileSchemaWriterTest extends BenchmarkTestBase { + + private static final Config config = ConfigDescriptor.getInstance().getConfig(); + + @Rule public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void writesInfoTxtAndNoSchemaTxt() throws Exception { + File dir = new File(folder.getRoot(), "ds"); + config.setREAL_DATASET_FORMAT(RealDatasetFormat.TSFILE); + config.setFILE_PATH(dir.getAbsolutePath()); + + assertTrue(SchemaWriter.getBasicWriter() instanceof TsFileSchemaWriter); + assertTrue(new TsFileSchemaWriter().writeSchema(Collections.emptyList())); + + File info = Paths.get(dir.getAbsolutePath(), Constants.INFO_PATH).toFile(); + assertTrue(info.exists()); + assertEquals(config.toInfoText(), new String(Files.readAllBytes(info.toPath()))); + assertFalse(Paths.get(dir.getAbsolutePath(), Constants.SCHEMA_PATH).toFile().exists()); + } +} diff --git a/core/src/test/java/cn/edu/tsinghua/iot/benchmark/schema/schemaImpl/RealMetaDataSchemaTsFileTest.java b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/schema/schemaImpl/RealMetaDataSchemaTsFileTest.java new file mode 100644 index 000000000..8387500c7 --- /dev/null +++ b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/schema/schemaImpl/RealMetaDataSchemaTsFileTest.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.schema.schemaImpl; + +import cn.edu.tsinghua.iot.benchmark.BenchmarkTestBase; +import cn.edu.tsinghua.iot.benchmark.conf.Config; +import cn.edu.tsinghua.iot.benchmark.conf.ConfigDescriptor; +import cn.edu.tsinghua.iot.benchmark.conf.Constants; +import cn.edu.tsinghua.iot.benchmark.conf.RealDatasetFormat; +import cn.edu.tsinghua.iot.benchmark.entity.Batch.IBatch; +import cn.edu.tsinghua.iot.benchmark.mode.enums.BenchmarkMode; +import cn.edu.tsinghua.iot.benchmark.schema.MetaUtil; +import cn.edu.tsinghua.iot.benchmark.source.DataReader; +import cn.edu.tsinghua.iot.benchmark.source.TsFileDataReader; +import cn.edu.tsinghua.iot.benchmark.source.TsFileTestFixtures; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class RealMetaDataSchemaTsFileTest extends BenchmarkTestBase { + + private static final Config config = ConfigDescriptor.getInstance().getConfig(); + + @Rule public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void registersDevicesAndDistributesFilesForTsFile() throws Exception { + File data = new File(folder.getRoot(), "data_0.tsfile"); + TsFileTestFixtures.writeSampleTable(data); + config.setREAL_DATASET_FORMAT(RealDatasetFormat.TSFILE); + config.setFILE_PATH(folder.getRoot().getAbsolutePath()); + config.setBENCHMARK_WORK_MODE(BenchmarkMode.VERIFICATION_WRITE); + config.setDATA_CLIENT_NUMBER(1); + config.setSCHEMA_CLIENT_NUMBER(1); + // info.txt must match current config for checkDataSet to pass. + Files.write( + Paths.get(folder.getRoot().getAbsolutePath(), Constants.INFO_PATH), + config.toInfoText().getBytes(StandardCharsets.UTF_8)); + + RealMetaDataSchema schema = new RealMetaDataSchema(); + assertTrue(schema.createMetaDataSchema()); + + List> clientFiles = MetaUtil.getClientFiles(); + assertEquals(1, clientFiles.size()); + assertEquals(1, clientFiles.get(0).size()); + assertTrue(clientFiles.get(0).get(0).endsWith("data_0.tsfile")); + + DataReader reader = DataReader.getInstance(clientFiles.get(0)); + assertTrue(reader instanceof TsFileDataReader); + int rows = 0; + while (reader.hasNextBatch()) { + IBatch b = reader.nextBatch(); + rows += b.getRecords().size(); + } + assertEquals(4, rows); + } + + /** + * Regression: when client count exceeds device count, every schema/data client id must still get + * a (possibly empty) device list, not null — otherwise SchemaClient NPEs during registration. + */ + @Test + public void everyClientGetsNonNullBucketWhenClientCountExceedsDevices() throws Exception { + // fixture has 2 devices (d_0, d_1); use 20 clients so ids 2..19 receive no device + File data = new File(folder.getRoot(), "data_0.tsfile"); + TsFileTestFixtures.writeSampleTable(data); + config.setREAL_DATASET_FORMAT(RealDatasetFormat.TSFILE); + config.setFILE_PATH(folder.getRoot().getAbsolutePath()); + config.setBENCHMARK_WORK_MODE(BenchmarkMode.VERIFICATION_WRITE); + config.setDATA_CLIENT_NUMBER(20); + config.setSCHEMA_CLIENT_NUMBER(20); + Files.write( + Paths.get(folder.getRoot().getAbsolutePath(), Constants.INFO_PATH), + config.toInfoText().getBytes(StandardCharsets.UTF_8)); + + RealMetaDataSchema schema = new RealMetaDataSchema(); + assertTrue(schema.createMetaDataSchema()); + + for (int id = 0; id < 20; id++) { + assertNotNull("schema client " + id, schema.getDeviceSchemaBySchemaClientId(id)); + assertNotNull("data client " + id, schema.getDeviceSchemaByDataClientId(id)); + } + } +} diff --git a/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/RealDatasetTsFileEndToEndTest.java b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/RealDatasetTsFileEndToEndTest.java new file mode 100644 index 000000000..2007e3d5e --- /dev/null +++ b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/RealDatasetTsFileEndToEndTest.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.source; + +import cn.edu.tsinghua.iot.benchmark.BenchmarkTestBase; +import cn.edu.tsinghua.iot.benchmark.conf.Config; +import cn.edu.tsinghua.iot.benchmark.conf.ConfigDescriptor; +import cn.edu.tsinghua.iot.benchmark.conf.RealDatasetFormat; +import cn.edu.tsinghua.iot.benchmark.entity.Batch.Batch; +import cn.edu.tsinghua.iot.benchmark.entity.Batch.IBatch; +import cn.edu.tsinghua.iot.benchmark.entity.Record; +import cn.edu.tsinghua.iot.benchmark.entity.Sensor; +import cn.edu.tsinghua.iot.benchmark.entity.enums.SensorType; +import cn.edu.tsinghua.iot.benchmark.extern.TsFileDataWriter; +import cn.edu.tsinghua.iot.benchmark.extern.TsFileSchemaWriter; +import cn.edu.tsinghua.iot.benchmark.mode.enums.BenchmarkMode; +import cn.edu.tsinghua.iot.benchmark.schema.MetaUtil; +import cn.edu.tsinghua.iot.benchmark.schema.schemaImpl.DeviceSchema; +import cn.edu.tsinghua.iot.benchmark.schema.schemaImpl.RealMetaDataSchema; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * End-to-end: drive the production TsFile real-dataset generate path ({@link + * TsFileSchemaWriter} + {@link TsFileDataWriter}) to produce a small dataset, then read it back + * through the production verification path ({@link RealMetaDataSchema} + {@link + * TsFileDataReader}) and assert the values round-trip. + * + *

The write phase runs under {@link BenchmarkMode#GENERATE_DATA}. This matters because + * constructing any {@code SchemaWriter} eagerly forces {@code MetaDataSchema.getInstance()} (a + * once-per-JVM singleton). Under {@code GENERATE_DATA} that resolves to {@code + * GenerateMetaDataSchema}, which synthesizes device schemas from config; under a verification mode + * it would instead build a {@code RealMetaDataSchema} whose constructor demands an + * already-generated dataset and calls {@code System.exit} when one is absent — exactly the + * generate-then-read ordering this test exercises. The read-back below does not rely on that + * singleton: it instantiates {@code RealMetaDataSchema} directly. + */ +public class RealDatasetTsFileEndToEndTest extends BenchmarkTestBase { + + private static final Config config = ConfigDescriptor.getInstance().getConfig(); + + @Rule public TemporaryFolder folder = new TemporaryFolder(); + + /** + * {@code config} is a process-wide singleton, and {@code DataReader.getInstance} short-circuits + * to {@link TsFileDataReader} whenever {@code REAL_DATASET_FORMAT == TSFILE}. Restore the default + * so a later CSV/copy test in the same fork still gets its expected reader. + */ + @After + public void restoreDefaultFormat() { + config.setREAL_DATASET_FORMAT(RealDatasetFormat.CSV); + } + + @Test + public void generatedTsFileReadsBackIdentically() throws Exception { + config.setREAL_DATASET_FORMAT(RealDatasetFormat.TSFILE); + config.setFILE_PATH(folder.getRoot().getAbsolutePath()); + config.setBENCHMARK_WORK_MODE(BenchmarkMode.GENERATE_DATA); + config.setDATA_CLIENT_NUMBER(1); + config.setSCHEMA_CLIENT_NUMBER(1); + config.setBATCH_SIZE_PER_WRITE(100); + + List sensors = + Arrays.asList(new Sensor("s_0", SensorType.INT32), new Sensor("s_1", SensorType.DOUBLE)); + DeviceSchema d0 = new DeviceSchema("g_0", "t_0", "d_0", sensors, new HashMap<>()); + + // 1) write dataset metadata (clears dir + info.txt) then data + new TsFileSchemaWriter().writeSchema(Collections.singletonList(d0)); + TsFileDataWriter writer = new TsFileDataWriter(0); + List recs = new ArrayList<>(); + recs.add(new Record(10L, Arrays.asList((Object) 1, 0.1))); + recs.add(new Record(20L, Arrays.asList((Object) 2, 0.2))); + assertTrue(writer.writeBatch(new Batch(d0, recs), 0L)); + writer.close(); + + // 2) read back through the production schema + reader path. + // createMetaDataSchema() is protected in RealMetaDataSchema (a different package), so it is + // invoked via reflection — same approach as RealMetaDataSchemaTest. + RealMetaDataSchema schema = new RealMetaDataSchema(); + Method createMetaDataSchema = + RealMetaDataSchema.class.getDeclaredMethod("createMetaDataSchema"); + createMetaDataSchema.setAccessible(true); + assertTrue((boolean) createMetaDataSchema.invoke(schema)); + List> clientFiles = MetaUtil.getClientFiles(); + + List readBack = new ArrayList<>(); + TsFileDataReader reader = new TsFileDataReader(clientFiles.get(0)); + while (reader.hasNextBatch()) { + IBatch b = reader.nextBatch(); + assertEquals("d_0", b.getDeviceSchema().getDevice()); + readBack.addAll(b.getRecords()); + } + + assertEquals(2, readBack.size()); + assertEquals(10L, readBack.get(0).getTimestamp()); + assertEquals(1, readBack.get(0).getRecordDataValue().get(0)); + assertEquals(0.1, (double) readBack.get(0).getRecordDataValue().get(1), 1e-9); + assertEquals(20L, readBack.get(1).getTimestamp()); + assertEquals(2, readBack.get(1).getRecordDataValue().get(0)); + } +} diff --git a/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/SchemaReaderCheckDataSetTest.java b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/SchemaReaderCheckDataSetTest.java new file mode 100644 index 000000000..f715cdb7f --- /dev/null +++ b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/SchemaReaderCheckDataSetTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.source; + +import cn.edu.tsinghua.iot.benchmark.BenchmarkTestBase; +import cn.edu.tsinghua.iot.benchmark.conf.Config; +import cn.edu.tsinghua.iot.benchmark.conf.ConfigDescriptor; +import cn.edu.tsinghua.iot.benchmark.conf.Constants; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class SchemaReaderCheckDataSetTest extends BenchmarkTestBase { + + private static final Config config = ConfigDescriptor.getInstance().getConfig(); + + @Rule public TemporaryFolder folder = new TemporaryFolder(); + + /** + * A shorter info.txt (e.g. an older dataset with fewer config lines) must return false, not + * throw. + */ + @Test + public void shortInfoTxtReturnsFalseInsteadOfThrowing() throws Exception { + config.setFILE_PATH(folder.getRoot().getAbsolutePath()); + Files.write( + Paths.get(folder.getRoot().getAbsolutePath(), Constants.INFO_PATH), + "LOOP=1".getBytes(StandardCharsets.UTF_8)); + + assertFalse(new CSVSchemaReader().checkDataSet()); + } + + @Test + public void matchingInfoTxtReturnsTrue() throws Exception { + config.setFILE_PATH(folder.getRoot().getAbsolutePath()); + Files.write( + Paths.get(folder.getRoot().getAbsolutePath(), Constants.INFO_PATH), + config.toInfoText().getBytes(StandardCharsets.UTF_8)); + + assertTrue(new CSVSchemaReader().checkDataSet()); + } +} diff --git a/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/TsFileDataReaderTest.java b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/TsFileDataReaderTest.java new file mode 100644 index 000000000..f66e0d4ac --- /dev/null +++ b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/TsFileDataReaderTest.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.source; + +import cn.edu.tsinghua.iot.benchmark.BenchmarkTestBase; +import cn.edu.tsinghua.iot.benchmark.conf.Config; +import cn.edu.tsinghua.iot.benchmark.conf.ConfigDescriptor; +import cn.edu.tsinghua.iot.benchmark.entity.Batch.IBatch; +import cn.edu.tsinghua.iot.benchmark.entity.Record; +import cn.edu.tsinghua.iot.benchmark.schema.schemaImpl.DeviceSchema; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TsFileDataReaderTest extends BenchmarkTestBase { + + private static final Config config = ConfigDescriptor.getInstance().getConfig(); + + @Rule public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void streamsBatchesGroupedByDevice() throws Exception { + File data = new File(folder.getRoot(), "data_0.tsfile"); + TsFileTestFixtures.writeSampleTable(data); + config.setFILE_PATH(folder.getRoot().getAbsolutePath()); + config.setBATCH_SIZE_PER_WRITE(100); + // No MetaDataSchema registration needed: the reader derives sensors from the TsFile itself. + + TsFileDataReader reader = + new TsFileDataReader(Collections.singletonList(data.getAbsolutePath())); + + Map> timesByDevice = new HashMap<>(); + int batchCount = 0; + while (reader.hasNextBatch()) { + IBatch batch = reader.nextBatch(); + batchCount++; + DeviceSchema ds = batch.getDeviceSchema(); + List times = timesByDevice.computeIfAbsent(ds.getDevice(), k -> new ArrayList<>()); + for (Record r : batch.getRecords()) { + times.add(r.getTimestamp()); + assertEquals(3, r.getRecordDataValue().size()); + } + } + assertFalse(reader.hasNextBatch()); + assertEquals(2, timesByDevice.size()); + assertEquals(java.util.Arrays.asList(50L, 60L), timesByDevice.get("d_0")); + assertEquals(java.util.Arrays.asList(100L, 200L), timesByDevice.get("d_1")); + assertTrue(batchCount >= 2); + } +} diff --git a/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/TsFileRoundTripRawTest.java b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/TsFileRoundTripRawTest.java new file mode 100644 index 000000000..1cf3eb85d --- /dev/null +++ b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/TsFileRoundTripRawTest.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.source; + +import org.apache.tsfile.enums.ColumnCategory; +import org.apache.tsfile.file.metadata.TableSchema; +import org.apache.tsfile.read.query.dataset.ResultSet; +import org.apache.tsfile.read.v4.DeviceTableModelReader; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class TsFileRoundTripRawTest { + + @Rule public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void writesAndReadsBackGroupedByDevice() throws Exception { + File f = new File(folder.getRoot(), "sample.tsfile"); + TsFileTestFixtures.writeSampleTable(f); + assertTrue(f.length() > 0); + + try (DeviceTableModelReader reader = new DeviceTableModelReader(f)) { + List tables = reader.getAllTableSchema(); + assertEquals(1, tables.size()); + TableSchema ts = tables.get(0); + assertEquals("t_0", ts.getTableName()); + assertEquals(1, ts.getTagColumnCnt()); + assertEquals("device_id", ts.getColumnSchemas().get(0).getMeasurementName()); + assertEquals(ColumnCategory.TAG, ts.getColumnTypes().get(0)); + + ResultSet rs = + reader.query( + ts.getTableName(), + Arrays.asList("device_id", "s_0", "s_1", "s_2"), + Long.MIN_VALUE, + Long.MAX_VALUE); + List seen = new ArrayList<>(); + List times = new ArrayList<>(); + while (rs.next()) { + times.add(rs.getLong(1)); // column 1 == Time + seen.add(rs.getString("device_id")); + } + rs.close(); + // grouped by device, time-ascending within device + assertEquals(Arrays.asList("d_0", "d_0", "d_1", "d_1"), seen); + assertEquals(Arrays.asList(50L, 60L, 100L, 200L), times); + } + } +} diff --git a/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/TsFileSchemaReaderTest.java b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/TsFileSchemaReaderTest.java new file mode 100644 index 000000000..c9ffff9ef --- /dev/null +++ b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/TsFileSchemaReaderTest.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.source; + +import cn.edu.tsinghua.iot.benchmark.BenchmarkTestBase; +import cn.edu.tsinghua.iot.benchmark.conf.Config; +import cn.edu.tsinghua.iot.benchmark.conf.ConfigDescriptor; +import cn.edu.tsinghua.iot.benchmark.entity.Sensor; +import cn.edu.tsinghua.iot.benchmark.entity.enums.SensorType; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class TsFileSchemaReaderTest extends BenchmarkTestBase { + + private static final Config config = ConfigDescriptor.getInstance().getConfig(); + + @Rule public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void enumeratesDevicesAndFieldSensors() throws Exception { + File data = new File(folder.getRoot(), "data_0.tsfile"); + TsFileTestFixtures.writeSampleTable(data); + config.setFILE_PATH(folder.getRoot().getAbsolutePath()); + + Map> result = new TsFileSchemaReader().getDeviceSchemaList(); + + assertEquals(2, result.size()); + assertTrue(result.containsKey("d_0")); + assertTrue(result.containsKey("d_1")); + List sensors = result.get("d_0"); + assertEquals(3, sensors.size()); + assertEquals("s_0", sensors.get(0).getName()); + assertEquals(SensorType.INT32, sensors.get(0).getSensorType()); + assertEquals(SensorType.DOUBLE, sensors.get(1).getSensorType()); + assertEquals(SensorType.BOOLEAN, sensors.get(2).getSensorType()); + } + + /** A table with no TAG/device_id column must be skipped (no devices), not crash with an NPE. */ + @Test + public void tableWithoutDeviceIdYieldsNoDevicesWithoutNpe() throws Exception { + File data = new File(folder.getRoot(), "notag.tsfile"); + TsFileTestFixtures.writeTableWithoutDeviceId(data); + config.setFILE_PATH(folder.getRoot().getAbsolutePath()); + + Map> result = new TsFileSchemaReader().getDeviceSchemaList(); + + assertTrue(result.isEmpty()); + } +} diff --git a/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/TsFileTableModelMappingTest.java b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/TsFileTableModelMappingTest.java new file mode 100644 index 000000000..c820fd33a --- /dev/null +++ b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/TsFileTableModelMappingTest.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.source; + +import cn.edu.tsinghua.iot.benchmark.entity.enums.SensorType; +import org.apache.tsfile.enums.TSDataType; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TsFileTableModelMappingTest { + + @Test + public void mapsTsDataTypeToSensorTypeByName() { + assertEquals(SensorType.INT32, TsFileTableModelMapping.toSensorType(TSDataType.INT32)); + assertEquals(SensorType.DOUBLE, TsFileTableModelMapping.toSensorType(TSDataType.DOUBLE)); + assertEquals(SensorType.BOOLEAN, TsFileTableModelMapping.toSensorType(TSDataType.BOOLEAN)); + assertEquals(SensorType.STRING, TsFileTableModelMapping.toSensorType(TSDataType.STRING)); + assertEquals(SensorType.TIMESTAMP, TsFileTableModelMapping.toSensorType(TSDataType.TIMESTAMP)); + assertEquals(SensorType.DATE, TsFileTableModelMapping.toSensorType(TSDataType.DATE)); + } + + @Test + public void mapsSensorTypeToTsDataTypeByName() { + assertEquals(TSDataType.INT64, TsFileTableModelMapping.toTsDataType(SensorType.INT64)); + assertEquals(TSDataType.FLOAT, TsFileTableModelMapping.toTsDataType(SensorType.FLOAT)); + assertEquals(TSDataType.TEXT, TsFileTableModelMapping.toTsDataType(SensorType.TEXT)); + } + + @Test + public void unknownTsDataTypeFallsBackToText() { + assertEquals(SensorType.TEXT, TsFileTableModelMapping.toSensorType(TSDataType.VECTOR)); + } +} diff --git a/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/TsFileTestFixtures.java b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/TsFileTestFixtures.java new file mode 100644 index 000000000..d6496fbe4 --- /dev/null +++ b/core/src/test/java/cn/edu/tsinghua/iot/benchmark/source/TsFileTestFixtures.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cn.edu.tsinghua.iot.benchmark.source; + +import org.apache.tsfile.enums.ColumnCategory; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.file.metadata.TableSchema; +import org.apache.tsfile.write.record.Tablet; +import org.apache.tsfile.write.schema.IMeasurementSchema; +import org.apache.tsfile.write.schema.MeasurementSchema; +import org.apache.tsfile.write.v4.DeviceTableModelWriter; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** Test-only helper: writes small table-model TsFiles for reader tests. */ +public class TsFileTestFixtures { + + public static final List FIELD_NAMES = Arrays.asList("s_0", "s_1", "s_2"); + public static final List FIELD_TYPES = + Arrays.asList(TSDataType.INT32, TSDataType.DOUBLE, TSDataType.BOOLEAN); + + /** + * Writes table t_0 with device_id TAG + 3 FIELD columns, 2 devices x 2 rows, into {@code file}. + */ + public static void writeSampleTable(File file) throws Exception { + List cols = new ArrayList<>(); + cols.add(new MeasurementSchema("device_id", TSDataType.STRING)); + for (int i = 0; i < FIELD_NAMES.size(); i++) { + cols.add(new MeasurementSchema(FIELD_NAMES.get(i), FIELD_TYPES.get(i))); + } + List cats = + Arrays.asList( + ColumnCategory.TAG, ColumnCategory.FIELD, ColumnCategory.FIELD, ColumnCategory.FIELD); + TableSchema schema = new TableSchema("t_0", cols, cats); + + List names = Arrays.asList("device_id", "s_0", "s_1", "s_2"); + List types = + Arrays.asList(TSDataType.STRING, TSDataType.INT32, TSDataType.DOUBLE, TSDataType.BOOLEAN); + + try (DeviceTableModelWriter w = new DeviceTableModelWriter(file, schema, 10 * 1024 * 1024L)) { + Tablet t = new Tablet("t_0", names, types, cats, 4); + // interleave d_1 before d_0 to prove the reader regroups by device + addRow(t, 0, 100L, "d_1", 11, 1.1, true); + addRow(t, 1, 50L, "d_0", 1, 0.1, false); + addRow(t, 2, 200L, "d_1", 12, 1.2, false); + addRow(t, 3, 60L, "d_0", 2, 0.2, true); + t.setRowSize(4); + w.write(t); + } + } + + private static void addRow( + Tablet t, int row, long time, String dev, int s0, double s1, boolean s2) { + t.addTimestamp(row, time); + t.addValue(row, "device_id", dev); + t.addValue(row, "s_0", s0); + t.addValue(row, "s_1", s1); + t.addValue(row, "s_2", s2); + } + + /** + * Writes a malformed-for-benchmark table with only FIELD columns and NO TAG/device_id column, so + * rows have no usable device identifier. Used to verify the reader skips them instead of NPEing. + */ + public static void writeTableWithoutDeviceId(File file) throws Exception { + List cols = new ArrayList<>(); + cols.add(new MeasurementSchema("s_0", TSDataType.INT32)); + cols.add(new MeasurementSchema("s_1", TSDataType.DOUBLE)); + List cats = Arrays.asList(ColumnCategory.FIELD, ColumnCategory.FIELD); + TableSchema schema = new TableSchema("t_notag", cols, cats); + + List names = Arrays.asList("s_0", "s_1"); + List types = Arrays.asList(TSDataType.INT32, TSDataType.DOUBLE); + + try (DeviceTableModelWriter w = new DeviceTableModelWriter(file, schema, 10 * 1024 * 1024L)) { + Tablet t = new Tablet("t_notag", names, types, cats, 2); + t.addTimestamp(0, 1L); + t.addValue(0, "s_0", 1); + t.addValue(0, "s_1", 0.1); + t.addTimestamp(1, 2L); + t.addValue(1, "s_0", 2); + t.addValue(1, "s_1", 0.2); + t.setRowSize(2); + w.write(t); + } + } +}