apkipa 1 mesiac pred
commit
d9b56e06c5

+ 38 - 0
.gitignore

@@ -0,0 +1,38 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 7 - 0
.idea/encodings.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding">
+    <file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
+  </component>
+</project>

+ 14 - 0
.idea/misc.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ExternalStorageConfigurationManager" enabled="true" />
+  <component name="MavenProjectsManager">
+    <option name="originalFiles">
+      <list>
+        <option value="$PROJECT_DIR$/pom.xml" />
+      </list>
+    </option>
+  </component>
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/out" />
+  </component>
+</project>

+ 124 - 0
.idea/uiDesigner.xml

@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Palette2">
+    <group name="Swing">
+      <item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
+      </item>
+      <item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
+      </item>
+      <item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
+      </item>
+      <item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
+        <default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
+      </item>
+      <item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
+        <initial-values>
+          <property name="text" value="Button" />
+        </initial-values>
+      </item>
+      <item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
+        <initial-values>
+          <property name="text" value="RadioButton" />
+        </initial-values>
+      </item>
+      <item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
+        <initial-values>
+          <property name="text" value="CheckBox" />
+        </initial-values>
+      </item>
+      <item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
+        <initial-values>
+          <property name="text" value="Label" />
+        </initial-values>
+      </item>
+      <item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
+          <preferred-size width="150" height="-1" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
+          <preferred-size width="150" height="-1" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
+          <preferred-size width="150" height="-1" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+          <preferred-size width="150" height="50" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+          <preferred-size width="150" height="50" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+          <preferred-size width="150" height="50" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
+      </item>
+      <item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+          <preferred-size width="150" height="50" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
+          <preferred-size width="150" height="50" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+          <preferred-size width="150" height="50" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
+          <preferred-size width="200" height="200" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
+          <preferred-size width="200" height="200" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
+      </item>
+      <item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
+      </item>
+      <item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
+      </item>
+      <item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
+      </item>
+      <item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
+          <preferred-size width="-1" height="20" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
+      </item>
+      <item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
+      </item>
+    </group>
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>

+ 95 - 0
pom.xml

@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.example</groupId>
+    <artifactId>java-tsdb-hf-stck</artifactId>
+    <version>1.0-SNAPSHOT</version>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>8</source>
+                    <target>8</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <version>3.1.1</version>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <mainClass>com.feng.fatjar.demo.FatJarMain</mainClass>
+                        </manifest>
+                    </archive>
+                    <descriptorRefs>
+                        <descriptorRef>jar-with-dependencies</descriptorRef>
+                    </descriptorRefs>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>make-assembly</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-api</artifactId>
+            <version>2.23.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-core</artifactId>
+            <version>2.23.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-slf4j2-impl</artifactId>
+            <version>2.23.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.11</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.18.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.11.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>it.unimi.dsi</groupId>
+            <artifactId>fastutil</artifactId>
+            <version>8.5.15</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.luben</groupId>
+            <artifactId>zstd-jni</artifactId>
+            <version>1.5.6-8</version>
+        </dependency>
+    </dependencies>
+
+</project>

+ 85 - 0
src/main/java/tsdb/TsdbApiEntry.java

@@ -0,0 +1,85 @@
+package tsdb;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+public class TsdbApiEntry {
+    private static final Logger LOG = LoggerFactory.getLogger(TsdbApiEntry.class);
+
+    private String metricName = "";
+    private String dataDir = "data";
+    private long partSize = 10000;
+    private String correlationId = "";
+
+    public String getMetricName() {
+        return metricName;
+    }
+
+    public void setMetricName(String metricName) {
+        LOG.debug("Setting metricName to {}, correlation ID = {}", metricName, correlationId);
+        this.metricName = metricName;
+    }
+
+    public String getDataDir() {
+        return dataDir;
+    }
+
+    public void setDataDir(String dataDir) {
+        LOG.debug("Setting dataDir to {}, correlation ID = {}", dataDir, correlationId);
+        if (StringUtils.isEmpty(dataDir)) {
+            throw new IllegalArgumentException("dataDir must not be empty");
+        }
+        this.dataDir = dataDir;
+    }
+
+    public long getPartSize() {
+        return partSize;
+    }
+
+    public void setPartSize(long partSize) {
+        LOG.debug("Setting partSize to {}, correlation ID = {}", partSize, correlationId);
+        if (partSize <= 0) {
+            throw new IllegalArgumentException("partSize must be greater than 0");
+        }
+        this.partSize = partSize;
+    }
+
+    String getCorrelationId() {
+        return correlationId;
+    }
+
+    public TsdbApiEntry() {
+        correlationId = UUID.randomUUID().toString();
+        LOG.debug("Created TsdbApiEntry, correlation ID = {}", correlationId);
+    }
+
+    @Override
+    public String toString() {
+        return "TsdbApiEntry{" +
+                "metricName=" + Util.escapeIntoSingleQuotedString(metricName) +
+                ", dataDir=" + Util.escapeIntoSingleQuotedString(dataDir) +
+                ", partSize=" + partSize +
+                ", correlationId=" + Util.escapeIntoSingleQuotedString(correlationId) +
+                '}';
+    }
+
+    /// Creates a new stream for writing points data.
+    /// @param name The name of the point.
+    /// @param timestampOffset The timestamp offset, in milliseconds.
+    /// @param interval The interval between points, in nanoseconds.
+    /// @return The new stream.
+    public TsdbApiEntryStream newStream(String name, long timestampOffset, long interval) {
+        try {
+            String dirName = String.format("%s-%s", name, Util.getFormattedTimestamp(LocalDateTime.now()));
+            String streamBasePath = Util.pathCombine(dataDir, dirName);
+            return new TsdbApiEntryStream(this, streamBasePath, name, timestampOffset, interval);
+        } catch (Exception e) {
+            LOG.error("Failed to create new stream for name = {}, timestampOffset = {}, interval = {}, correlation ID = {}", name, timestampOffset, interval, correlationId, e);
+            throw e;
+        }
+    }
+}

+ 260 - 0
src/main/java/tsdb/TsdbApiEntryStream.java

@@ -0,0 +1,260 @@
+package tsdb;
+
+import com.github.luben.zstd.EndDirective;
+import com.github.luben.zstd.ZstdCompressCtx;
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import it.unimi.dsi.fastutil.doubles.AbstractDoubleList;
+import it.unimi.dsi.fastutil.doubles.DoubleListIterator;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.RandomAccess;
+import java.util.UUID;
+
+public class TsdbApiEntryStream implements AutoCloseable {
+    private static final Logger LOG = LoggerFactory.getLogger(TsdbApiEntryStream.class);
+    private final StreamMetadata metadata;
+    private final String streamBaseDir;
+    private final ByteBuffer bufInput = ByteBuffer.allocateDirect(16 * 1024).order(ByteOrder.LITTLE_ENDIAN);
+    private final ByteBuffer bufOutput = ByteBuffer.allocateDirect(16 * 1024).order(ByteOrder.LITTLE_ENDIAN);
+    private long currentPartRemainingSize = 0;
+    private FileChannel currentPartFile = null;
+    private String currentPartPath;
+    private ZstdCompressCtx compressCtx = new ZstdCompressCtx();
+    private String correlationId = "";
+
+    TsdbApiEntryStream(TsdbApiEntry parent, String streamBaseDir, String name, long timestampOffset, long interval) {
+        metadata = new StreamMetadata(parent.getMetricName(), name, timestampOffset, interval, parent.getPartSize());
+        this.streamBaseDir = streamBaseDir;
+        correlationId = UUID.randomUUID().toString();
+        LOG.debug("Creating TsdbApiEntryStream from TsdbApiEntry {}, correlation ID = {}",
+                parent.getCorrelationId(), correlationId);
+        // Validate metadata
+        Util.ensureDir(streamBaseDir);
+        if (metadata.partSize <= 0) {
+            throw new IllegalArgumentException("Part size must be positive");
+        }
+        if (metadata.interval <= 0) {
+            throw new IllegalArgumentException("Interval must be positive");
+        }
+        if (metadata.timestampOffset < 0) {
+            throw new IllegalArgumentException("Timestamp offset must be non-negative");
+        }
+        if (StringUtils.isEmpty(metadata.name)) {
+            throw new IllegalArgumentException("Name must not be empty");
+        }
+        // No need to check metric name: it is allowed to be null
+
+        // Emit metadata
+        rewriteMetadata();
+    }
+
+    @Override
+    public void close() {
+        if (isClosed()) {
+            return;
+        }
+
+        LOG.info("Closing TsdbApiEntryStream, correlation id = {}", correlationId);
+
+        // WARN: If an exception is thrown, data will definitely be lost, because the stream goes to a closed state
+        //       and is irreversible.
+
+        try (ZstdCompressCtx ctx = compressCtx) {
+            closeCurrentPart();
+            rewriteMetadata();
+        } catch (Exception e) {
+            LOG.error("Error while closing TsdbApiEntryStream", e);
+            throw e;
+        } finally {
+            compressCtx = null;
+        }
+    }
+
+    public void insertPoint(double value) {
+        checkClosed();
+
+        LOG.debug("Inserting 1 point, correlation ID = {}", correlationId);
+        insertPointInner(value);
+    }
+
+    public void insertPoints(AbstractDoubleList values) {
+        checkClosed();
+
+        LOG.debug("Inserting {} points, correlation ID = {}", values.size(), correlationId);
+        if (values instanceof RandomAccess) {
+            // Fast path
+            for (int i = 0; i < values.size(); i++) {
+                insertPointInner(values.getDouble(i));
+            }
+        } else {
+            // Slow path
+            DoubleListIterator it = values.iterator();
+            while (it.hasNext()) {
+                insertPointInner(it.nextDouble());
+            }
+        }
+    }
+
+    private void insertPointInner(double value) {
+        if (currentPartRemainingSize <= 0) {
+            createNewPart();
+        }
+
+        // Write the value to the buffer
+        // NOTE: We cache writes eagerly to avoid unnecessary JNI overhead
+        if (bufInput.remaining() < 8) {
+            // Not enough space in the buffer, flush now
+            flushIoBuffer(EndDirective.CONTINUE);
+        }
+        bufInput.putDouble(value);
+        metadata.currentPointsCount++;
+        currentPartRemainingSize--;
+    }
+
+    private void createNewPart() {
+        closeCurrentPart();
+
+        String partName = String.format("part_%09d.zst.part", metadata.currentPartIndex);
+        String partPath = Util.pathCombine(streamBaseDir, partName);
+        LOG.debug("Opening new part {}, correlation ID = {}", partPath, correlationId);
+        try {
+//            File f = new File(partPath);
+//            if (f.exists()) {
+//                throw new IllegalStateException("Part file already exists: " + partPath);
+//            }
+//            currentPartFile = FileChannel.open(f.toPath(), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
+            currentPartFile = FileChannel.open(Paths.get(partPath), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
+            currentPartPath = partPath;
+            currentPartRemainingSize = metadata.partSize;
+            metadata.currentPartIndex++;
+        } catch (Exception e) {
+            LOG.error("Error while creating new part, correlation ID = {}", correlationId, e);
+            throw new RuntimeException("Error while creating new part", e);
+        }
+    }
+
+    private void closeCurrentPart() {
+        if (currentPartFile != null) {
+            LOG.debug("Closing current part, correlation ID = {}", correlationId);
+            try {
+                try (FileChannel fc = currentPartFile) {
+                    // Flush the remaining data
+                    flushIoBuffer(EndDirective.END);
+                } finally {
+                    currentPartFile = null;
+                }
+                Util.renameFileToExtension(currentPartPath, "");
+            } catch (Exception e) {
+                LOG.error("Error while closing current part, correlation ID = {}", correlationId, e);
+                throw new RuntimeException("Error while closing current part", e);
+            }
+        }
+    }
+
+    private void flushIoBuffer(EndDirective directive) {
+        // Uncompressed data goes to bufInput, compressed data goes to bufOutput.
+        // Data is compressed and written to currentPartStream when it is full.
+        if (currentPartFile == null) {
+            throw new IllegalStateException("Current part is not open");
+        }
+
+//        if (bufInput.position() == 0) {
+//            return;
+//        }
+        // Compress the data
+        bufInput.flip();
+        bufOutput.clear();
+        if (directive == EndDirective.END) {
+            while (!compressCtx.compressDirectByteBufferStream(bufOutput, bufInput, directive)) {
+                // Write compressed data to the file
+                flushOutputBufToFile();
+            }
+        } else {
+            // Return value of `compressDirectByteBufferStream` is not interesting here
+            // It is OK to return as soon as we've drained the input buffer
+            while (bufInput.hasRemaining()) {
+                compressCtx.compressDirectByteBufferStream(bufOutput, bufInput, directive);
+                flushOutputBufToFile();
+            }
+        }
+        bufInput.clear();
+    }
+
+    private void flushOutputBufToFile() {
+        bufOutput.flip();
+        try {
+            Util.writeFileExact(currentPartFile, bufOutput);
+        } catch (Exception e) {
+            LOG.error("Error while writing compressed data, correlation ID = {}", correlationId, e);
+            throw e;
+        }
+        bufOutput.clear();
+    }
+
+    private void rewriteMetadata() {
+        LOG.debug("Rewriting metadata, correlation ID = {}", correlationId);
+
+        String metadataPath = Util.pathCombine(streamBaseDir, "metadata.json.part");
+        try {
+            try (FileChannel fc = FileChannel.open(Paths.get(metadataPath), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
+                // Write metadata
+                String json = StreamMetadata.GSON.toJson(metadata);
+                ByteBuffer buf = ByteBuffer.wrap(json.getBytes(StandardCharsets.UTF_8));
+                Util.writeFileExact(fc, buf);
+            }
+            Util.renameFileToExtension(metadataPath, "");
+        } catch (Exception e) {
+            LOG.error("Error while rewriting metadata, correlation ID = {}", correlationId, e);
+            throw new RuntimeException("Error while rewriting metadata", e);
+        }
+    }
+
+    private boolean isClosed() {
+        return compressCtx == null;
+    }
+
+    private void checkClosed() {
+        if (isClosed()) {
+            throw new IllegalStateException("Stream is closed");
+        }
+    }
+
+    private static class StreamMetadata {
+        public final String metricName;
+        public final String name;
+        public final long timestampOffset;
+        public final long interval;
+        @SerializedName("points_per_part")
+        public final long partSize;
+        @SerializedName("parts_count")
+        public long currentPartIndex = 0;
+        @SerializedName("total_points")
+        public long currentPointsCount = 0;
+
+        public static final Gson GSON = new GsonBuilder()
+                .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+                .create();
+
+        public StreamMetadata(String metricName, String name, long timestampOffset, long interval, long partSize) {
+            this.metricName = metricName;
+            this.name = name;
+            this.timestampOffset = timestampOffset;
+            this.interval = interval;
+            this.partSize = partSize;
+        }
+    }
+}

+ 111 - 0
src/main/java/tsdb/Util.java

@@ -0,0 +1,111 @@
+package tsdb;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+class Util {
+    public static void ensureDir(String dir) {
+        java.io.File f = new java.io.File(dir);
+        if (f.exists()) {
+            if (!f.isDirectory()) {
+                throw new RuntimeException("File " + dir + " exists and is not a directory");
+            }
+        } else {
+            if (!f.mkdirs()) {
+                throw new RuntimeException("Could not create directory " + dir);
+            }
+        }
+    }
+
+    public static String escapeIntoSingleQuotedString(String s) {
+        StringBuilder sb = new StringBuilder();
+        sb.append('\'');
+        for (int i = 0; i < s.length(); i++) {
+            char c = s.charAt(i);
+            if (c == '\'' || c == '\\') {
+                sb.append('\\');
+                sb.append(c);
+            } else {
+                sb.append(c);
+            }
+        }
+        sb.append('\'');
+        return sb.toString();
+    }
+
+    public static String pathCombine(String path1, String path2) {
+        return Paths.get(path1, path2).toString();
+    }
+
+    public static void renameFileToExtension(String path, String extension) {
+        java.io.File f = new java.io.File(path);
+        String oldExt = FilenameUtils.getExtension(path);
+        if (!StringUtils.isEmpty(oldExt)) {
+            path = FilenameUtils.removeExtension(path);
+        }
+        if (extension == null) {
+            extension = "";
+        }
+        if (!extension.isEmpty() && !StringUtils.startsWith(extension, ".")) {
+            extension = "." + extension;
+        }
+        String newPath = path + extension;
+//        if (!f.exists()) {
+//            throw new RuntimeException("File " + path + " does not exist");
+//        }
+//        if (!f.isFile()) {
+//            throw new RuntimeException("File " + path + " is not a file");
+//        }
+        java.io.File newFile = new java.io.File(newPath);
+        if (f.renameTo(newFile)) {
+            return;
+        }
+        // Failed, try to delete the file first
+        if (!newFile.delete()) {
+            throw new RuntimeException("Could not delete file `" + newPath + "`");
+        }
+        // Try again
+        if (!f.renameTo(newFile)) {
+            throw new RuntimeException("Could not rename file `" + path + "` to `" + newPath + "`");
+        }
+    }
+
+    public static void writeFileExact(FileChannel fc, ByteBuffer buf) {
+        try {
+            while (buf.hasRemaining()) {
+                if (fc.write(buf) == 0) {
+                    throw new RuntimeException("File write returned 0; no space on device?");
+                }
+            }
+        } catch (IOException e) {
+//            rethrow(e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Cast a CheckedException as an unchecked one.
+     *
+     * @param throwable to cast
+     * @param <T>       the type of the Throwable
+     * @return this method will never return a Throwable instance, it will just throw it.
+     * @throws T the throwable as an unchecked throwable
+     */
+    @SuppressWarnings("unchecked")
+    public static <T extends Throwable> void rethrow(Throwable throwable) throws T {
+        throw (T) throwable; // rely on vacuous cast
+    }
+
+    public static String getFormattedTimestamp(LocalDateTime t) {
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss.SSSSSS");
+        return t.format(formatter);
+    }
+}

+ 36 - 0
src/test/java/TestRun.java

@@ -0,0 +1,36 @@
+import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
+import tsdb.TsdbApiEntry;
+import tsdb.TsdbApiEntryStream;
+
+import java.util.ArrayList;
+
+public class TestRun {
+    public static void main(String[] args) {
+        TsdbApiEntry db = new TsdbApiEntry();
+        // 设置 metric 名称
+        db.setMetricName("test_java_metric1");
+        // 设置数据存储目录
+        db.setDataDir("/tmp/tsdb_data/");
+        // 设置每 1_000_000 个数据进行一次分片存盘
+        db.setPartSize(1_000_000);
+        // 新建传感器名为 test_java_pt1 的 stream 对象,从 UTC 时间 1000000 毫秒开始,两点间的时间间隔为 5000 纳秒
+        long startTime = System.currentTimeMillis();
+        try (TsdbApiEntryStream stream = db.newStream("test_java_pt1", 1000000, 5000)) {
+            // 批量插入 8 个数据点(不推荐此方式;如需调用,请直接使用 DoubleArrayList 来避免数据拷贝)
+            {
+                ArrayList<Double> data = new ArrayList<>();
+                for (int i = 0; i < 8; i++) {
+                    data.add(0.1 + 0.1 * i);
+                }
+                stream.insertPoints(new DoubleArrayList(data));
+            }
+            // 插入 10_000_000 个数据点
+            for (int i = 0; i < 10_000_000; i++) {
+                stream.insertPoint(4.2 + i);
+            }
+        }
+        long endTime = System.currentTimeMillis();
+        System.out.println("Time cost: " + (endTime - startTime) + " ms");
+        System.out.println("All success!");
+    }
+}