diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f717dfe..21df7cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,31 +28,30 @@ jobs: - name: Checkout opendata-java uses: actions/checkout@v4 + - name: Checkout opendata + run: git clone --depth 1 https://github.com/opendata-oss/opendata.git ../opendata + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable + - name: Compute opendata cache key + id: opendata-hash + run: echo "cargo-lock-hash=$(sha256sum ../opendata/Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" + - name: Cache cargo registry uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git - log/native/target - key: ${{ runner.os }}-cargo-${{ hashFiles('log/native/Cargo.lock') }} + ../opendata/target + key: ${{ runner.os }}-cargo-opendata-${{ steps.opendata-hash.outputs.cargo-lock-hash }} restore-keys: | - ${{ runner.os }}-cargo- - - - name: Check formatting - working-directory: log/native - run: cargo fmt --all -- --check - - - name: Run clippy - working-directory: log/native - run: cargo clippy --all-targets -- -D warnings + ${{ runner.os }}-cargo-opendata- - - name: Build native JNI library - working-directory: log/native - run: cargo build --release + - name: Build opendata-log C library + working-directory: ../opendata + run: cargo build --release -p opendata-log-c - name: Set up Java 24 uses: actions/setup-java@v4 diff --git a/AGENTS.md b/AGENTS.md index c88c783..1e69090 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,14 +4,14 @@ ## Project Overview -OpenData Java provides Java bindings for [OpenData](https://github.com/opendata-oss/opendata) systems via JNI. Each binding module contains both Rust JNI glue code and Java interface classes. +OpenData Java provides Java bindings for [OpenData](https://github.com/opendata-oss/opendata) systems via Panama FFM (Foreign Function & Memory). Each binding module uses jextract to generate Java bindings from C headers exposed by the OpenData native libraries. -**Important**: This project depends on a sibling clone of the `opendata` repository. The Rust JNI code references OpenData crates via relative paths (`../../../opendata/`). +**Important**: This project depends on a sibling clone of the `opendata` repository. The C library headers are referenced via relative paths (`../../opendata/`). ### Modules - **common**: Shared Java utilities, configuration records, and exceptions (`dev.opendata.common` package) -- **log**: Java bindings for OpenData Log with Rust JNI bridge (`dev.opendata` package) +- **log**: Java bindings for OpenData Log via Panama FFM (`dev.opendata` package) ### Directory Structure @@ -23,63 +23,59 @@ opendata-java/ │ ├── ObjectStoreConfig.java # Sealed interface: InMemory, Aws, Local │ └── OpenDataNativeException.java ├── log/ -│ ├── native/ -│ │ ├── Cargo.toml # Rust JNI crate -│ │ └── src/lib.rs # JNI implementation │ └── src/main/java/dev/opendata/ │ ├── LogDb.java # Main write API │ ├── LogDbReader.java # Read-only API +│ ├── RecordBatch.java # Zero-copy batch builder for writes +│ ├── Record.java # Single record (key + value + timestamp) +│ ├── LogScanIterator.java # Heap-copying iterator over scan results +│ ├── LogScanRawIterator.java # Zero-copy iterator (native memory views) +│ ├── NativeInterop.java # Panama FFM interop layer │ ├── LogDbConfig.java # Configuration record │ └── ... ├── build.gradle # Gradle multi-module build └── settings.gradle ``` -## JNI Architecture +## Panama FFM Architecture -### Native Method Pattern +### jextract Bindings -Java classes declare native methods and load the shared library: +The `log/build.gradle` uses the `de.infolektuell.jextract` Gradle plugin to generate Java bindings from the C header at `opendata/log/c/include/opendata_log.h`. Generated code lives in the `dev.opendata.ffi` package under the `Native` class. -```java -public class LogDb implements AutoCloseable { - static { - System.loadLibrary("opendata_log_jni"); - } +### NativeInterop Layer - private static native long nativeCreate(LogDbConfig config); - private static native void nativeClose(long handle); -} -``` +`NativeInterop.java` is the package-private interop layer that wraps the generated FFM bindings: -Rust implements JNI functions following the naming convention `Java___`: +```java +// Handle-based resource management with AtomicBoolean for thread safety +static abstract class NativeHandle implements AutoCloseable { + private final AtomicBoolean closed = new AtomicBoolean(false); + private volatile MemorySegment segment; + // ... +} -```rust -#[no_mangle] -pub extern "system" fn Java_dev_opendata_LogDb_nativeCreate( - mut env: JNIEnv, - _class: JClass, - config: JObject, -) -> jlong { ... } +// Concrete handles: LogHandle, ReaderHandle, IteratorHandle, ObjectStoreHandle ``` -### Handle-Based Resource Management +### Error Handling + +Every C API call returns `opendata_log_result_t`. The `checkResult()` method inspects the error kind and throws the appropriate Java exception: -- Native resources are represented as `long` handles in Java -- Rust stores actual objects (e.g., `LogDb`, Tokio runtime) behind the handle -- Java classes implement `AutoCloseable` to ensure cleanup via `nativeClose` +- `QUEUE_FULL` → `QueueFullException` +- `TIMEOUT` → `AppendTimeoutException` +- All others → `OpenDataNativeException` -### Async Bridge +### Write Paths -The Rust JNI layer bridges synchronous JNI calls to async OpenData operations: +There are two write paths through `NativeInterop`: -- Uses two Tokio runtimes: one for user operations, one for SlateDB compaction (prevents deadlock) -- JNI methods call `runtime.block_on(async_operation)` to wait for results -- Error handling converts Rust errors to `OpenDataNativeException` +- **`Record[]`** — `doAppend()` copies each record's `byte[] key` and `byte[] value` into arena-allocated segments at append time. Simple but involves per-record allocation. +- **`RecordBatch`** — `doAppendBatch()` slices into the batch's pre-built contiguous segments (zero-copy pointer building). The batch builder (`RecordBatch`) accumulates records into two contiguous `MemorySegment`s (keys and values) with timestamp headers already prepended, so the append path only needs to build the pointer arrays. ### Timestamp Header -The JNI layer prepends an 8-byte timestamp to values for latency measurement: +The Java layer prepends an 8-byte timestamp to values for latency measurement: ``` ┌─────────────────────┬──────────────────────┐ @@ -92,44 +88,30 @@ The JNI layer prepends an 8-byte timestamp to values for latency measurement: ### Prerequisites -- Rust stable toolchain +- Rust stable toolchain (for building the C library) - Java 24+ - Sibling clone of `opendata` repository ### Building ```bash -# Build native JNI library -cd log/native +# Build the C library +cd ../opendata/log/c cargo build --release -# Build Java modules -cd ../.. +# Build Java modules (generates FFM bindings via jextract) +cd ../../../opendata-java ./gradlew build ``` ### Testing ```bash -# Rust tests -cd log/native -cargo test - -# Java tests (requires native library) +# Java tests (requires C library built) ./gradlew test -``` - -### Formatting and Linting -Always run before committing: - -```bash -# Rust -cd log/native -cargo fmt -cargo clippy --all-targets -- -D warnings - -# Java uses standard conventions (4-space indent) +# Log integration tests specifically +./gradlew :log:test --tests "dev.opendata.*" ``` ## Code Conventions @@ -145,52 +127,28 @@ public sealed interface StorageConfig { } ``` -The Rust JNI layer extracts these via reflection (`instanceof` checks and field access). - ### Tests -Use the **given/when/then** pattern with `should_` naming: - -**Rust:** -```rust -#[test] -fn should_extract_config_fields() { - // given - let config = create_test_config(); - - // when - let result = extract_fields(&config); +Use the **given/when/then** pattern with `should` naming: - // then - assert!(result.is_ok()); -} -``` - -**Java:** ```java @Test void shouldAppendAndScanEntries() { - // given - var config = new LogDbConfig(new StorageConfig.InMemory(), null); + try (LogDb log = LogDb.openInMemory()) { + log.tryAppend(key, value); - // when - try (var log = LogDb.create(config)) { - log.append("key".getBytes(), "value".getBytes()); + try (LogScanIterator iter = log.scan(key, 0)) { + // assertions... + } } - - // then - // assertions... } ``` ### Error Handling -- Rust JNI code should return errors via exceptions, not panics - Use `OpenDataNativeException` for all native errors -- Document any overhead in `lib.rs` header comments +- Specific subclasses for recoverable errors (QueueFullException, AppendTimeoutException) ### Imports -Rust: Place all `use` statements at module level, not inside functions. - Java: Standard import ordering (java.*, external, project). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a003af..a9e0305 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,8 +15,8 @@ Thank you for your interest in contributing! This project provides Java bindings This project contains: -- `log/native` - Rust JNI bindings to the OpenData Log -- `log/src` - Java interface classes (`dev.opendata` package) +- `log/src` - Java interface classes and Panama FFM bindings (`dev.opendata` package) +- `common/src` - Shared utilities, configuration records, and exceptions Before contributing, read the [README](README.md). @@ -59,38 +59,23 @@ Open an issue describing: ``` your-workspace/ ├── opendata/ # Clone of opendata repo -└── opendata-java/ # This project (opendata-omb) +└── opendata-java/ # This project ``` ### Building ```bash -# Build native JNI library -cd log/native +# Build the native C library +cd ../opendata/log/c cargo build --release -# Build Java modules -cd ../.. +# Build Java modules (generates FFM bindings via jextract) +cd ../../../opendata-java ./gradlew build ``` ## Code Style -### Rust - -Run `cargo fmt` before committing: - -```bash -cd log/native -cargo fmt -``` - -All code must pass clippy with no warnings: - -```bash -cargo clippy --all-targets -- -D warnings -``` - ### Java - Follow standard Java conventions @@ -101,33 +86,13 @@ cargo clippy --all-targets -- -D warnings ### Guidelines - Write clear, self-documenting code -- Add comments for complex logic, especially JNI boundary handling -- Document any overhead introduced (see `lib.rs` header) -- Prefer returning errors over panicking in JNI code +- Add comments for complex logic, especially FFM boundary handling +- Prefer returning errors over panicking in native code ## Testing Include tests for new functionality. -### Rust Test Style - -Use the `should_` prefix and given/when/then pattern: - -```rust -#[test] -fn should_extract_timestamp_from_header() { - // given - let value = create_timestamped_value(12345, b"payload"); - - // when - let (timestamp, payload) = extract_timestamp_and_payload(&value); - - // then - assert_eq!(timestamp, 12345); - assert_eq!(payload, b"payload"); -} -``` - ### Java Test Style ```java @@ -138,7 +103,7 @@ void shouldAppendAndReadEntry() { byte[] value = "test-value".getBytes(); // when - AppendResult result = log.append(key, value); + AppendResult result = log.tryAppend(key, value); // then assertThat(result.sequence()).isGreaterThan(0); @@ -148,21 +113,20 @@ void shouldAppendAndReadEntry() { ### Running Tests ```bash -# Rust tests -cd log/native -cargo test +# Build C library first +cd ../opendata/log/c && cargo build --release -# Java tests (requires native library built) +# Java tests (requires C library built) +cd ../../../opendata-java ./gradlew test ``` ## Pull Request Process 1. Ensure your code builds and tests pass -2. Run `cargo fmt` and `cargo clippy` -3. Update documentation if needed -4. Add a clear description of changes -5. Reference any related issues +2. Update documentation if needed +3. Add a clear description of changes +4. Reference any related issues ## License diff --git a/README.md b/README.md index 9198da6..4867f76 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,23 @@ Java bindings for OpenData systems. ## Modules -| Module | Maven Coordinates | Description | -|--------|-------------------|-------------| -| `common` | `dev.opendata:common` | Common utilities and exceptions | -| `log` | `dev.opendata:log` | Java bindings for OpenData Log | +| Module | Description | +|--------|-------------| +| `common` | Common utilities and exceptions | +| `log` | Java bindings for OpenData Log | ## Building Prerequisites: - Rust toolchain -- Java 17+ -- Maven 3.8+ +- Java 24+ +- Sibling clone of [opendata](https://github.com/opendata-oss/opendata) repository ```bash -# Build native library (fetches opendata dependency via git automatically) -cd log/native +# Build the native C library +cd ../opendata/log/c cargo build --release # Build and test Java modules -cd ../.. -mvn verify -Djava.library.path=log/native/target/release +cd ../../../opendata-java +./gradlew build ``` diff --git a/common/src/main/java/dev/opendata/common/Bytes.java b/common/src/main/java/dev/opendata/common/Bytes.java new file mode 100644 index 0000000..d282189 --- /dev/null +++ b/common/src/main/java/dev/opendata/common/Bytes.java @@ -0,0 +1,90 @@ +package dev.opendata.common; + +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; + +/** + * A read-only view over native memory, backed by a {@link MemorySegment}. + * + *

Instances are created by iterators and are valid until the next + * {@code next()} call or {@code close()} on the owning iterator. Accessing + * a {@code Bytes} instance after it has been invalidated throws + * {@link IllegalStateException}. + * + *

To obtain a stable copy, call {@link #toArray()}. + */ +public final class Bytes { + + private volatile MemorySegment segment; + + /** + * Creates a new Bytes wrapping the given segment. + * + * @param segment the backing memory segment (should be read-only) + */ + public Bytes(MemorySegment segment) { + this.segment = segment; + } + + /** + * Returns the number of bytes. + */ + public int length() { + return (int) segment().byteSize(); + } + + /** + * Returns the byte at the given index. + * + * @param index the zero-based byte index + * @return the byte value + * @throws IndexOutOfBoundsException if index is out of range + */ + public byte get(int index) { + return segment().get(ValueLayout.JAVA_BYTE, index); + } + + /** + * Copies the contents to a new {@code byte[]}. + * + * @return a freshly allocated byte array with all bytes copied + */ + public byte[] toArray() { + return segment().toArray(ValueLayout.JAVA_BYTE); + } + + /** + * Copies bytes from this Bytes into the destination array. + * + * @param dest the destination array + * @param destOffset the starting offset in the destination + * @param srcOffset the starting offset in this Bytes + * @param len the number of bytes to copy + */ + public void copyTo(byte[] dest, int destOffset, int srcOffset, int len) { + MemorySegment.copy(segment(), ValueLayout.JAVA_BYTE, srcOffset, + MemorySegment.ofArray(dest), ValueLayout.JAVA_BYTE, destOffset, len); + } + + /** + * Returns the underlying {@link MemorySegment} for advanced callers. + * + * @return the backing memory segment + * @throws IllegalStateException if this Bytes has been invalidated + */ + public MemorySegment segment() { + MemorySegment s = segment; + if (s == null) { + throw new IllegalStateException("Bytes has been invalidated — the backing native memory has been freed"); + } + return s; + } + + /** + * Invalidates this Bytes. Called by the owning iterator when the + * backing native memory is about to be freed. + */ + public void invalidate() { + segment = null; + } +} diff --git a/common/src/test/java/dev/opendata/common/BytesTest.java b/common/src/test/java/dev/opendata/common/BytesTest.java new file mode 100644 index 0000000..edb8d11 --- /dev/null +++ b/common/src/test/java/dev/opendata/common/BytesTest.java @@ -0,0 +1,121 @@ +package dev.opendata.common; + +import org.junit.jupiter.api.Test; + +import java.lang.foreign.MemorySegment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class BytesTest { + + private static Bytes bytesOf(byte[] data) { + return new Bytes(MemorySegment.ofArray(data)); + } + + @Test + void shouldReturnCorrectLength() { + Bytes bytes = bytesOf(new byte[]{1, 2, 3}); + assertThat(bytes.length()).isEqualTo(3); + } + + @Test + void shouldReturnCorrectLengthForEmpty() { + Bytes bytes = bytesOf(new byte[0]); + assertThat(bytes.length()).isEqualTo(0); + } + + @Test + void shouldGetByteAtIndex() { + Bytes bytes = bytesOf(new byte[]{10, 20, 30}); + assertThat(bytes.get(0)).isEqualTo((byte) 10); + assertThat(bytes.get(1)).isEqualTo((byte) 20); + assertThat(bytes.get(2)).isEqualTo((byte) 30); + } + + @Test + void shouldThrowOnOutOfBoundsGet() { + Bytes bytes = bytesOf(new byte[]{1, 2}); + assertThatThrownBy(() -> bytes.get(2)) + .isInstanceOf(IndexOutOfBoundsException.class); + assertThatThrownBy(() -> bytes.get(-1)) + .isInstanceOf(IndexOutOfBoundsException.class); + } + + @Test + void shouldRoundtripToArray() { + byte[] original = {0, 127, -128, 1, -1}; + Bytes bytes = bytesOf(original); + assertThat(bytes.toArray()).isEqualTo(original); + } + + @Test + void shouldRoundtripEmptyToArray() { + Bytes bytes = bytesOf(new byte[0]); + assertThat(bytes.toArray()).isEmpty(); + } + + @Test + void shouldCopyToDestination() { + Bytes bytes = bytesOf(new byte[]{10, 20, 30, 40, 50}); + byte[] dest = new byte[3]; + bytes.copyTo(dest, 0, 1, 3); + assertThat(dest).isEqualTo(new byte[]{20, 30, 40}); + } + + @Test + void shouldCopyToWithOffset() { + Bytes bytes = bytesOf(new byte[]{10, 20, 30}); + byte[] dest = new byte[5]; + bytes.copyTo(dest, 2, 0, 3); + assertThat(dest).isEqualTo(new byte[]{0, 0, 10, 20, 30}); + } + + @Test + void shouldReturnSegment() { + MemorySegment segment = MemorySegment.ofArray(new byte[]{1}); + Bytes bytes = new Bytes(segment); + assertThat(bytes.segment()).isSameAs(segment); + } + + @Test + void shouldThrowAfterInvalidateOnLength() { + Bytes bytes = bytesOf(new byte[]{1}); + bytes.invalidate(); + assertThatThrownBy(bytes::length) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void shouldThrowAfterInvalidateOnGet() { + Bytes bytes = bytesOf(new byte[]{1}); + bytes.invalidate(); + assertThatThrownBy(() -> bytes.get(0)) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void shouldThrowAfterInvalidateOnToArray() { + Bytes bytes = bytesOf(new byte[]{1}); + bytes.invalidate(); + assertThatThrownBy(bytes::toArray) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void shouldThrowAfterInvalidateOnCopyTo() { + Bytes bytes = bytesOf(new byte[]{1}); + bytes.invalidate(); + byte[] dest = new byte[1]; + assertThatThrownBy(() -> bytes.copyTo(dest, 0, 0, 1)) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void shouldThrowAfterInvalidateOnSegment() { + Bytes bytes = bytesOf(new byte[]{1}); + bytes.invalidate(); + assertThatThrownBy(bytes::segment) + .isInstanceOf(IllegalStateException.class); + } +} diff --git a/log/build.gradle b/log/build.gradle index 746d6be..4d71cb4 100644 --- a/log/build.gradle +++ b/log/build.gradle @@ -1,8 +1,65 @@ +plugins { + id 'de.infolektuell.jextract' version '1.2.0' +} + +// ============================================================================== +// jextract configuration to generate Java bindings for the opendata-log C library. +// ============================================================================== + +def opendataLogHeader = layout.projectDirectory.file('../../opendata/log/c/include/opendata_log.h') +def opendataLogHeaderText = opendataLogHeader.asFile.getText('UTF-8') +// Captures top-level `opendata_log_*` function names (excluding typedefs/comments). +def includeFunctions = ((opendataLogHeaderText =~ /(?m)^(?!\s*(?:\/\/|\/\*|\*))\s*(?!typedef\b).*?\b(opendata_log_[A-Za-z0-9_]+)\s*\(/).collect { it[1] } as Set).toList().sort() +// Captures public `OPENDATA_LOG_*` preprocessor constants. +def includeConstants = ((opendataLogHeaderText =~ /(?m)^\s*#define\s+(OPENDATA_LOG_[A-Z0-9_]+)\b/).collect { it[1] } as Set).toList().sort() +// Captures named struct types declared as `typedef struct opendata_log_* { ... }`. +def includeStructs = ((opendataLogHeaderText =~ /(?m)^\s*typedef\s+struct\s+(opendata_log_[A-Za-z0-9_]+)\s*\{/).collect { it[1] } as Set).toList().sort() +// Captures `opendata_log_*` typedef names, including function-pointer typedef aliases. +def includeTypedefs = ((opendataLogHeaderText =~ /(?ms)^\s*typedef\b.*?(?:\(\*\s*)?(opendata_log_[A-Za-z0-9_]+)\s*\)?\s*;/).collect { it[1] } as Set).toList().sort() + +def opendataLogJextractLibrary = jextract.libraries.create('opendataLog') { + header = opendataLogHeader + includes = [layout.projectDirectory.dir('../../opendata/log/c/include')] + targetPackage = 'dev.opendata.ffi' + headerClassName = 'Native' + libraries = ['opendata_log_c'] + useSystemLoadLibrary = true + whitelist { + functions = includeFunctions + constants = includeConstants + structs = includeStructs + typedefs = includeTypedefs + } +} + +// ============================================================================== +// Standard Java library configuration below. +// ============================================================================== + +sourceSets { + main { + jextract { + libraries.add(opendataLogJextractLibrary) + } + } +} + dependencies { implementation project(':common') } test { - jvmArgs "-Djava.library.path=${project.projectDir}/native/target/release" jvmArgs '--enable-native-access=ALL-UNNAMED' + + // The C library is part of a Cargo workspace, so output goes to the workspace-level target/. + def opendataRoot = layout.projectDirectory.dir('../../opendata').asFile.absoluteFile + def libraryPathEntries = [ + new File(opendataRoot, 'target/release').absolutePath, + new File(opendataRoot, 'target/debug').absolutePath + ] + def existingLibraryPath = System.getProperty('java.library.path') + if (existingLibraryPath != null && !existingLibraryPath.isBlank()) { + libraryPathEntries.add(existingLibraryPath) + } + jvmArgs "-Djava.library.path=${libraryPathEntries.join(File.pathSeparator)}" } diff --git a/log/native/Cargo.lock b/log/native/Cargo.lock deleted file mode 100644 index c29b898..0000000 --- a/log/native/Cargo.lock +++ /dev/null @@ -1,3710 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "aliasable" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" - -[[package]] -name = "arc-swap" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ded5f9a03ac8f24d1b8a25101ee812cd32cdc8c50a4c50237de2c4915850e73" -dependencies = [ - "rustversion", -] - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "auto_enums" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c170965892137a3a9aeb000b4524aa3cc022a310e709d848b6e1cdce4ab4781" -dependencies = [ - "derive_utils", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "backon" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" -dependencies = [ - "fastrand", - "gloo-timers", - "tokio", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -dependencies = [ - "serde", -] - -[[package]] -name = "cc" -version = "1.2.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link 0.2.1", -] - -[[package]] -name = "cmsketch" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ee2cfacbd29706479902b06d75ad8f1362900836aa32799eabc7e004bfd854" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-skiplist" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" -dependencies = [ - "darling_core 0.14.4", - "darling_macro 0.14.4", -] - -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - -[[package]] -name = "darling_core" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 1.0.109", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.114", -] - -[[package]] -name = "darling_macro" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" -dependencies = [ - "darling_core 0.14.4", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "derive_utils" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccfae181bab5ab6c5478b2ccb69e4c68a02f8c3ec72f6616bfec9dbc599d2ee0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - -[[package]] -name = "duration-str" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88959de2d447fd3eddcf1909d1f19fe084e27a056a6904203dc5d8b9e771c1e" -dependencies = [ - "rust_decimal", - "serde", - "thiserror 2.0.18", - "time", - "winnow 0.6.26", -] - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fail-parallel" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5666e8ca4ec174d896fb742789c29b1bea9319dcfd623c41bececc0a60c4939d" -dependencies = [ - "log", - "once_cell", - "rand 0.8.5", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "figment" -version = "0.10.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" -dependencies = [ - "atomic", - "pear", - "serde", - "serde_json", - "serde_yaml", - "toml 0.8.23", - "uncased", - "version_check", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flatbuffers" -version = "25.12.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" -dependencies = [ - "bitflags", - "rustc_version", -] - -[[package]] -name = "flume" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" -dependencies = [ - "futures-core", - "futures-sink", - "nanorand", - "spin", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "foyer" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642093b1a72c4a0ef89862484d669a353e732974781bb9c49a979526d1e30edc" -dependencies = [ - "equivalent", - "foyer-common", - "foyer-memory", - "foyer-storage", - "madsim-tokio", - "mixtrics", - "pin-project", - "serde", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "foyer-common" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9db9c0e4648b13e9216d785b308d43751ca975301aeb83e607ec630b6f956944" -dependencies = [ - "bincode", - "bytes", - "cfg-if", - "itertools", - "madsim-tokio", - "mixtrics", - "parking_lot", - "pin-project", - "serde", - "thiserror 2.0.18", - "tokio", - "twox-hash", -] - -[[package]] -name = "foyer-intrusive-collections" -version = "0.10.0-dev" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4fee46bea69e0596130e3210e65d3424e0ac1e6df3bde6636304bdf1ca4a3b" -dependencies = [ - "memoffset", -] - -[[package]] -name = "foyer-memory" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "040dc38acbfca8f1def26bbbd9e9199090884aabb15de99f7bf4060be66ff608" -dependencies = [ - "arc-swap", - "bitflags", - "cmsketch", - "equivalent", - "foyer-common", - "foyer-intrusive-collections", - "hashbrown 0.15.5", - "itertools", - "madsim-tokio", - "mixtrics", - "parking_lot", - "pin-project", - "serde", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "foyer-storage" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a77ed888da490e997da6d6d62fcbce3f202ccf28be098c4ea595ca046fc4a9" -dependencies = [ - "allocator-api2", - "anyhow", - "auto_enums", - "bytes", - "equivalent", - "flume", - "foyer-common", - "foyer-memory", - "fs4", - "futures-core", - "futures-util", - "itertools", - "libc", - "lz4", - "madsim-tokio", - "ordered_hash_map", - "parking_lot", - "paste", - "pin-project", - "rand 0.9.2", - "serde", - "thiserror 2.0.18", - "tokio", - "tracing", - "twox-hash", - "zstd", -] - -[[package]] -name = "fs4" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" -dependencies = [ - "rustix", - "windows-sys 0.59.0", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "h2" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap 2.13.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "inlinable_string" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.180" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "lz4" -version = "1.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a20b523e860d03443e98350ceaac5e71c6ba89aea7d960769ec3ce37f4de5af4" -dependencies = [ - "lz4-sys", -] - -[[package]] -name = "lz4-sys" -version = "1.11.1+lz4-1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "madsim" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18351aac4194337d6ea9ffbd25b3d1540ecc0754142af1bff5ba7392d1f6f771" -dependencies = [ - "ahash", - "async-channel", - "async-stream", - "async-task", - "bincode", - "bytes", - "downcast-rs", - "errno", - "futures-util", - "lazy_static", - "libc", - "madsim-macros", - "naive-timer", - "panic-message", - "rand 0.8.5", - "rand_xoshiro 0.6.0", - "rustversion", - "serde", - "spin", - "tokio", - "tokio-util", - "toml 0.9.11+spec-1.1.0", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "madsim-macros" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d248e97b1a48826a12c3828d921e8548e714394bf17274dd0a93910dc946e1" -dependencies = [ - "darling 0.14.4", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "madsim-tokio" -version = "0.2.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3eb2acc57c82d21d699119b859e2df70a91dbdb84734885a1e72be83bdecb5" -dependencies = [ - "madsim", - "spin", - "tokio", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "mixtrics" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb252c728b9d77c6ef9103f0c81524fa0a3d3b161d0a936295d7fbeff6e04c11" -dependencies = [ - "itertools", - "parking_lot", -] - -[[package]] -name = "naive-timer" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034a0ad7deebf0c2abcf2435950a6666c3c15ea9d8fad0c0f48efa8a7f843fed" - -[[package]] -name = "nanorand" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "ntapi" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" -dependencies = [ - "winapi", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-conv" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags", -] - -[[package]] -name = "objc2-io-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" -dependencies = [ - "libc", - "objc2-core-foundation", -] - -[[package]] -name = "object_store" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbfbfff40aeccab00ec8a910b57ca8ecf4319b335c542f2edcd19dd25a1e2a00" -dependencies = [ - "async-trait", - "base64", - "bytes", - "chrono", - "form_urlencoded", - "futures", - "http", - "http-body-util", - "humantime", - "hyper", - "itertools", - "md-5", - "parking_lot", - "percent-encoding", - "quick-xml", - "rand 0.9.2", - "reqwest", - "ring", - "serde", - "serde_json", - "serde_urlencoded", - "thiserror 2.0.18", - "tokio", - "tracing", - "url", - "walkdir", - "wasm-bindgen-futures", - "web-time", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "opendata-common" -version = "0.1.6" -source = "git+https://github.com/opendata-oss/opendata.git#9ca32ec3ca66b9403efe9aa313c88d73cf96c5e6" -dependencies = [ - "async-trait", - "bytes", - "futures", - "serde", - "slatedb", - "tokio", - "tokio-util", - "tracing", - "uuid", -] - -[[package]] -name = "opendata-log" -version = "0.2.1" -source = "git+https://github.com/opendata-oss/opendata.git#9ca32ec3ca66b9403efe9aa313c88d73cf96c5e6" -dependencies = [ - "async-trait", - "bytes", - "opendata-common", - "prost", - "serde", - "serde_json", - "serde_with", - "slatedb", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "opendata-log-jni" -version = "0.1.0" -dependencies = [ - "bytes", - "jni", - "opendata-common", - "opendata-log", - "tokio", -] - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "ordered_hash_map" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0e5f22bf6dd04abd854a8874247813a8fa2c8c1260eba6fbb150270ce7c176" -dependencies = [ - "hashbrown 0.13.2", -] - -[[package]] -name = "ouroboros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" -dependencies = [ - "aliasable", - "ouroboros_macro", - "static_assertions", -] - -[[package]] -name = "ouroboros_macro" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" -dependencies = [ - "heck", - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "panic-message" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384e52fd8fbd4cbe3c317e8216260c21a0f9134de108cea8a4dd4e7e152c472d" - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link 0.2.1", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pear" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" -dependencies = [ - "inlinable_string", - "pear_codegen", - "yansi", -] - -[[package]] -name = "pear_codegen" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" -dependencies = [ - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", - "version_check", - "yansi", -] - -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_xoshiro" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" -dependencies = [ - "rand_core 0.6.4", -] - -[[package]] -name = "rand_xoshiro" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" -dependencies = [ - "rand_core 0.9.5", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64", - "bytes", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rust_decimal" -version = "1.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" -dependencies = [ - "arrayvec", - "num-traits", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" -dependencies = [ - "base64", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.13.0", - "schemars 0.9.0", - "schemars 1.2.1", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" -dependencies = [ - "darling 0.21.3", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.13.0", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "slatedb" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "588e9ae32019696205a05e54e4456486e8d11b69c72662739be853736833b3dd" -dependencies = [ - "anyhow", - "async-trait", - "atomic", - "backon", - "bitflags", - "bytemuck", - "bytes", - "chrono", - "crc32fast", - "crossbeam-skiplist", - "dotenvy", - "duration-str", - "fail-parallel", - "figment", - "flatbuffers", - "foyer", - "futures", - "log", - "object_store", - "once_cell", - "ouroboros", - "parking_lot", - "rand 0.9.2", - "rand_xoshiro 0.7.0", - "serde", - "serde_json", - "siphasher", - "sysinfo", - "thiserror 1.0.69", - "thread_local", - "tokio", - "tokio-util", - "tracing", - "ulid", - "url", - "uuid", - "walkdir", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "sysinfo" -version = "0.35.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e" -dependencies = [ - "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "objc2-io-kit", - "windows", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "futures-util", - "hashbrown 0.15.5", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit", -] - -[[package]] -name = "toml" -version = "0.9.11+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" -dependencies = [ - "indexmap 2.13.0", - "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.14", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap 2.13.0", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_write", - "winnow 0.7.14", -] - -[[package]] -name = "toml_parser" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" -dependencies = [ - "winnow 0.7.14", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "toml_writer" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "twox-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" -dependencies = [ - "rand 0.9.2", -] - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "ulid" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" -dependencies = [ - "rand 0.9.2", - "serde", - "web-time", -] - -[[package]] -name = "uncased" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" -dependencies = [ - "getrandom 0.3.4", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.114", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.6.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "zmij" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/log/native/Cargo.toml b/log/native/Cargo.toml deleted file mode 100644 index 3fb826c..0000000 --- a/log/native/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "opendata-log-jni" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -jni = "0.21" -bytes = "1" -tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] } - -log = { package = "opendata-log", git = "https://github.com/opendata-oss/opendata.git" } -common = { package = "opendata-common", git = "https://github.com/opendata-oss/opendata.git" } diff --git a/log/native/src/lib.rs b/log/native/src/lib.rs deleted file mode 100644 index cba1143..0000000 --- a/log/native/src/lib.rs +++ /dev/null @@ -1,1323 +0,0 @@ -//! JNI bindings for OpenData LogDb. -//! -//! This crate provides Java Native Interface bindings to the OpenData LogDb, -//! enabling use from the OpenMessaging Benchmark framework. -//! -//! # Timestamp Header -//! -//! The upstream LogDb API does not yet support timestamps. To enable OMB latency -//! measurement, this layer prepends an 8-byte timestamp header to each value: -//! -//! ```text -//! ┌─────────────────────┬──────────────────────┐ -//! │ timestamp_ms (8B) │ original payload │ -//! │ big-endian i64 │ │ -//! └─────────────────────┴──────────────────────┘ -//! ``` -//! -//! - On `append`: timestamp from Java Record is prepended to the value (captured at submission time) -//! - On `read`: timestamp is extracted from the header and returned separately -//! -//! This is transparent to the Java caller and will be removed once upstream -//! adds native timestamp support. -//! -//! # Benchmark Overhead -//! -//! These bindings introduce overhead compared to native Rust usage. When -//! interpreting benchmark results, consider the following costs: -//! -//! ## Data Copies -//! -//! | Operation | Copies | Notes | -//! |-----------|--------|-------| -//! | `append(key, value)` | 1 + 1 | key: Java→Rust; value: Java→Rust (directly into timestamped buffer) | -//! | `read()` → entries | 2 per entry | Rust `Bytes` → Java `byte[]` for key and value | -//! -//! The value copy on append is optimized using `get_byte_array_region` to copy -//! directly into a pre-allocated buffer that includes space for the timestamp -//! header, avoiding an intermediate allocation. -//! -//! ## Async Runtime -//! -//! The LogDb API is async, but JNI calls are synchronous. We maintain a global -//! Tokio runtime and use `block_on()` for each operation. This adds: -//! - Thread context switching overhead -//! - Potential contention on the runtime's task scheduler -//! -//! ## JNI Call Overhead -//! -//! Each native method invocation has baseline JNI overhead (~tens of nanoseconds) -//! for argument marshalling and stack frame setup. This is negligible for -//! non-trivial operations but adds up for high-frequency calls. -//! -//! ## Comparison Baseline -//! -//! For fair comparison with systems like WarpStream (which use native clients), -//! consider that this JNI layer adds constant overhead per operation. The -//! overhead should be relatively smaller for larger payloads and batch sizes. - -use bytes::Bytes; -use jni::objects::{JByteArray, JClass, JObject, JObjectArray, JValue}; -use jni::sys::{jlong, jobject, jobjectArray}; -use jni::JNIEnv; -use tokio::runtime::{Handle, Runtime}; - -/// Size of the timestamp header prepended to values. -const TIMESTAMP_HEADER_SIZE: usize = 8; - -// Re-export log crate types with explicit naming to avoid confusion with std log -use common::storage::config::{ - AwsObjectStoreConfig, LocalObjectStoreConfig, ObjectStoreConfig, SlateDbStorageConfig, - StorageConfig, -}; -use common::StorageRuntime; -use log::{ - AppendError, AppendOutput, Config, LogDb, LogDbBuilder, LogDbReader, LogEntry, LogRead, - ReaderConfig, Record, SegmentConfig, -}; - -/// Handle to a LogDb instance with its associated Tokio runtime. -/// -/// Uses block_on for JNI operations. A separate compaction runtime is used for -/// SlateDB's compaction/GC tasks to prevent deadlock when the main runtime's -/// threads are blocked in JNI calls. -struct LogHandle { - /// The LogDb instance - log: LogDb, - /// Handle to the runtime for async operations - runtime_handle: Handle, - /// The main runtime (kept alive for the lifetime of the LogDb) - runtime: Option, - /// Separate runtime for SlateDB compaction/GC tasks - compaction_runtime: Option, -} - -// ============================================================================= -// LogDb JNI Methods -// ============================================================================= - -/// Creates a new LogDb instance with the specified configuration. -/// -/// # Arguments -/// * `config` - Java LogDbConfig object -/// -/// # Safety -/// This is a JNI function - must be called from Java with valid JNIEnv. -#[no_mangle] -pub extern "system" fn Java_dev_opendata_LogDb_nativeCreate<'local>( - mut env: JNIEnv<'local>, - _class: JClass<'local>, - config: JObject<'local>, -) -> jlong { - // Extract storage config from LogDbConfig - let storage_config = match extract_storage_config(&mut env, &config) { - Ok(c) => c, - Err(e) => { - let _ = env.throw_new("java/lang/IllegalArgumentException", e); - return 0; - } - }; - - // Extract segment config from LogDbConfig - let segment_config = match extract_segment_config(&mut env, &config) { - Ok(c) => c, - Err(e) => { - let _ = env.throw_new("java/lang/IllegalArgumentException", e); - return 0; - } - }; - - let config = Config { - storage: storage_config, - segmentation: segment_config, - }; - - // Create a dedicated runtime for this LogDb instance (for user operations) - let runtime = match tokio::runtime::Builder::new_multi_thread() - .enable_all() - .thread_name("opendata-log") - .build() - { - Ok(rt) => rt, - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - return 0; - } - }; - - // Create a SEPARATE runtime for SlateDB compaction/GC tasks. - // This prevents deadlock when the main runtime's threads are blocked in JNI calls - // while SlateDB's background tasks need to make progress. - let compaction_runtime = match tokio::runtime::Builder::new_multi_thread() - .enable_all() - .thread_name("opendata-compaction") - .build() - { - Ok(rt) => rt, - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - return 0; - } - }; - - // Open the LogDb using LogDbBuilder with separate compaction runtime - let result = runtime.block_on(async { - let storage_runtime = - StorageRuntime::new().with_compaction_runtime(compaction_runtime.handle().clone()); - LogDbBuilder::new(config) - .with_storage_runtime(storage_runtime) - .build() - .await - }); - - match result { - Ok(log) => { - let handle = Box::new(LogHandle { - log, - runtime_handle: runtime.handle().clone(), - runtime: Some(runtime), - compaction_runtime: Some(compaction_runtime), - }); - Box::into_raw(handle) as jlong - } - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - 0 - } - } -} - -// ============================================================================= -// Config Extraction Helpers -// ============================================================================= - -/// Extracts StorageConfig from a Java LogDbConfig object. -fn extract_storage_config( - env: &mut JNIEnv<'_>, - config: &JObject<'_>, -) -> Result { - // Get the storage field from LogDbConfig - let storage_obj = env - .call_method( - config, - "storage", - "()Ldev/opendata/common/StorageConfig;", - &[], - ) - .map_err(|e| format!("Failed to get storage: {}", e))? - .l() - .map_err(|e| format!("Failed to get storage object: {}", e))?; - - // Check which type of StorageConfig it is using instanceof - let in_memory_class = env - .find_class("dev/opendata/common/StorageConfig$InMemory") - .map_err(|e| format!("Failed to find InMemory class: {}", e))?; - - let slatedb_class = env - .find_class("dev/opendata/common/StorageConfig$SlateDb") - .map_err(|e| format!("Failed to find SlateDb class: {}", e))?; - - if env - .is_instance_of(&storage_obj, &in_memory_class) - .map_err(|e| format!("instanceof check failed: {}", e))? - { - Ok(StorageConfig::InMemory) - } else if env - .is_instance_of(&storage_obj, &slatedb_class) - .map_err(|e| format!("instanceof check failed: {}", e))? - { - extract_slatedb_config(env, &storage_obj) - } else { - Err("Unknown StorageConfig type".to_string()) - } -} - -/// Extracts SlateDbStorageConfig from a Java StorageConfig.SlateDb record. -fn extract_slatedb_config( - env: &mut JNIEnv<'_>, - slatedb_obj: &JObject<'_>, -) -> Result { - // Get path field - let path_obj = env - .call_method(slatedb_obj, "path", "()Ljava/lang/String;", &[]) - .map_err(|e| format!("Failed to get path: {}", e))? - .l() - .map_err(|e| format!("Failed to get path object: {}", e))?; - let path: String = env - .get_string((&path_obj).into()) - .map_err(|e| format!("Failed to convert path: {}", e))? - .into(); - - // Get objectStore field - let object_store_obj = env - .call_method( - slatedb_obj, - "objectStore", - "()Ldev/opendata/common/ObjectStoreConfig;", - &[], - ) - .map_err(|e| format!("Failed to get objectStore: {}", e))? - .l() - .map_err(|e| format!("Failed to get objectStore object: {}", e))?; - let object_store = extract_object_store_config(env, &object_store_obj)?; - - // Get settingsPath field (nullable) - let settings_path_obj = env - .call_method(slatedb_obj, "settingsPath", "()Ljava/lang/String;", &[]) - .map_err(|e| format!("Failed to get settingsPath: {}", e))? - .l() - .map_err(|e| format!("Failed to get settingsPath object: {}", e))?; - let settings_path = if settings_path_obj.is_null() { - None - } else { - Some( - env.get_string((&settings_path_obj).into()) - .map_err(|e| format!("Failed to convert settingsPath: {}", e))? - .into(), - ) - }; - - Ok(StorageConfig::SlateDb(SlateDbStorageConfig { - path, - object_store, - settings_path, - })) -} - -/// Extracts ObjectStoreConfig from a Java ObjectStoreConfig object. -fn extract_object_store_config( - env: &mut JNIEnv<'_>, - obj: &JObject<'_>, -) -> Result { - let in_memory_class = env - .find_class("dev/opendata/common/ObjectStoreConfig$InMemory") - .map_err(|e| format!("Failed to find ObjectStoreConfig.InMemory class: {}", e))?; - - let aws_class = env - .find_class("dev/opendata/common/ObjectStoreConfig$Aws") - .map_err(|e| format!("Failed to find ObjectStoreConfig.Aws class: {}", e))?; - - let local_class = env - .find_class("dev/opendata/common/ObjectStoreConfig$Local") - .map_err(|e| format!("Failed to find ObjectStoreConfig.Local class: {}", e))?; - - if env - .is_instance_of(obj, &in_memory_class) - .map_err(|e| format!("instanceof check failed: {}", e))? - { - Ok(ObjectStoreConfig::InMemory) - } else if env - .is_instance_of(obj, &aws_class) - .map_err(|e| format!("instanceof check failed: {}", e))? - { - // Extract region and bucket from Aws record - let region_obj = env - .call_method(obj, "region", "()Ljava/lang/String;", &[]) - .map_err(|e| format!("Failed to get region: {}", e))? - .l() - .map_err(|e| format!("Failed to get region object: {}", e))?; - let region: String = env - .get_string((®ion_obj).into()) - .map_err(|e| format!("Failed to convert region: {}", e))? - .into(); - - let bucket_obj = env - .call_method(obj, "bucket", "()Ljava/lang/String;", &[]) - .map_err(|e| format!("Failed to get bucket: {}", e))? - .l() - .map_err(|e| format!("Failed to get bucket object: {}", e))?; - let bucket: String = env - .get_string((&bucket_obj).into()) - .map_err(|e| format!("Failed to convert bucket: {}", e))? - .into(); - - Ok(ObjectStoreConfig::Aws(AwsObjectStoreConfig { - region, - bucket, - })) - } else if env - .is_instance_of(obj, &local_class) - .map_err(|e| format!("instanceof check failed: {}", e))? - { - // Extract path from Local record - let path_obj = env - .call_method(obj, "path", "()Ljava/lang/String;", &[]) - .map_err(|e| format!("Failed to get path: {}", e))? - .l() - .map_err(|e| format!("Failed to get path object: {}", e))?; - let path: String = env - .get_string((&path_obj).into()) - .map_err(|e| format!("Failed to convert path: {}", e))? - .into(); - - Ok(ObjectStoreConfig::Local(LocalObjectStoreConfig { path })) - } else { - Err("Unknown ObjectStoreConfig type".to_string()) - } -} - -/// Extracts SegmentConfig from a Java LogDbConfig object. -fn extract_segment_config( - env: &mut JNIEnv<'_>, - config: &JObject<'_>, -) -> Result { - let seg_obj = env - .call_method( - config, - "segmentation", - "()Ldev/opendata/SegmentConfig;", - &[], - ) - .map_err(|e| format!("Failed to get segmentation: {}", e))? - .l() - .map_err(|e| format!("Failed to get segmentation object: {}", e))?; - - // Get sealIntervalMs (nullable Long) - let interval_obj = env - .call_method(&seg_obj, "sealIntervalMs", "()Ljava/lang/Long;", &[]) - .map_err(|e| format!("Failed to get sealIntervalMs: {}", e))? - .l() - .map_err(|e| format!("Failed to get sealIntervalMs object: {}", e))?; - - let seal_interval = if interval_obj.is_null() { - None - } else { - let ms = env - .call_method(&interval_obj, "longValue", "()J", &[]) - .map_err(|e| format!("Failed to unbox sealIntervalMs: {}", e))? - .j() - .map_err(|e| format!("Failed to get long value: {}", e))?; - Some(std::time::Duration::from_millis(ms as u64)) - }; - - Ok(SegmentConfig { seal_interval }) -} - -/// Extracts StorageConfig from a Java LogDbReaderConfig object. -fn extract_reader_storage_config( - env: &mut JNIEnv<'_>, - config: &JObject<'_>, -) -> Result { - // Get the storage field from LogDbReaderConfig - let storage_obj = env - .call_method( - config, - "storage", - "()Ldev/opendata/common/StorageConfig;", - &[], - ) - .map_err(|e| format!("Failed to get storage: {}", e))? - .l() - .map_err(|e| format!("Failed to get storage object: {}", e))?; - - // Check which type of StorageConfig it is using instanceof - let in_memory_class = env - .find_class("dev/opendata/common/StorageConfig$InMemory") - .map_err(|e| format!("Failed to find InMemory class: {}", e))?; - - let slatedb_class = env - .find_class("dev/opendata/common/StorageConfig$SlateDb") - .map_err(|e| format!("Failed to find SlateDb class: {}", e))?; - - if env - .is_instance_of(&storage_obj, &in_memory_class) - .map_err(|e| format!("instanceof check failed: {}", e))? - { - Ok(StorageConfig::InMemory) - } else if env - .is_instance_of(&storage_obj, &slatedb_class) - .map_err(|e| format!("instanceof check failed: {}", e))? - { - extract_slatedb_config(env, &storage_obj) - } else { - Err("Unknown StorageConfig type".to_string()) - } -} - -/// Extracts the optional refresh interval from a Java LogDbReaderConfig object. -fn extract_refresh_interval( - env: &mut JNIEnv<'_>, - config: &JObject<'_>, -) -> Result, String> { - // Get the refreshIntervalMs field (returns Long, which may be null) - let interval_obj = env - .call_method(config, "refreshIntervalMs", "()Ljava/lang/Long;", &[]) - .map_err(|e| format!("Failed to get refreshIntervalMs: {}", e))? - .l() - .map_err(|e| format!("Failed to get refreshIntervalMs object: {}", e))?; - - if interval_obj.is_null() { - return Ok(None); - } - - // Unbox the Long to get the primitive value - let interval_ms = env - .call_method(&interval_obj, "longValue", "()J", &[]) - .map_err(|e| format!("Failed to unbox refreshIntervalMs: {}", e))? - .j() - .map_err(|e| format!("Failed to get long value: {}", e))?; - - Ok(Some(std::time::Duration::from_millis(interval_ms as u64))) -} - -/// Appends a batch of records without blocking for queue space. -/// -/// Each value is stored as: `[8-byte timestamp (big-endian i64)] + [original payload]` -/// The timestamp is read from each Java Record object (captured at submission time). -/// -/// # Arguments -/// * `handle` - Native LogDb pointer -/// * `records` - Array of Java Record objects (each with key, value, timestampMs) -/// -/// # Returns -/// AppendResult jobject with start_sequence and timestamp of first record -/// -/// # Safety -/// JNI function - handle must be a valid pointer returned by nativeCreate. -#[no_mangle] -#[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "system" fn Java_dev_opendata_LogDb_nativeTryAppend<'local>( - mut env: JNIEnv<'local>, - _class: JClass<'local>, - handle: jlong, - records: jobjectArray, -) -> jobject { - if handle == 0 { - let _ = env.throw_new("java/lang/NullPointerException", "LogDb handle is null"); - return std::ptr::null_mut(); - } - - let log_handle = unsafe { &*(handle as *const LogHandle) }; - - let (rust_records, first_timestamp_ms) = match convert_java_records(&mut env, records) { - Ok(v) => v, - Err(()) => return std::ptr::null_mut(), // exception already thrown - }; - - let result = log_handle - .runtime_handle - .block_on(async { log_handle.log.try_append(rust_records).await }); - - match result { - Ok(append_output) => { - match create_append_result(&mut env, &append_output, first_timestamp_ms) { - Ok(obj) => obj.into_raw(), - Err(e) => { - let _ = - env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - std::ptr::null_mut() - } - } - } - Err(e) => { - throw_append_error(&mut env, e); - std::ptr::null_mut() - } - } -} - -/// Appends a batch of records, blocking up to `timeoutMs` for queue space. -/// -/// # Safety -/// JNI function - handle must be a valid pointer returned by nativeCreate. -#[no_mangle] -#[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "system" fn Java_dev_opendata_LogDb_nativeAppendTimeout<'local>( - mut env: JNIEnv<'local>, - _class: JClass<'local>, - handle: jlong, - records: jobjectArray, - timeout_ms: jlong, -) -> jobject { - if handle == 0 { - let _ = env.throw_new("java/lang/NullPointerException", "LogDb handle is null"); - return std::ptr::null_mut(); - } - - let log_handle = unsafe { &*(handle as *const LogHandle) }; - - let (rust_records, first_timestamp_ms) = match convert_java_records(&mut env, records) { - Ok(v) => v, - Err(()) => return std::ptr::null_mut(), - }; - - let timeout = std::time::Duration::from_millis(timeout_ms as u64); - let result = log_handle - .runtime_handle - .block_on(async { log_handle.log.append_timeout(rust_records, timeout).await }); - - match result { - Ok(append_output) => { - match create_append_result(&mut env, &append_output, first_timestamp_ms) { - Ok(obj) => obj.into_raw(), - Err(e) => { - let _ = - env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - std::ptr::null_mut() - } - } - } - Err(e) => { - throw_append_error(&mut env, e); - std::ptr::null_mut() - } - } -} - -/// Converts a Java Record[] into a Rust `Vec` and the first record's timestamp. -/// -/// Returns `Err(())` if a JNI exception was thrown (caller should return null). -fn convert_java_records( - env: &mut JNIEnv<'_>, - records: jobjectArray, -) -> Result<(Vec, i64), ()> { - let records_array = unsafe { JObjectArray::from_raw(records) }; - let len = match env.get_array_length(&records_array) { - Ok(l) => l as usize, - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - return Err(()); - } - }; - - if len == 0 { - let _ = env.throw_new( - "java/lang/IllegalArgumentException", - "Records array is empty", - ); - return Err(()); - } - - let mut rust_records = Vec::with_capacity(len); - let mut first_timestamp_ms: i64 = 0; - - for i in 0..len { - let record_obj = match env.get_object_array_element(&records_array, i as i32) { - Ok(obj) => obj, - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - return Err(()); - } - }; - - // Extract key byte[] from Record - let key_obj = match env.call_method(&record_obj, "key", "()[B", &[]) { - Ok(v) => v.l().unwrap(), - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - return Err(()); - } - }; - let key_array: JByteArray = key_obj.into(); - let key_bytes = match env.convert_byte_array(&key_array) { - Ok(b) => Bytes::from(b), - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - return Err(()); - } - }; - - // Extract value byte[] from Record - let value_obj = match env.call_method(&record_obj, "value", "()[B", &[]) { - Ok(v) => v.l().unwrap(), - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - return Err(()); - } - }; - let value_array: JByteArray = value_obj.into(); - - // Extract timestampMs from Record - let timestamp_ms = match env.call_method(&record_obj, "timestampMs", "()J", &[]) { - Ok(v) => v.j().unwrap(), - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - return Err(()); - } - }; - - if i == 0 { - first_timestamp_ms = timestamp_ms; - } - - // Convert value with timestamp header - let value_bytes = match copy_value_with_timestamp(env, &value_array, timestamp_ms) { - Ok(b) => b, - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - return Err(()); - } - }; - - rust_records.push(Record { - key: key_bytes, - value: value_bytes, - }); - } - - Ok((rust_records, first_timestamp_ms)) -} - -/// Maps an `AppendError` to the appropriate Java exception and throws it. -fn throw_append_error(env: &mut JNIEnv<'_>, error: AppendError) { - match error { - AppendError::QueueFull(_) => { - let _ = env.throw_new( - "dev/opendata/common/QueueFullException", - "Write queue is full", - ); - } - AppendError::Timeout(_) => { - let _ = env.throw_new( - "dev/opendata/common/AppendTimeoutException", - "Timed out waiting for queue space", - ); - } - AppendError::Shutdown => { - let _ = env.throw_new( - "dev/opendata/common/OpenDataNativeException", - "Writer has shut down", - ); - } - AppendError::InvalidRecord(msg) => { - let _ = env.throw_new( - "dev/opendata/common/OpenDataNativeException", - format!("Invalid record: {}", msg), - ); - } - } -} - -/// Copies a Java byte array into a Rust buffer with a prepended timestamp header. -/// -/// This avoids an intermediate allocation by copying directly into the final buffer. -fn copy_value_with_timestamp( - env: &mut JNIEnv<'_>, - value: &JByteArray<'_>, - timestamp_ms: i64, -) -> Result { - let payload_len = env.get_array_length(value)? as usize; - - // Allocate final buffer: 8-byte header + payload - let mut buffer = vec![0u8; TIMESTAMP_HEADER_SIZE + payload_len]; - - // Write timestamp header (big-endian) - buffer[..TIMESTAMP_HEADER_SIZE].copy_from_slice(×tamp_ms.to_be_bytes()); - - // Copy payload directly from Java into buffer, avoiding intermediate Vec - if payload_len > 0 { - // Safety: buffer[TIMESTAMP_HEADER_SIZE..] has exactly payload_len bytes - // get_byte_array_region expects i8 slice, so we need to cast - let dest = &mut buffer[TIMESTAMP_HEADER_SIZE..]; - let dest_i8 = - unsafe { std::slice::from_raw_parts_mut(dest.as_mut_ptr() as *mut i8, payload_len) }; - env.get_byte_array_region(value, 0, dest_i8)?; - } - - Ok(Bytes::from(buffer)) -} - -/// Flushes all pending writes to durable storage. -/// -/// # Safety -/// JNI function - handle must be a valid pointer returned by nativeCreate. -#[no_mangle] -#[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "system" fn Java_dev_opendata_LogDb_nativeFlush<'local>( - mut env: JNIEnv<'local>, - _class: JClass<'local>, - handle: jlong, -) { - if handle == 0 { - let _ = env.throw_new("java/lang/NullPointerException", "LogDb handle is null"); - return; - } - - let log_handle = unsafe { &*(handle as *const LogHandle) }; - - let result = log_handle - .runtime_handle - .block_on(async { log_handle.log.flush().await }); - - if let Err(e) = result { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - } -} - -/// Closes and frees a LogDb instance and its associated runtime. -/// -/// # Safety -/// JNI function - handle must be a valid pointer returned by nativeCreate. -#[no_mangle] -pub extern "system" fn Java_dev_opendata_LogDb_nativeClose<'local>( - mut env: JNIEnv<'local>, - _class: JClass<'local>, - handle: jlong, -) { - if handle != 0 { - let log_handle = unsafe { Box::from_raw(handle as *mut LogHandle) }; - - // Destructure to take ownership of components - let LogHandle { - log, - runtime_handle, - runtime, - compaction_runtime, - } = *log_handle; - - // Flush pending writes then close the log - let result = runtime_handle.block_on(async { - log.flush().await?; - log.close().await - }); - - if let Err(e) = result { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - } - - // Shutdown the runtimes - if let Some(rt) = compaction_runtime { - rt.shutdown_background(); - } - if let Some(rt) = runtime { - rt.shutdown_background(); - } - } -} - -/// Scans entries from the log for a given key. -/// -/// Uses the LogDb (which implements LogRead) to scan entries. -/// -/// # Safety -/// JNI function - handle must be a valid pointer returned by nativeCreate. -#[no_mangle] -#[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "system" fn Java_dev_opendata_LogDb_nativeScan<'local>( - mut env: JNIEnv<'local>, - _class: JClass<'local>, - handle: jlong, - key: JByteArray<'local>, - start_sequence: jlong, - max_entries: jlong, -) -> jobjectArray { - if handle == 0 { - let _ = env.throw_new("java/lang/NullPointerException", "LogDb handle is null"); - return std::ptr::null_mut(); - } - - let log_handle = unsafe { &*(handle as *const LogHandle) }; - - let key_bytes = match env.convert_byte_array(&key) { - Ok(b) => Bytes::from(b), - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - return std::ptr::null_mut(); - } - }; - - let max = max_entries as usize; - let start_seq = start_sequence as u64; - - // Scan entries using the LogDb (which implements LogRead) - let entries_result = log_handle.runtime_handle.block_on(async { - let mut iter = log_handle.log.scan(key_bytes, start_seq..).await?; - let mut entries = Vec::with_capacity(max); - while entries.len() < max { - match iter.next().await? { - Some(entry) => entries.push(entry), - None => break, - } - } - Ok::, log::Error>(entries) - }); - - match entries_result { - Ok(entries) => match create_log_entry_array(&mut env, &entries) { - Ok(arr) => arr, - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - std::ptr::null_mut() - } - }, - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - std::ptr::null_mut() - } - } -} - -// ============================================================================= -// LogDbReader JNI Methods -// ============================================================================= - -/// Handle to a LogDbReader instance with its associated Tokio runtime. -/// -/// Unlike LogReader which borrows from a parent LogDb, LogDbReader owns its -/// own storage connection and runtime. This allows it to coexist with a -/// separate LogDb writer for realistic end-to-end latency benchmarking. -struct LogDbReaderHandle { - /// The LogDbReader instance - reader: LogDbReader, - /// Handle to the runtime for async operations - runtime_handle: Handle, - /// The runtime (kept alive for the lifetime of the reader) - runtime: Option, -} - -/// Creates a new LogDbReader instance with the specified configuration. -/// -/// # Arguments -/// * `config` - Java LogDbReaderConfig object -/// -/// # Safety -/// This is a JNI function - must be called from Java with valid JNIEnv. -#[no_mangle] -pub extern "system" fn Java_dev_opendata_LogDbReader_nativeCreate<'local>( - mut env: JNIEnv<'local>, - _class: JClass<'local>, - java_config: JObject<'local>, -) -> jlong { - // Extract storage config from LogDbReaderConfig - let storage_config = match extract_reader_storage_config(&mut env, &java_config) { - Ok(c) => c, - Err(e) => { - let _ = env.throw_new("java/lang/IllegalArgumentException", e); - return 0; - } - }; - - // Start with default config and override fields as needed - let mut config = ReaderConfig { - storage: storage_config, - ..ReaderConfig::default() - }; - - // Override refresh interval if explicitly provided - match extract_refresh_interval(&mut env, &java_config) { - Ok(Some(interval)) => config.refresh_interval = interval, - Ok(None) => {} // Use default from ReaderConfig - Err(e) => { - let _ = env.throw_new("java/lang/IllegalArgumentException", e); - return 0; - } - } - - // Create a dedicated runtime for this LogDbReader instance - let runtime = match tokio::runtime::Builder::new_multi_thread() - .enable_all() - .thread_name("opendata-reader") - .build() - { - Ok(rt) => rt, - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - return 0; - } - }; - - // Open the LogDbReader - let result = runtime.block_on(async { LogDbReader::open(config).await }); - - match result { - Ok(reader) => { - let handle = Box::new(LogDbReaderHandle { - reader, - runtime_handle: runtime.handle().clone(), - runtime: Some(runtime), - }); - Box::into_raw(handle) as jlong - } - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - 0 - } - } -} - -/// Scans entries from the log for a given key using LogDbReader. -/// -/// # Safety -/// JNI function - handle must be a valid pointer returned by nativeCreate. -#[no_mangle] -#[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "system" fn Java_dev_opendata_LogDbReader_nativeScan<'local>( - mut env: JNIEnv<'local>, - _class: JClass<'local>, - handle: jlong, - key: JByteArray<'local>, - start_sequence: jlong, - max_entries: jlong, -) -> jobjectArray { - if handle == 0 { - let _ = env.throw_new( - "java/lang/NullPointerException", - "LogDbReader handle is null", - ); - return std::ptr::null_mut(); - } - - let reader_handle = unsafe { &*(handle as *const LogDbReaderHandle) }; - - let key_bytes = match env.convert_byte_array(&key) { - Ok(b) => Bytes::from(b), - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - return std::ptr::null_mut(); - } - }; - - let max = max_entries as usize; - let start_seq = start_sequence as u64; - - // Scan entries using the LogDbReader - let entries_result = reader_handle.runtime_handle.block_on(async { - let mut iter = reader_handle.reader.scan(key_bytes, start_seq..).await?; - let mut entries = Vec::with_capacity(max); - while entries.len() < max { - match iter.next().await? { - Some(entry) => entries.push(entry), - None => break, - } - } - Ok::, log::Error>(entries) - }); - - match entries_result { - Ok(entries) => match create_log_entry_array(&mut env, &entries) { - Ok(arr) => arr, - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - std::ptr::null_mut() - } - }, - Err(e) => { - let _ = env.throw_new("dev/opendata/common/OpenDataNativeException", e.to_string()); - std::ptr::null_mut() - } - } -} - -/// Closes and frees a LogDbReader instance. -/// -/// # Safety -/// JNI function - handle must be a valid pointer returned by nativeCreate. -#[no_mangle] -pub extern "system" fn Java_dev_opendata_LogDbReader_nativeClose<'local>( - _env: JNIEnv<'local>, - _class: JClass<'local>, - handle: jlong, -) { - if handle != 0 { - let reader_handle = unsafe { Box::from_raw(handle as *mut LogDbReaderHandle) }; - - // Shutdown the runtime - if let Some(rt) = reader_handle.runtime { - rt.shutdown_background(); - } - // LogDbReader drops automatically - } -} - -// ============================================================================= -// Helper Functions -// ============================================================================= - -/// Creates a Java AppendResult object from a Rust AppendOutput. -fn create_append_result<'local>( - env: &mut JNIEnv<'local>, - result: &AppendOutput, - timestamp_ms: i64, -) -> Result, jni::errors::Error> { - let class = env.find_class("dev/opendata/AppendResult")?; - - // AppendResult is a record with (long sequence, long timestamp) - let obj = env.new_object( - class, - "(JJ)V", - &[ - JValue::Long(result.start_sequence as i64), - JValue::Long(timestamp_ms), - ], - )?; - - Ok(obj) -} - -/// Creates a Java LogEntry[] array from Rust LogEntry vector. -/// -/// Extracts the timestamp header from each entry's value and returns the -/// original payload (without header) to Java. -fn create_log_entry_array<'local>( - env: &mut JNIEnv<'local>, - entries: &[LogEntry], -) -> Result { - let class = env.find_class("dev/opendata/LogEntry")?; - - let array = env.new_object_array(entries.len() as i32, &class, JObject::null())?; - - for (i, entry) in entries.iter().enumerate() { - // Extract timestamp from header and get original payload - let (timestamp_ms, payload) = extract_timestamp_and_payload(&entry.value); - - let key_arr = env.byte_array_from_slice(&entry.key)?; - let value_arr = env.byte_array_from_slice(payload)?; - - // LogEntry is a record with (long sequence, long timestamp, byte[] key, byte[] value) - let obj = env.new_object( - &class, - "(JJ[B[B)V", - &[ - JValue::Long(entry.sequence as i64), - JValue::Long(timestamp_ms), - JValue::Object(&key_arr.into()), - JValue::Object(&value_arr.into()), - ], - )?; - - env.set_object_array_element(&array, i as i32, &obj)?; - } - - Ok(array.into_raw()) -} - -/// Extracts the timestamp header and original payload from a stored value. -/// -/// Returns (timestamp_ms, payload_slice). If the value is too short to contain -/// a header, returns (0, full_value) for graceful degradation. -fn extract_timestamp_and_payload(value: &[u8]) -> (i64, &[u8]) { - if value.len() < TIMESTAMP_HEADER_SIZE { - // Value doesn't have header (shouldn't happen, but handle gracefully) - return (0, value); - } - - let timestamp_bytes: [u8; 8] = value[..TIMESTAMP_HEADER_SIZE] - .try_into() - .expect("slice is exactly 8 bytes"); - let timestamp_ms = i64::from_be_bytes(timestamp_bytes); - let payload = &value[TIMESTAMP_HEADER_SIZE..]; - - (timestamp_ms, payload) -} - -/// Returns current wall-clock time as milliseconds since Unix epoch (for testing). -#[cfg(test)] -fn current_timestamp_ms() -> i64 { - use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time before Unix epoch") - .as_millis() as i64 -} - -/// Creates a value with timestamp header prepended (for testing). -#[cfg(test)] -fn create_timestamped_value(timestamp_ms: i64, payload: &[u8]) -> Vec { - let mut buffer = Vec::with_capacity(TIMESTAMP_HEADER_SIZE + payload.len()); - buffer.extend_from_slice(×tamp_ms.to_be_bytes()); - buffer.extend_from_slice(payload); - buffer -} - -#[cfg(test)] -mod tests { - use super::*; - - // ========================================================================= - // current_timestamp_ms tests - // ========================================================================= - - #[test] - fn should_return_reasonable_timestamp() { - // given - // Unix timestamp for 2020-01-01 00:00:00 UTC - let min_expected: i64 = 1_577_836_800_000; - // Unix timestamp for 2100-01-01 00:00:00 UTC - let max_expected: i64 = 4_102_444_800_000; - - // when - let timestamp = current_timestamp_ms(); - - // then - assert!( - timestamp > min_expected, - "timestamp {} should be after 2020", - timestamp - ); - assert!( - timestamp < max_expected, - "timestamp {} should be before 2100", - timestamp - ); - } - - #[test] - fn should_return_monotonically_increasing_timestamps() { - // given - let first = current_timestamp_ms(); - - // when - let second = current_timestamp_ms(); - - // then - assert!( - second >= first, - "second timestamp {} should be >= first {}", - second, - first - ); - } - - // ========================================================================= - // extract_timestamp_and_payload tests - // ========================================================================= - - #[test] - fn should_extract_timestamp_and_payload() { - // given - let timestamp: i64 = 1_700_000_000_000; - let payload = b"hello world"; - let value = create_timestamped_value(timestamp, payload); - - // when - let (extracted_ts, extracted_payload) = extract_timestamp_and_payload(&value); - - // then - assert_eq!(extracted_ts, timestamp); - assert_eq!(extracted_payload, payload); - } - - #[test] - fn should_extract_timestamp_with_empty_payload() { - // given - let timestamp: i64 = 1_700_000_000_000; - let payload = b""; - let value = create_timestamped_value(timestamp, payload); - - // when - let (extracted_ts, extracted_payload) = extract_timestamp_and_payload(&value); - - // then - assert_eq!(extracted_ts, timestamp); - assert_eq!(extracted_payload, b""); - } - - #[test] - fn should_handle_value_shorter_than_header() { - // given - let short_value = vec![1, 2, 3]; // Only 3 bytes, header needs 8 - - // when - let (extracted_ts, extracted_payload) = extract_timestamp_and_payload(&short_value); - - // then - graceful degradation - assert_eq!(extracted_ts, 0); - assert_eq!(extracted_payload, &[1, 2, 3]); - } - - #[test] - fn should_handle_empty_value() { - // given - let empty_value: Vec = vec![]; - - // when - let (extracted_ts, extracted_payload) = extract_timestamp_and_payload(&empty_value); - - // then - graceful degradation - assert_eq!(extracted_ts, 0); - assert!(extracted_payload.is_empty()); - } - - #[test] - fn should_handle_exactly_header_size_value() { - // given - value is exactly 8 bytes (header only, no payload) - let timestamp: i64 = 1_700_000_000_000; - let value = timestamp.to_be_bytes().to_vec(); - - // when - let (extracted_ts, extracted_payload) = extract_timestamp_and_payload(&value); - - // then - assert_eq!(extracted_ts, timestamp); - assert!(extracted_payload.is_empty()); - } - - #[test] - fn should_preserve_large_payload() { - // given - let timestamp: i64 = 1_700_000_000_000; - let payload: Vec = (0..10_000).map(|i| (i % 256) as u8).collect(); - let value = create_timestamped_value(timestamp, &payload); - - // when - let (extracted_ts, extracted_payload) = extract_timestamp_and_payload(&value); - - // then - assert_eq!(extracted_ts, timestamp); - assert_eq!(extracted_payload, payload.as_slice()); - } - - // ========================================================================= - // Round-trip tests - // ========================================================================= - - #[test] - fn should_roundtrip_timestamp_and_payload() { - // given - let original_timestamp: i64 = 1_705_123_456_789; - let original_payload = b"test payload with special chars: \x00\xff\n\t"; - - // when - create and extract - let value = create_timestamped_value(original_timestamp, original_payload); - let (extracted_ts, extracted_payload) = extract_timestamp_and_payload(&value); - - // then - assert_eq!(extracted_ts, original_timestamp); - assert_eq!(extracted_payload, original_payload); - } - - #[test] - fn should_roundtrip_zero_timestamp() { - // given - let timestamp: i64 = 0; - let payload = b"payload"; - - // when - let value = create_timestamped_value(timestamp, payload); - let (extracted_ts, extracted_payload) = extract_timestamp_and_payload(&value); - - // then - assert_eq!(extracted_ts, 0); - assert_eq!(extracted_payload, payload); - } - - #[test] - fn should_roundtrip_negative_timestamp() { - // given - negative timestamp (before Unix epoch, unlikely but valid i64) - let timestamp: i64 = -1_000_000; - let payload = b"ancient history"; - - // when - let value = create_timestamped_value(timestamp, payload); - let (extracted_ts, extracted_payload) = extract_timestamp_and_payload(&value); - - // then - assert_eq!(extracted_ts, timestamp); - assert_eq!(extracted_payload, payload); - } - - #[test] - fn should_roundtrip_max_timestamp() { - // given - let timestamp: i64 = i64::MAX; - let payload = b"far future"; - - // when - let value = create_timestamped_value(timestamp, payload); - let (extracted_ts, extracted_payload) = extract_timestamp_and_payload(&value); - - // then - assert_eq!(extracted_ts, timestamp); - assert_eq!(extracted_payload, payload); - } -} diff --git a/log/src/main/java/dev/opendata/LogDb.java b/log/src/main/java/dev/opendata/LogDb.java index 74fa46f..74cf3ea 100644 --- a/log/src/main/java/dev/opendata/LogDb.java +++ b/log/src/main/java/dev/opendata/LogDb.java @@ -2,30 +2,25 @@ import dev.opendata.common.AppendTimeoutException; import dev.opendata.common.QueueFullException; +import dev.opendata.common.StorageConfig; import java.io.Closeable; -import java.util.List; /** * Java binding for the OpenData LogDb trait. * - *

Provides append-only log operations backed by a native Rust implementation. - * This is a thin wrapper over the native layer - callers are responsible for - * batching and backpressure. + *

Provides append-only log operations backed by a native C implementation + * via Panama FFM. This is a thin wrapper over the native layer - callers are + * responsible for batching and backpressure. * *

Implements {@link LogRead} for read operations. For read-only access without * write capabilities, use {@link LogDbReader} instead. */ public class LogDb implements Closeable, LogRead { - static { - System.loadLibrary("opendata_log_jni"); - } - - private final long handle; - private volatile boolean closed = false; + private final NativeInterop.LogHandle handle; - private LogDb(long handle) { + private LogDb(NativeInterop.LogHandle handle) { this.handle = handle; } @@ -39,11 +34,28 @@ public static LogDb open(LogDbConfig config) { if (config == null) { throw new IllegalArgumentException("config must not be null"); } - long handle = nativeCreate(config); - if (handle == 0) { - throw new RuntimeException("Failed to create LogDb instance"); + + StorageConfig storage = config.storage(); + long sealIntervalMs = config.segmentation().sealIntervalMs() != null + ? config.segmentation().sealIntervalMs() + : -1; + + switch (storage) { + case StorageConfig.InMemory() -> { + try (NativeInterop.ObjectStoreHandle objectStore = NativeInterop.objectStoreInMemory()) { + NativeInterop.LogHandle logHandle = NativeInterop.logOpen( + 0, null, objectStore.segment(), null, sealIntervalMs); + return new LogDb(logHandle); + } + } + case StorageConfig.SlateDb slateDb -> { + try (NativeInterop.ObjectStoreHandle objectStore = NativeInterop.resolveObjectStore(slateDb.objectStore())) { + NativeInterop.LogHandle logHandle = NativeInterop.logOpen( + 1, slateDb.path(), objectStore.segment(), slateDb.settingsPath(), sealIntervalMs); + return new LogDb(logHandle); + } + } } - return new LogDb(handle); } /** @@ -67,7 +79,21 @@ public static LogDb openInMemory() { */ public AppendResult tryAppend(Record[] records) { checkNotClosed(); - return nativeTryAppend(handle, records); + return NativeInterop.logTryAppend(handle, records); + } + + /** + * Appends a pre-built {@link RecordBatch} without blocking for queue space. + * + *

The batch is not closed by this method — the caller retains ownership. + * + * @param batch the record batch to append + * @return the result of the append operation (sequence of first record) + * @throws QueueFullException if the write queue is full + */ + public AppendResult tryAppend(RecordBatch batch) { + checkNotClosed(); + return NativeInterop.logTryAppend(handle, batch); } /** @@ -100,7 +126,23 @@ public AppendResult tryAppend(byte[] key, byte[] value) { */ public AppendResult appendTimeout(Record[] records, long timeoutMs) { checkNotClosed(); - return nativeAppendTimeout(handle, records, timeoutMs); + return NativeInterop.logAppendTimeout(handle, records, timeoutMs); + } + + /** + * Appends a pre-built {@link RecordBatch}, blocking up to {@code timeoutMs} for queue space. + * + *

The batch is not closed by this method — the caller retains ownership. + * + * @param batch the record batch to append + * @param timeoutMs maximum time to wait in milliseconds + * @return the result of the append operation (sequence of first record) + * @throws AppendTimeoutException if the timeout expires before queue space is available + * @throws QueueFullException if the write queue is full (unlikely with timeout) + */ + public AppendResult appendTimeout(RecordBatch batch, long timeoutMs) { + checkNotClosed(); + return NativeInterop.logAppendTimeout(handle, batch, timeoutMs); } /** @@ -118,10 +160,9 @@ public AppendResult appendTimeout(byte[] key, byte[] value, long timeoutMs) { } @Override - public List scan(byte[] key, long startSequence, int maxEntries) { + public LogScanRawIterator scanRaw(byte[] key, long startSequence) { checkNotClosed(); - LogEntry[] entries = nativeScan(handle, key, startSequence, maxEntries); - return entries != null ? List.of(entries) : List.of(); + return new LogScanRawIterator(NativeInterop.logScan(handle, key, startSequence)); } /** @@ -133,32 +174,17 @@ public List scan(byte[] key, long startSequence, int maxEntries) { */ public void flush() { checkNotClosed(); - nativeFlush(handle); + NativeInterop.logFlush(handle); } @Override public void close() { - if (!closed) { - closed = true; - nativeClose(handle); - } + handle.close(); } private void checkNotClosed() { - if (closed) { + if (handle.isClosed()) { throw new IllegalStateException("LogDb is closed"); } } - - long getHandle() { - return handle; - } - - // Native methods - private static native long nativeCreate(LogDbConfig config); - private static native AppendResult nativeTryAppend(long handle, Record[] records); - private static native AppendResult nativeAppendTimeout(long handle, Record[] records, long timeoutMs); - private static native LogEntry[] nativeScan(long handle, byte[] key, long startSequence, long maxEntries); - private static native void nativeFlush(long handle); - private static native void nativeClose(long handle); } diff --git a/log/src/main/java/dev/opendata/LogDbReader.java b/log/src/main/java/dev/opendata/LogDbReader.java index b11f949..d3cf23f 100644 --- a/log/src/main/java/dev/opendata/LogDbReader.java +++ b/log/src/main/java/dev/opendata/LogDbReader.java @@ -1,15 +1,15 @@ package dev.opendata; +import dev.opendata.common.StorageConfig; + import java.io.Closeable; -import java.util.List; /** * A read-only view of the log. * *

LogDbReader provides access to all read operations via the {@link LogRead} - * interface, but not write operations. Unlike {@link LogReader} which shares - * storage with a parent {@link LogDb}, LogDbReader opens storage independently - * and can coexist with a separate LogDb writer. + * interface, but not write operations. Unlike {@link LogDb} which has write access, + * LogDbReader opens storage independently and can coexist with a separate LogDb writer. * *

This is useful for: *

    @@ -20,25 +20,21 @@ * *

    Example

    *
    {@code
    - * LogDbConfig config = new LogDbConfig(new StorageConfig.SlateDb(...));
    + * LogDbReaderConfig config = new LogDbReaderConfig(new StorageConfig.SlateDb(...));
      * try (LogDbReader reader = LogDbReader.open(config)) {
    - *     List entries = reader.read(key, 0, 100);
    - *     for (LogEntry entry : entries) {
    - *         process(entry);
    + *     try (LogScanIterator iter = reader.scan(key, 0)) {
    + *         for (LogEntry entry : iter) {
    + *             process(entry);
    + *         }
      *     }
      * }
      * }
    */ public class LogDbReader implements Closeable, LogRead { - static { - System.loadLibrary("opendata_log_jni"); - } - - private final long handle; - private volatile boolean closed = false; + private final NativeInterop.ReaderHandle handle; - private LogDbReader(long handle) { + private LogDbReader(NativeInterop.ReaderHandle handle) { this.handle = handle; } @@ -56,47 +52,40 @@ public static LogDbReader open(LogDbReaderConfig config) { if (config == null) { throw new IllegalArgumentException("config must not be null"); } - long handle = nativeCreate(config); - if (handle == 0) { - throw new RuntimeException("Failed to create LogDbReader instance"); - } - return new LogDbReader(handle); - } - /** - * Scans entries from the log for the given key starting at a sequence number. - * - *

    Returns immediately with available entries, which may be fewer than requested - * or empty if no new entries are available. - * - * @param key the key to scan - * @param startSequence the sequence number to start scanning from - * @param maxEntries maximum number of entries to return - * @return list of log entries (may be empty) - */ - @Override - public List scan(byte[] key, long startSequence, int maxEntries) { - checkNotClosed(); - LogEntry[] entries = nativeScan(handle, key, startSequence, maxEntries); - return entries != null ? List.of(entries) : List.of(); - } + StorageConfig storage = config.storage(); + long refreshIntervalMs = config.refreshIntervalMs() != null + ? config.refreshIntervalMs() + : -1; - @Override - public void close() { - if (!closed) { - closed = true; - nativeClose(handle); + switch (storage) { + case StorageConfig.InMemory() -> { + try (NativeInterop.ObjectStoreHandle objectStore = NativeInterop.objectStoreInMemory()) { + NativeInterop.ReaderHandle readerHandle = NativeInterop.readerOpen( + 0, null, objectStore.segment(), null, refreshIntervalMs); + return new LogDbReader(readerHandle); + } + } + case StorageConfig.SlateDb slateDb -> { + try (NativeInterop.ObjectStoreHandle objectStore = NativeInterop.resolveObjectStore(slateDb.objectStore())) { + NativeInterop.ReaderHandle readerHandle = NativeInterop.readerOpen( + 1, slateDb.path(), objectStore.segment(), slateDb.settingsPath(), refreshIntervalMs); + return new LogDbReader(readerHandle); + } + } } } - private void checkNotClosed() { - if (closed) { + @Override + public LogScanRawIterator scanRaw(byte[] key, long startSequence) { + if (handle.isClosed()) { throw new IllegalStateException("LogDbReader is closed"); } + return new LogScanRawIterator(NativeInterop.readerScan(handle, key, startSequence)); } - // Native methods - private static native long nativeCreate(LogDbReaderConfig config); - private static native LogEntry[] nativeScan(long handle, byte[] key, long startSequence, long maxEntries); - private static native void nativeClose(long handle); + @Override + public void close() { + handle.close(); + } } diff --git a/log/src/main/java/dev/opendata/LogEntry.java b/log/src/main/java/dev/opendata/LogEntry.java index e8e936e..cd770ab 100644 --- a/log/src/main/java/dev/opendata/LogEntry.java +++ b/log/src/main/java/dev/opendata/LogEntry.java @@ -1,12 +1,10 @@ package dev.opendata; /** - * A single entry read from the log. + * An immutable log entry with heap-owned key and value data. * - * @param sequence the sequence number of this entry - * @param timestamp the timestamp (epoch millis) when this entry was appended - * @param key the key this entry was appended under - * @param value the value of this entry + *

    Unlike {@link LogEntryView}, the key and value arrays are safe to retain + * indefinitely. Instances are produced by {@link LogScanIterator}. */ public record LogEntry(long sequence, long timestamp, byte[] key, byte[] value) { } diff --git a/log/src/main/java/dev/opendata/LogEntryView.java b/log/src/main/java/dev/opendata/LogEntryView.java new file mode 100644 index 0000000..fb11d88 --- /dev/null +++ b/log/src/main/java/dev/opendata/LogEntryView.java @@ -0,0 +1,59 @@ +package dev.opendata; + +import dev.opendata.common.Bytes; + +/** + * A zero-copy view of a log entry backed by native memory. + * + *

    The {@link #key()} and {@link #value()} are valid only until the next + * call to {@code next()} on the owning iterator, or until the iterator is + * closed. After that, accessing the {@link Bytes} will throw + * {@link IllegalStateException}. + * + *

    To obtain a stable copy, call {@link Bytes#toArray()} on the key or value, + * or use {@link LogScanIterator} which copies automatically. + */ +public final class LogEntryView { + + private final long sequence; + private final long timestamp; + private final Bytes key; + private final Bytes value; + + LogEntryView(long sequence, long timestamp, Bytes key, Bytes value) { + this.sequence = sequence; + this.timestamp = timestamp; + this.key = key; + this.value = value; + } + + /** + * Returns the sequence number of this entry. + */ + public long sequence() { + return sequence; + } + + /** + * Returns the timestamp (epoch millis) when this entry was appended. + */ + public long timestamp() { + return timestamp; + } + + /** + * Returns the key as a zero-copy {@link Bytes} view. + * Valid until the next iterator advance or close. + */ + public Bytes key() { + return key; + } + + /** + * Returns the value as a zero-copy {@link Bytes} view. + * Valid until the next iterator advance or close. + */ + public Bytes value() { + return value; + } +} diff --git a/log/src/main/java/dev/opendata/LogRead.java b/log/src/main/java/dev/opendata/LogRead.java index 222219d..a809303 100644 --- a/log/src/main/java/dev/opendata/LogRead.java +++ b/log/src/main/java/dev/opendata/LogRead.java @@ -1,7 +1,5 @@ package dev.opendata; -import java.util.List; - /** * Interface for read operations on the log. * @@ -19,13 +17,28 @@ public interface LogRead { /** * Scans entries from the log for the given key starting at a sequence number. * - *

    Returns immediately with available entries, which may be fewer than requested - * or empty if no new entries are available. + *

    Returns a copying iterator that yields {@link LogEntry} records with + * heap-owned key and value arrays. The caller must close the iterator when + * done to release native resources. + * + * @param key the key to scan + * @param startSequence the sequence number to start scanning from + * @return a copying iterator over log entries (caller must close) + */ + default LogScanIterator scan(byte[] key, long startSequence) { + return new LogScanIterator(scanRaw(key, startSequence)); + } + + /** + * Scans entries from the log returning zero-copy views backed by native memory. + * + *

    Each {@link LogEntryView} returned by the iterator is valid only until the + * next call to {@code next()} or {@code close()}. Use {@link #scan(byte[], long)} + * for an iterator that automatically copies entries. * * @param key the key to scan * @param startSequence the sequence number to start scanning from - * @param maxEntries maximum number of entries to return - * @return list of log entries (may be empty) + * @return an iterator over zero-copy log entry views (caller must close) */ - List scan(byte[] key, long startSequence, int maxEntries); + LogScanRawIterator scanRaw(byte[] key, long startSequence); } diff --git a/log/src/main/java/dev/opendata/LogScanIterator.java b/log/src/main/java/dev/opendata/LogScanIterator.java new file mode 100644 index 0000000..f19a44e --- /dev/null +++ b/log/src/main/java/dev/opendata/LogScanIterator.java @@ -0,0 +1,78 @@ +package dev.opendata; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * A copying iterator over log scan results. + * + *

    Wraps a {@link LogScanRawIterator} and copies each entry's key and value to + * heap-owned {@code byte[]} arrays, producing {@link LogEntry} records that are + * safe to retain after the iterator advances. + * + *

    Implements {@link Iterator} for use with for-each loops and streams, and + * {@link AutoCloseable} to release the underlying native iterator. + * + *

    Note: This is a single-use {@link Iterable} — {@link #iterator()} returns + * {@code this}, so the iterator can only be traversed once. + * + *

    Usage: + *

    {@code
    + * try (LogScanIterator iter = log.scan(key, 0)) {
    + *     for (LogEntry entry : iter) {
    + *         process(entry.key(), entry.value());
    + *     }
    + * }
    + * }
    + */ +public final class LogScanIterator implements Iterator, AutoCloseable, Iterable { + + private final LogScanRawIterator inner; + private LogEntry prefetched; + private boolean done; + + LogScanIterator(LogScanRawIterator inner) { + this.inner = inner; + } + + @Override + public boolean hasNext() { + if (prefetched != null) { + return true; + } + if (done) { + return false; + } + LogEntryView view = inner.next(); + if (view == null) { + done = true; + return false; + } + prefetched = new LogEntry( + view.sequence(), + view.timestamp(), + view.key().toArray(), + view.value().toArray()); + return true; + } + + @Override + public LogEntry next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + LogEntry entry = prefetched; + prefetched = null; + return entry; + } + + @Override + public Iterator iterator() { + return this; + } + + @Override + public void close() { + inner.close(); + } +} diff --git a/log/src/main/java/dev/opendata/LogScanRawIterator.java b/log/src/main/java/dev/opendata/LogScanRawIterator.java new file mode 100644 index 0000000..e3e4383 --- /dev/null +++ b/log/src/main/java/dev/opendata/LogScanRawIterator.java @@ -0,0 +1,129 @@ +package dev.opendata; + +import dev.opendata.common.Bytes; + +import java.lang.foreign.MemorySegment; + +/** + * An iterator over log scan results returning zero-copy views. + * + *

    Wraps a native iterator handle and returns {@link LogEntryView} instances + * one at a time. Must be closed when done to release native resources. + * + *

    Each {@link LogEntryView} returned by {@link #next()} is a zero-copy view + * backed by native memory. The entry's {@link Bytes} fields are valid only + * until the next call to {@code next()} or {@code close()}. + * + *

    For an iterator that automatically copies entries to heap memory, use + * {@link LogScanIterator} via {@link LogRead#scan(byte[], long)}. + * + *

    Usage: + *

    {@code
    + * try (LogScanRawIterator iter = log.scanRaw(key, 0)) {
    + *     LogEntryView entry;
    + *     while ((entry = iter.next()) != null) {
    + *         // entry.key() and entry.value() are valid here
    + *         process(entry.key(), entry.value());
    + *     }
    + * }
    + * }
    + */ +public final class LogScanRawIterator implements AutoCloseable { + + private NativeInterop.IteratorHandle handle; + private boolean closed; + + // Track previous entry's native memory for deferred free + private MemorySegment pendingKeyPtr; + private long pendingKeyLen; + private MemorySegment pendingValuePtr; + private long pendingValueLen; + private LogEntryView currentEntry; + + LogScanRawIterator(NativeInterop.IteratorHandle handle) { + this.handle = handle; + } + + /** + * Returns a zero-copy view of the next log entry, or {@code null} when exhausted. + * + *

    The returned entry's {@link Bytes} fields are backed by native memory and + * are valid only until the next call to {@code next()} or {@code close()}. + * + * @return the next entry view, or null if no more entries + */ + public LogEntryView next() { + if (closed) { + throw new IllegalStateException("Iterator is closed"); + } + freePending(); + invalidateCurrent(); + + NativeInterop.RawIteratorResult raw = NativeInterop.iteratorNextRaw(handle); + if (!raw.present()) { + return null; + } + + // Wrap key as read-only Bytes (zero-copy) + Bytes key = new Bytes(raw.keyPtr().reinterpret(raw.keyLen()).asReadOnly()); + + // Wrap value payload (after timestamp header) as read-only Bytes + Bytes value; + long valueLen = raw.valueLen(); + if (valueLen >= NativeInterop.TIMESTAMP_HEADER_SIZE) { + long payloadLen = valueLen - NativeInterop.TIMESTAMP_HEADER_SIZE; + if (payloadLen > 0) { + value = new Bytes(raw.valuePtr().reinterpret(valueLen) + .asSlice(NativeInterop.TIMESTAMP_HEADER_SIZE, payloadLen).asReadOnly()); + } else { + value = new Bytes(MemorySegment.ofArray(new byte[0])); + } + } else { + if (valueLen > 0) { + value = new Bytes(raw.valuePtr().reinterpret(valueLen).asReadOnly()); + } else { + value = new Bytes(MemorySegment.ofArray(new byte[0])); + } + } + + currentEntry = new LogEntryView(raw.sequence(), raw.timestamp(), key, value); + pendingKeyPtr = raw.keyPtr(); + pendingKeyLen = raw.keyLen(); + pendingValuePtr = raw.valuePtr(); + pendingValueLen = raw.valueLen(); + return currentEntry; + } + + @Override + public void close() { + if (closed) { + return; + } + freePending(); + invalidateCurrent(); + handle.close(); + handle = null; + closed = true; + } + + private void freePending() { + if (pendingKeyPtr != null) { + NativeInterop.freeBytes(pendingKeyPtr, pendingKeyLen); + pendingKeyPtr = null; + pendingKeyLen = 0; + } + if (pendingValuePtr != null) { + NativeInterop.freeBytes(pendingValuePtr, pendingValueLen); + pendingValuePtr = null; + pendingValueLen = 0; + } + } + + private void invalidateCurrent() { + if (currentEntry != null) { + currentEntry.key().invalidate(); + currentEntry.value().invalidate(); + currentEntry = null; + } + } +} diff --git a/log/src/main/java/dev/opendata/NativeInterop.java b/log/src/main/java/dev/opendata/NativeInterop.java new file mode 100644 index 0000000..65cf488 --- /dev/null +++ b/log/src/main/java/dev/opendata/NativeInterop.java @@ -0,0 +1,518 @@ +package dev.opendata; + +import dev.opendata.common.AppendTimeoutException; +import dev.opendata.common.OpenDataNativeException; +import dev.opendata.common.QueueFullException; +import dev.opendata.ffi.Native; +import dev.opendata.ffi.opendata_log_config_t; +import dev.opendata.ffi.opendata_log_reader_config_t; +import dev.opendata.ffi.opendata_log_seq_bound_t; +import dev.opendata.ffi.opendata_log_seq_range_t; +import dev.opendata.ffi.opendata_log_result_t; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import dev.opendata.common.ObjectStoreConfig; + +/** + * Panama FFM interop layer for the opendata-log C library. + * + *

    This class provides the bridge between Java and the native C library, + * handling memory management, error checking, and data marshaling. It follows + * the same patterns as slatedb-java's NativeInterop. + * + *

    Timestamp Header

    + *

    The Java layer prepends an 8-byte big-endian timestamp to each value + * before passing it to the C library. On read, the timestamp is extracted + * and the original payload is returned. This enables end-to-end latency + * measurement for benchmarking (e.g., openmessaging benchmark). + * + *

    + * ┌─────────────────────┬──────────────────────┐
    + * │ timestamp_ms (8B)   │ original payload     │
    + * │ big-endian i64      │                      │
    + * └─────────────────────┴──────────────────────┘
    + * 
    + */ +final class NativeInterop { + + static final int TIMESTAMP_HEADER_SIZE = 8; + + // Result kind codes from the C API (opendata_log_result_t.kind) + private static final int OPENDATA_LOG_OK = 0; + private static final int OPENDATA_LOG_ERROR_QUEUE_FULL = 5; + private static final int OPENDATA_LOG_ERROR_TIMEOUT = 6; + + private NativeInterop() { + } + + // ========================================================================= + // Handle classes + // ========================================================================= + + static abstract class NativeHandle implements AutoCloseable { + + private final String handleType; + private final AtomicBoolean closed = new AtomicBoolean(false); + private volatile MemorySegment segment; + + NativeHandle(String handleType, MemorySegment segment) { + this.handleType = Objects.requireNonNull(handleType, "handleType"); + this.segment = requireNativeHandle(segment, handleType); + } + + final MemorySegment segment() { + MemorySegment current = segment; + if (closed.get() || current.equals(MemorySegment.NULL)) { + throw new IllegalStateException(handleType + " is closed"); + } + return current; + } + + final boolean isClosed() { + return closed.get(); + } + + @Override + public final void close() { + if (!closed.compareAndSet(false, true)) { + return; + } + MemorySegment current = segment; + try { + closeNative(current); + } finally { + segment = MemorySegment.NULL; + } + } + + protected abstract void closeNative(MemorySegment segment); + } + + static final class ObjectStoreHandle extends NativeHandle { + ObjectStoreHandle(MemorySegment segment) { + super("ObjectStore", segment); + } + + @Override + protected void closeNative(MemorySegment segment) { + try (Arena arena = Arena.ofConfined()) { + checkResult(Native.opendata_log_object_store_close(arena, segment)); + } + } + } + + static final class LogHandle extends NativeHandle { + LogHandle(MemorySegment segment) { + super("Log", segment); + } + + @Override + protected void closeNative(MemorySegment segment) { + try (Arena arena = Arena.ofConfined()) { + checkResult(Native.opendata_log_close(arena, segment)); + } + } + } + + static final class ReaderHandle extends NativeHandle { + ReaderHandle(MemorySegment segment) { + super("Reader", segment); + } + + @Override + protected void closeNative(MemorySegment segment) { + try (Arena arena = Arena.ofConfined()) { + checkResult(Native.opendata_log_reader_close(arena, segment)); + } + } + } + + static final class IteratorHandle extends NativeHandle { + IteratorHandle(MemorySegment segment) { + super("Iterator", segment); + } + + @Override + protected void closeNative(MemorySegment segment) { + try (Arena arena = Arena.ofConfined()) { + checkResult(Native.opendata_log_iterator_close(arena, segment)); + } + } + } + + // ========================================================================= + // Iterator result + // ========================================================================= + + record RawIteratorResult( + boolean present, long sequence, long timestamp, + MemorySegment keyPtr, long keyLen, + MemorySegment valuePtr, long valueLen + ) { + } + + // ========================================================================= + // Object store factory methods + // ========================================================================= + + static ObjectStoreHandle objectStoreInMemory() { + try (Arena arena = Arena.ofConfined()) { + MemorySegment outStore = arena.allocate(Native.C_POINTER); + checkResult(Native.opendata_log_object_store_in_memory(arena, outStore)); + return new ObjectStoreHandle(outStore.get(Native.C_POINTER, 0)); + } + } + + static ObjectStoreHandle objectStoreLocal(String path) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment outStore = arena.allocate(Native.C_POINTER); + MemorySegment nativePath = marshalCString(arena, path); + checkResult(Native.opendata_log_object_store_local(arena, nativePath, outStore)); + return new ObjectStoreHandle(outStore.get(Native.C_POINTER, 0)); + } + } + + static ObjectStoreHandle objectStoreAws(String region, String bucket) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment outStore = arena.allocate(Native.C_POINTER); + MemorySegment nativeRegion = marshalCString(arena, region); + MemorySegment nativeBucket = marshalCString(arena, bucket); + checkResult(Native.opendata_log_object_store_aws(arena, nativeRegion, nativeBucket, outStore)); + return new ObjectStoreHandle(outStore.get(Native.C_POINTER, 0)); + } + } + + static ObjectStoreHandle resolveObjectStore(ObjectStoreConfig config) { + return switch (config) { + case ObjectStoreConfig.InMemory() -> objectStoreInMemory(); + case ObjectStoreConfig.Aws aws -> objectStoreAws(aws.region(), aws.bucket()); + case ObjectStoreConfig.Local local -> objectStoreLocal(local.path()); + }; + } + + // ========================================================================= + // Log operations + // ========================================================================= + + static LogHandle logOpen(int storageType, String slatedbPath, + MemorySegment objectStore, String settingsPath, + long sealIntervalMs) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment config = opendata_log_config_t.allocate(arena); + opendata_log_config_t.storage_type(config, (byte) storageType); + opendata_log_config_t.slatedb_path(config, marshalNullableCString(arena, slatedbPath)); + opendata_log_config_t.object_store(config, objectStore); + opendata_log_config_t.settings_path(config, marshalNullableCString(arena, settingsPath)); + opendata_log_config_t.seal_interval_ms(config, sealIntervalMs); + + MemorySegment outLog = arena.allocate(Native.C_POINTER); + checkResult(Native.opendata_log_open(arena, config, outLog)); + return new LogHandle(outLog.get(Native.C_POINTER, 0)); + } + } + + static void logFlush(LogHandle handle) { + try (Arena arena = Arena.ofConfined()) { + checkResult(Native.opendata_log_flush(arena, handle.segment())); + } + } + + static AppendResult logTryAppend(LogHandle handle, Record[] records) { + return doAppend(handle.segment(), records, (arena, seg, keys, keyLens, vals, valLens, count, outSeq) -> + Native.opendata_log_try_append(arena, seg, keys, keyLens, vals, valLens, count, outSeq)); + } + + static AppendResult logTryAppend(LogHandle handle, RecordBatch batch) { + return doAppendBatch(handle.segment(), batch, (arena, seg, keys, keyLens, vals, valLens, count, outSeq) -> + Native.opendata_log_try_append(arena, seg, keys, keyLens, vals, valLens, count, outSeq)); + } + + static AppendResult logAppendTimeout(LogHandle handle, Record[] records, long timeoutMs) { + return doAppend(handle.segment(), records, (arena, seg, keys, keyLens, vals, valLens, count, outSeq) -> + Native.opendata_log_append_timeout(arena, seg, keys, keyLens, vals, valLens, count, timeoutMs, outSeq)); + } + + static AppendResult logAppendTimeout(LogHandle handle, RecordBatch batch, long timeoutMs) { + return doAppendBatch(handle.segment(), batch, (arena, seg, keys, keyLens, vals, valLens, count, outSeq) -> + Native.opendata_log_append_timeout(arena, seg, keys, keyLens, vals, valLens, count, timeoutMs, outSeq)); + } + + static IteratorHandle logScan(LogHandle handle, byte[] key, long startSequence) { + return doScan(handle.segment(), key, startSequence, Native::opendata_log_scan); + } + + // ========================================================================= + // Reader operations + // ========================================================================= + + static ReaderHandle readerOpen(int storageType, String slatedbPath, + MemorySegment objectStore, String settingsPath, + long refreshIntervalMs) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment config = opendata_log_reader_config_t.allocate(arena); + opendata_log_reader_config_t.storage_type(config, (byte) storageType); + opendata_log_reader_config_t.slatedb_path(config, marshalNullableCString(arena, slatedbPath)); + opendata_log_reader_config_t.object_store(config, objectStore); + opendata_log_reader_config_t.settings_path(config, marshalNullableCString(arena, settingsPath)); + opendata_log_reader_config_t.refresh_interval_ms(config, refreshIntervalMs); + + MemorySegment outReader = arena.allocate(Native.C_POINTER); + checkResult(Native.opendata_log_reader_open(arena, config, outReader)); + return new ReaderHandle(outReader.get(Native.C_POINTER, 0)); + } + } + + static IteratorHandle readerScan(ReaderHandle handle, byte[] key, long startSequence) { + return doScan(handle.segment(), key, startSequence, Native::opendata_log_reader_scan); + } + + // ========================================================================= + // Iterator operations + // ========================================================================= + + static RawIteratorResult iteratorNextRaw(IteratorHandle handle) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment outPresent = arena.allocate(ValueLayout.JAVA_BOOLEAN); + MemorySegment outKey = arena.allocate(Native.C_POINTER); + MemorySegment outKeyLen = arena.allocate(Native.C_LONG); + MemorySegment outSequence = arena.allocate(ValueLayout.JAVA_LONG); + MemorySegment outValue = arena.allocate(Native.C_POINTER); + MemorySegment outValueLen = arena.allocate(Native.C_LONG); + + checkResult(Native.opendata_log_iterator_next(arena, handle.segment(), + outPresent, outKey, outKeyLen, outSequence, outValue, outValueLen)); + + boolean present = outPresent.get(ValueLayout.JAVA_BOOLEAN, 0); + if (!present) { + return new RawIteratorResult(false, 0, 0, + MemorySegment.NULL, 0, MemorySegment.NULL, 0); + } + + long sequence = outSequence.get(ValueLayout.JAVA_LONG, 0); + MemorySegment keyPtr = outKey.get(Native.C_POINTER, 0); + long keyLen = outKeyLen.get(Native.C_LONG, 0); + MemorySegment valuePtr = outValue.get(Native.C_POINTER, 0); + long valueLen = outValueLen.get(Native.C_LONG, 0); + + // Extract timestamp from the value segment header + long timestamp = 0; + if (valueLen >= TIMESTAMP_HEADER_SIZE) { + MemorySegment valueSeg = valuePtr.reinterpret(valueLen); + timestamp = Long.reverseBytes( + valueSeg.get(ValueLayout.JAVA_LONG_UNALIGNED, 0)); + } + + return new RawIteratorResult(true, sequence, timestamp, + keyPtr, keyLen, valuePtr, valueLen); + } + } + + static void freeBytes(MemorySegment ptr, long len) { + if (!ptr.equals(MemorySegment.NULL) && len > 0) { + Native.opendata_log_bytes_free(ptr, len); + } + } + + // ========================================================================= + // Shared append / scan helpers + // ========================================================================= + + @FunctionalInterface + private interface AppendCall { + MemorySegment invoke(Arena arena, MemorySegment handle, + MemorySegment keys, MemorySegment keyLens, + MemorySegment values, MemorySegment valueLens, + long count, MemorySegment outSeq); + } + + private static AppendResult doAppend(MemorySegment handle, Record[] records, AppendCall call) { + int count = records.length; + try (Arena arena = Arena.ofConfined()) { + MemorySegment nativeKeys = arena.allocate(Native.C_POINTER, count); + MemorySegment keyLens = arena.allocate(Native.C_LONG, count); + MemorySegment nativeValues = arena.allocate(Native.C_POINTER, count); + MemorySegment valueLens = arena.allocate(Native.C_LONG, count); + + marshalRecords(arena, records, nativeKeys, keyLens, nativeValues, valueLens); + + MemorySegment outSeq = arena.allocate(ValueLayout.JAVA_LONG); + checkResult(call.invoke(arena, handle, + nativeKeys, keyLens, nativeValues, valueLens, count, outSeq)); + + long startSeq = outSeq.get(ValueLayout.JAVA_LONG, 0); + return new AppendResult(startSeq, records[0].timestampMs()); + } + } + + private static AppendResult doAppendBatch(MemorySegment handle, RecordBatch batch, AppendCall call) { + int count = batch.count(); + if (count == 0) { + throw new IllegalArgumentException("batch must not be empty"); + } + try (Arena arena = Arena.ofConfined()) { + MemorySegment nativeKeys = arena.allocate(Native.C_POINTER, count); + MemorySegment keyLens = arena.allocate(Native.C_LONG, count); + MemorySegment nativeValues = arena.allocate(Native.C_POINTER, count); + MemorySegment valueLens = arena.allocate(Native.C_LONG, count); + + long[] batchKeyOffsets = batch.keyOffsets(); + long[] batchKeyLengths = batch.keyLengths(); + long[] batchValueOffsets = batch.valueOffsets(); + long[] batchValueLengths = batch.valueLengths(); + MemorySegment batchKeysData = batch.keysData(); + MemorySegment batchValuesData = batch.valuesData(); + + for (int i = 0; i < count; i++) { + nativeKeys.setAtIndex(Native.C_POINTER, i, + batchKeysData.asSlice(batchKeyOffsets[i], batchKeyLengths[i])); + keyLens.setAtIndex(Native.C_LONG, i, batchKeyLengths[i]); + nativeValues.setAtIndex(Native.C_POINTER, i, + batchValuesData.asSlice(batchValueOffsets[i], batchValueLengths[i])); + valueLens.setAtIndex(Native.C_LONG, i, batchValueLengths[i]); + } + + MemorySegment outSeq = arena.allocate(ValueLayout.JAVA_LONG); + checkResult(call.invoke(arena, handle, + nativeKeys, keyLens, nativeValues, valueLens, count, outSeq)); + + long startSeq = outSeq.get(ValueLayout.JAVA_LONG, 0); + return new AppendResult(startSeq, batch.firstTimestampMs()); + } + } + + @FunctionalInterface + private interface ScanCall { + MemorySegment invoke(Arena arena, MemorySegment handle, + MemorySegment key, long keyLen, + MemorySegment seqRange, MemorySegment outIterator); + } + + private static IteratorHandle doScan(MemorySegment handle, byte[] key, + long startSequence, ScanCall call) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment nativeKey = marshalBytes(arena, key); + MemorySegment seqRange = marshalSeqRange(arena, startSequence); + MemorySegment outIterator = arena.allocate(Native.C_POINTER); + checkResult(call.invoke(arena, handle, nativeKey, key.length, seqRange, outIterator)); + return new IteratorHandle(outIterator.get(Native.C_POINTER, 0)); + } + } + + // ========================================================================= + // Memory marshaling helpers + // ========================================================================= + + private static MemorySegment marshalBytes(Arena arena, byte[] bytes) { + Objects.requireNonNull(bytes, "bytes"); + if (bytes.length == 0) { + return MemorySegment.NULL; + } + MemorySegment nativeBytes = arena.allocate(bytes.length, 1); + MemorySegment.copy(MemorySegment.ofArray(bytes), 0, nativeBytes, 0, bytes.length); + return nativeBytes; + } + + private static MemorySegment marshalCString(Arena arena, String value) { + Objects.requireNonNull(value, "value"); + byte[] utf8 = value.getBytes(StandardCharsets.UTF_8); + MemorySegment nativeString = arena.allocate(utf8.length + 1L, 1); + if (utf8.length > 0) { + MemorySegment.copy(MemorySegment.ofArray(utf8), 0, nativeString, 0, utf8.length); + } + nativeString.set(ValueLayout.JAVA_BYTE, utf8.length, (byte) 0); + return nativeString; + } + + private static MemorySegment marshalNullableCString(Arena arena, String value) { + if (value == null) { + return MemorySegment.NULL; + } + return marshalCString(arena, value); + } + + private static void marshalRecords(Arena arena, Record[] records, + MemorySegment nativeKeys, MemorySegment keyLens, + MemorySegment nativeValues, MemorySegment valueLens) { + for (int i = 0; i < records.length; i++) { + byte[] key = records[i].key(); + MemorySegment keyData = marshalBytes(arena, key); + nativeKeys.setAtIndex(Native.C_POINTER, i, keyData); + keyLens.setAtIndex(Native.C_LONG, i, key.length); + + // Write timestamp header + value directly into arena, avoiding intermediate byte[] + byte[] value = records[i].value(); + long totalLen = TIMESTAMP_HEADER_SIZE + value.length; + MemorySegment valueData = arena.allocate(totalLen, 1); + valueData.set(ValueLayout.JAVA_LONG_UNALIGNED, 0, + Long.reverseBytes(records[i].timestampMs())); // big-endian + MemorySegment.copy(MemorySegment.ofArray(value), 0, + valueData, TIMESTAMP_HEADER_SIZE, value.length); + nativeValues.setAtIndex(Native.C_POINTER, i, valueData); + valueLens.setAtIndex(Native.C_LONG, i, totalLen); + } + } + + private static MemorySegment marshalSeqRange(Arena arena, long startSequence) { + MemorySegment seqRange = opendata_log_seq_range_t.allocate(arena); + + MemorySegment start = opendata_log_seq_range_t.start(seqRange); + opendata_log_seq_bound_t.kind(start, (byte) Native.OPENDATA_LOG_BOUND_INCLUDED()); + opendata_log_seq_bound_t.value(start, startSequence); + + MemorySegment end = opendata_log_seq_range_t.end(seqRange); + opendata_log_seq_bound_t.kind(end, (byte) Native.OPENDATA_LOG_BOUND_UNBOUNDED()); + opendata_log_seq_bound_t.value(end, 0); + + return seqRange; + } + + // ========================================================================= + // Error handling + // ========================================================================= + + private static MemorySegment requireNativeHandle(MemorySegment segment, String handleType) { + if (segment == null || segment.equals(MemorySegment.NULL)) { + throw new IllegalStateException("Failed to create " + handleType + " handle: null pointer"); + } + return segment; + } + + private static void checkResult(MemorySegment result) { + int kindCode = opendata_log_result_t.kind(result); + + if (kindCode == OPENDATA_LOG_OK) { + Native.opendata_log_result_free(result); + return; + } + + String message; + try { + MemorySegment msgPtr = opendata_log_result_t.message(result); + if (!msgPtr.equals(MemorySegment.NULL)) { + message = msgPtr.reinterpret(Long.MAX_VALUE).getString(0, StandardCharsets.UTF_8); + } else { + message = "Unknown error (kind=" + kindCode + ")"; + } + } finally { + Native.opendata_log_result_free(result); + } + + throw mapError(kindCode, message); + } + + static RuntimeException mapError(int kindCode, String message) { + if (kindCode == OPENDATA_LOG_ERROR_QUEUE_FULL) { + return new QueueFullException(message); + } else if (kindCode == OPENDATA_LOG_ERROR_TIMEOUT) { + return new AppendTimeoutException(message); + } else { + return new OpenDataNativeException(message); + } + } +} diff --git a/log/src/main/java/dev/opendata/RecordBatch.java b/log/src/main/java/dev/opendata/RecordBatch.java new file mode 100644 index 0000000..b78fc05 --- /dev/null +++ b/log/src/main/java/dev/opendata/RecordBatch.java @@ -0,0 +1,215 @@ +package dev.opendata; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.util.Arrays; +import java.util.Objects; + +/** + * A builder that accumulates records into contiguous native memory segments, + * eliminating per-append copy overhead when writing to {@link LogDb}. + * + *

    Records are written into two arena-backed {@link MemorySegment}s — one for + * keys and one for values. Values are stored with an 8-byte big-endian timestamp + * header already prepended, matching the format expected by the native layer. + * + *

    Typical usage: + *

    {@code
    + * try (RecordBatch batch = RecordBatch.create()) {
    + *     for (...) {
    + *         batch.add(key, value);
    + *     }
    + *     AppendResult result = log.tryAppend(batch);
    + * }
    + * }
    + * + *

    The batch is not closed by append methods — the caller retains ownership. + */ +public final class RecordBatch implements AutoCloseable { + + private static final long DEFAULT_DATA_CAPACITY = 4096; + private static final int DEFAULT_RECORD_CAPACITY = 64; + + private final Arena arena; + + private MemorySegment keysData; + private long keysOffset; + private MemorySegment valuesData; + private long valuesOffset; + + private long[] keyOffsets; + private long[] keyLengths; + private long[] valueOffsets; + private long[] valueLengths; + + private int count; + private long firstTimestampMs; + private boolean closed; + + private RecordBatch(Arena arena, long dataCapacity, int recordCapacity) { + this.arena = arena; + this.keysData = arena.allocate(dataCapacity, 1); + this.valuesData = arena.allocate(dataCapacity, 1); + this.keyOffsets = new long[recordCapacity]; + this.keyLengths = new long[recordCapacity]; + this.valueOffsets = new long[recordCapacity]; + this.valueLengths = new long[recordCapacity]; + } + + /** + * Creates a new batch with default capacities. + */ + public static RecordBatch create() { + return create(DEFAULT_DATA_CAPACITY, DEFAULT_RECORD_CAPACITY); + } + + /** + * Creates a new batch with specified capacities. + * + * @param dataCapacity initial byte capacity for key and value segments + * @param recordCapacity initial number of records the batch can hold before growing + */ + public static RecordBatch create(long dataCapacity, int recordCapacity) { + if (dataCapacity <= 0) { + throw new IllegalArgumentException("dataCapacity must be positive"); + } + if (recordCapacity <= 0) { + throw new IllegalArgumentException("recordCapacity must be positive"); + } + return new RecordBatch(Arena.ofConfined(), dataCapacity, recordCapacity); + } + + /** + * Adds a record with the current wall-clock time as timestamp. + */ + public void add(byte[] key, byte[] value) { + add(key, value, System.currentTimeMillis()); + } + + /** + * Adds a record with an explicit timestamp. + * + * @param key the record key + * @param value the record value payload + * @param timestampMs wall-clock time (epoch millis) + */ + public void add(byte[] key, byte[] value, long timestampMs) { + checkNotClosed(); + Objects.requireNonNull(key, "key must not be null"); + Objects.requireNonNull(value, "value must not be null"); + + // Grow record arrays if needed + if (count == keyOffsets.length) { + int newCap = keyOffsets.length * 2; + keyOffsets = Arrays.copyOf(keyOffsets, newCap); + keyLengths = Arrays.copyOf(keyLengths, newCap); + valueOffsets = Arrays.copyOf(valueOffsets, newCap); + valueLengths = Arrays.copyOf(valueLengths, newCap); + } + + // Copy key into keys segment + long keyLen = key.length; + keysData = ensureCapacity(keysData, keysOffset, keyLen); + MemorySegment.copy(MemorySegment.ofArray(key), 0, keysData, keysOffset, keyLen); + keyOffsets[count] = keysOffset; + keyLengths[count] = keyLen; + keysOffset += keyLen; + + // Copy timestamp header + value into values segment + long totalValueLen = NativeInterop.TIMESTAMP_HEADER_SIZE + value.length; + valuesData = ensureCapacity(valuesData, valuesOffset, totalValueLen); + valuesData.set(ValueLayout.JAVA_LONG_UNALIGNED, valuesOffset, + Long.reverseBytes(timestampMs)); + if (value.length > 0) { + MemorySegment.copy(MemorySegment.ofArray(value), 0, + valuesData, valuesOffset + NativeInterop.TIMESTAMP_HEADER_SIZE, value.length); + } + valueOffsets[count] = valuesOffset; + valueLengths[count] = totalValueLen; + valuesOffset += totalValueLen; + + if (count == 0) { + firstTimestampMs = timestampMs; + } + count++; + } + + /** + * Returns the number of records in this batch. + */ + public int count() { + return count; + } + + /** + * Returns {@code true} if this batch contains no records. + */ + public boolean isEmpty() { + return count == 0; + } + + /** + * Returns the timestamp of the first record added, or 0 if empty. + */ + public long firstTimestampMs() { + return firstTimestampMs; + } + + @Override + public void close() { + if (!closed) { + closed = true; + arena.close(); + } + } + + // Package-private accessors for NativeInterop + MemorySegment keysData() { + return keysData; + } + + long[] keyOffsets() { + return keyOffsets; + } + + long[] keyLengths() { + return keyLengths; + } + + MemorySegment valuesData() { + return valuesData; + } + + long[] valueOffsets() { + return valueOffsets; + } + + long[] valueLengths() { + return valueLengths; + } + + private void checkNotClosed() { + if (closed) { + throw new IllegalStateException("RecordBatch is closed"); + } + } + + /** + * Ensures the segment has enough room for {@code needed} more bytes starting at {@code offset}. + * If not, allocates a 2x segment and copies existing data. + */ + private MemorySegment ensureCapacity(MemorySegment segment, long offset, long needed) { + long required = offset + needed; + if (required <= segment.byteSize()) { + return segment; + } + long newSize = segment.byteSize(); + while (newSize < required) { + newSize *= 2; + } + MemorySegment grown = arena.allocate(newSize, 1); + MemorySegment.copy(segment, 0, grown, 0, offset); + return grown; + } +} diff --git a/log/src/test/java/dev/opendata/LogDbIntegrationTest.java b/log/src/test/java/dev/opendata/LogDbIntegrationTest.java index e18ae57..0e72157 100644 --- a/log/src/test/java/dev/opendata/LogDbIntegrationTest.java +++ b/log/src/test/java/dev/opendata/LogDbIntegrationTest.java @@ -2,26 +2,34 @@ import dev.opendata.common.ObjectStoreConfig; import dev.opendata.common.StorageConfig; +import dev.opendata.common.Bytes; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** - * Integration tests for LogDb that exercise the native JNI bindings. + * Integration tests for LogDb that exercise the Panama FFM bindings. * - *

    These tests require the native library to be built. Run: + *

    These tests require the native C library to be built. Run: *

    - *   cd log/native && cargo build --release
    + *   cd ../opendata/log/c && cargo build --release
      * 
    */ class LogDbIntegrationTest { + private static List collect(LogScanIterator iter) { + List entries = new ArrayList<>(); + iter.forEachRemaining(entries::add); + return entries; + } + @Test void shouldOpenAndCloseInMemoryLog() { try (LogDb log = LogDb.openInMemory()) { @@ -47,11 +55,13 @@ void shouldAppendAndReadSingleRecord() { assertThat(result.sequence()).isEqualTo(0); - List entries = log.scan(key, 0, 10); - assertThat(entries).hasSize(1); - assertThat(entries.get(0).sequence()).isEqualTo(0); - assertThat(entries.get(0).key()).isEqualTo(key); - assertThat(entries.get(0).value()).isEqualTo(value); + try (LogScanIterator iter = log.scan(key, 0)) { + List entries = collect(iter); + assertThat(entries).hasSize(1); + assertThat(entries.get(0).sequence()).isEqualTo(0); + assertThat(entries.get(0).key()).isEqualTo(key); + assertThat(entries.get(0).value()).isEqualTo(value); + } } } @@ -69,14 +79,16 @@ void shouldAppendBatchOfRecords() { assertThat(result.sequence()).isEqualTo(0); - List entries = log.scan(key, 0, 10); - assertThat(entries).hasSize(3); - assertThat(entries.get(0).sequence()).isEqualTo(0); - assertThat(entries.get(1).sequence()).isEqualTo(1); - assertThat(entries.get(2).sequence()).isEqualTo(2); - assertThat(new String(entries.get(0).value(), StandardCharsets.UTF_8)).isEqualTo("value-0"); - assertThat(new String(entries.get(1).value(), StandardCharsets.UTF_8)).isEqualTo("value-1"); - assertThat(new String(entries.get(2).value(), StandardCharsets.UTF_8)).isEqualTo("value-2"); + try (LogScanIterator iter = log.scan(key, 0)) { + List entries = collect(iter); + assertThat(entries).hasSize(3); + assertThat(entries.get(0).sequence()).isEqualTo(0); + assertThat(entries.get(1).sequence()).isEqualTo(1); + assertThat(entries.get(2).sequence()).isEqualTo(2); + assertThat(new String(entries.get(0).value(), StandardCharsets.UTF_8)).isEqualTo("value-0"); + assertThat(new String(entries.get(1).value(), StandardCharsets.UTF_8)).isEqualTo("value-1"); + assertThat(new String(entries.get(2).value(), StandardCharsets.UTF_8)).isEqualTo("value-2"); + } } } @@ -91,11 +103,13 @@ void shouldAssignSequentialSequencesAcrossAppends() { assertThat(third.sequence()).isEqualTo(2); - List entries = log.scan(key, 0, 10); - assertThat(entries).hasSize(3); - assertThat(entries.get(0).sequence()).isEqualTo(0); - assertThat(entries.get(1).sequence()).isEqualTo(1); - assertThat(entries.get(2).sequence()).isEqualTo(2); + try (LogScanIterator iter = log.scan(key, 0)) { + List entries = collect(iter); + assertThat(entries).hasSize(3); + assertThat(entries.get(0).sequence()).isEqualTo(0); + assertThat(entries.get(1).sequence()).isEqualTo(1); + assertThat(entries.get(2).sequence()).isEqualTo(2); + } } } @@ -109,15 +123,17 @@ void shouldReadFromStartSequence() { log.tryAppend(key, "value-2".getBytes(StandardCharsets.UTF_8)); // Read starting from sequence 1 - List entries = log.scan(key, 1, 10); - assertThat(entries).hasSize(2); - assertThat(entries.get(0).sequence()).isEqualTo(1); - assertThat(entries.get(1).sequence()).isEqualTo(2); + try (LogScanIterator iter = log.scan(key, 1)) { + List entries = collect(iter); + assertThat(entries).hasSize(2); + assertThat(entries.get(0).sequence()).isEqualTo(1); + assertThat(entries.get(1).sequence()).isEqualTo(2); + } } } @Test - void shouldRespectMaxEntries() { + void shouldIteratePartiallyAndCloseEarly() { try (LogDb log = LogDb.openInMemory()) { byte[] key = "limit-key".getBytes(StandardCharsets.UTF_8); @@ -125,20 +141,28 @@ void shouldRespectMaxEntries() { log.tryAppend(key, ("value-" + i).getBytes(StandardCharsets.UTF_8)); } - List entries = log.scan(key, 0, 3); - assertThat(entries).hasSize(3); + // Iterate only 3 entries then close early + try (LogScanIterator iter = log.scan(key, 0)) { + List entries = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + assertThat(iter.hasNext()).isTrue(); + entries.add(iter.next()); + } + assertThat(entries).hasSize(3); + } } } @Test - void shouldReturnEmptyListForUnknownKey() { + void shouldReturnEmptyForUnknownKey() { try (LogDb log = LogDb.openInMemory()) { byte[] key = "known".getBytes(StandardCharsets.UTF_8); log.tryAppend(key, "value".getBytes(StandardCharsets.UTF_8)); byte[] unknownKey = "unknown".getBytes(StandardCharsets.UTF_8); - List entries = log.scan(unknownKey, 0, 10); - assertThat(entries).isEmpty(); + try (LogScanIterator iter = log.scan(unknownKey, 0)) { + assertThat(iter.hasNext()).isFalse(); + } } } @@ -152,14 +176,18 @@ void shouldIsolateEntriesByKey() { log.tryAppend(keyB, "value-b-0".getBytes(StandardCharsets.UTF_8)); log.tryAppend(keyA, "value-a-1".getBytes(StandardCharsets.UTF_8)); - List entriesA = log.scan(keyA, 0, 10); - assertThat(entriesA).hasSize(2); - assertThat(new String(entriesA.get(0).value(), StandardCharsets.UTF_8)).isEqualTo("value-a-0"); - assertThat(new String(entriesA.get(1).value(), StandardCharsets.UTF_8)).isEqualTo("value-a-1"); + try (LogScanIterator iter = log.scan(keyA, 0)) { + List entriesA = collect(iter); + assertThat(entriesA).hasSize(2); + assertThat(new String(entriesA.get(0).value(), StandardCharsets.UTF_8)).isEqualTo("value-a-0"); + assertThat(new String(entriesA.get(1).value(), StandardCharsets.UTF_8)).isEqualTo("value-a-1"); + } - List entriesB = log.scan(keyB, 0, 10); - assertThat(entriesB).hasSize(1); - assertThat(new String(entriesB.get(0).value(), StandardCharsets.UTF_8)).isEqualTo("value-b-0"); + try (LogScanIterator iter = log.scan(keyB, 0)) { + List entriesB = collect(iter); + assertThat(entriesB).hasSize(1); + assertThat(new String(entriesB.get(0).value(), StandardCharsets.UTF_8)).isEqualTo("value-b-0"); + } } } @@ -191,9 +219,11 @@ void shouldOpenWithSlateDbLocalConfig(@TempDir Path tempDir) { log.tryAppend(key, value); - List entries = log.scan(key, 0, 10); - assertThat(entries).hasSize(1); - assertThat(entries.get(0).value()).isEqualTo(value); + try (LogScanIterator iter = log.scan(key, 0)) { + List entries = collect(iter); + assertThat(entries).hasSize(1); + assertThat(entries.get(0).value()).isEqualTo(value); + } } } @@ -208,9 +238,11 @@ void shouldHandleLargeValues() { log.tryAppend(key, largeValue); - List entries = log.scan(key, 0, 10); - assertThat(entries).hasSize(1); - assertThat(entries.get(0).value()).isEqualTo(largeValue); + try (LogScanIterator iter = log.scan(key, 0)) { + List entries = collect(iter); + assertThat(entries).hasSize(1); + assertThat(entries.get(0).value()).isEqualTo(largeValue); + } } } @@ -224,12 +256,14 @@ void shouldPreserveTimestamp() { log.tryAppend(key, value); long afterAppend = System.currentTimeMillis(); - List entries = log.scan(key, 0, 10); - assertThat(entries).hasSize(1); - // Timestamp should be within the append window - assertThat(entries.get(0).timestamp()) - .isGreaterThanOrEqualTo(beforeAppend) - .isLessThanOrEqualTo(afterAppend); + try (LogScanIterator iter = log.scan(key, 0)) { + List entries = collect(iter); + assertThat(entries).hasSize(1); + // Timestamp should be within the append window + assertThat(entries.get(0).timestamp()) + .isGreaterThanOrEqualTo(beforeAppend) + .isLessThanOrEqualTo(afterAppend); + } } } @@ -254,15 +288,17 @@ void shouldReadFromSeparateLogDbReader(@TempDir Path tempDir) { // Read with separate LogDbReader try (LogDbReader reader = LogDbReader.open(readerConfig)) { - List entries = reader.scan(key, 0, 10); - - assertThat(entries).hasSize(3); - assertThat(new String(entries.get(0).value(), StandardCharsets.UTF_8)).isEqualTo("value-0"); - assertThat(new String(entries.get(1).value(), StandardCharsets.UTF_8)).isEqualTo("value-1"); - assertThat(new String(entries.get(2).value(), StandardCharsets.UTF_8)).isEqualTo("value-2"); - assertThat(entries.get(0).sequence()).isEqualTo(0); - assertThat(entries.get(1).sequence()).isEqualTo(1); - assertThat(entries.get(2).sequence()).isEqualTo(2); + try (LogScanIterator iter = reader.scan(key, 0)) { + List entries = collect(iter); + + assertThat(entries).hasSize(3); + assertThat(new String(entries.get(0).value(), StandardCharsets.UTF_8)).isEqualTo("value-0"); + assertThat(new String(entries.get(1).value(), StandardCharsets.UTF_8)).isEqualTo("value-1"); + assertThat(new String(entries.get(2).value(), StandardCharsets.UTF_8)).isEqualTo("value-2"); + assertThat(entries.get(0).sequence()).isEqualTo(0); + assertThat(entries.get(1).sequence()).isEqualTo(1); + assertThat(entries.get(2).sequence()).isEqualTo(2); + } } } @@ -279,15 +315,18 @@ void shouldCoexistWriterAndReaderWithoutFencingError(@TempDir Path tempDir) { // Open writer and keep it open try (LogDb writer = LogDb.open(writerConfig)) { - // Write initial data + // Write initial data and flush so reader can see it writer.tryAppend(key, "value-0".getBytes(StandardCharsets.UTF_8)); + writer.flush(); // Open reader while writer is still open - this should NOT cause fencing error try (LogDbReader reader = LogDbReader.open(readerConfig)) { // Reader can read the data written by writer - List entries = reader.scan(key, 0, 10); - assertThat(entries).hasSize(1); - assertThat(new String(entries.get(0).value(), StandardCharsets.UTF_8)).isEqualTo("value-0"); + try (LogScanIterator iter = reader.scan(key, 0)) { + List entries = collect(iter); + assertThat(entries).hasSize(1); + assertThat(new String(entries.get(0).value(), StandardCharsets.UTF_8)).isEqualTo("value-0"); + } // Writer can still write more data while reader is open writer.tryAppend(key, "value-1".getBytes(StandardCharsets.UTF_8)); @@ -297,8 +336,186 @@ void shouldCoexistWriterAndReaderWithoutFencingError(@TempDir Path tempDir) { // After reader closes, writer should still work writer.tryAppend(key, "value-3".getBytes(StandardCharsets.UTF_8)); - List finalEntries = writer.scan(key, 0, 10); - assertThat(finalEntries).hasSize(4); + try (LogScanIterator iter = writer.scan(key, 0)) { + List finalEntries = collect(iter); + assertThat(finalEntries).hasSize(4); + } + } + } + + @Test + void shouldReadEntriesOneAtATime() { + try (LogDb log = LogDb.openInMemory()) { + byte[] key = "view-key".getBytes(StandardCharsets.UTF_8); + log.tryAppend(key, "value-0".getBytes(StandardCharsets.UTF_8)); + log.tryAppend(key, "value-1".getBytes(StandardCharsets.UTF_8)); + log.tryAppend(key, "value-2".getBytes(StandardCharsets.UTF_8)); + + try (LogScanRawIterator iter = log.scanRaw(key, 0)) { + LogEntryView entry0 = iter.next(); + assertThat(entry0).isNotNull(); + assertThat(entry0.sequence()).isEqualTo(0); + assertThat(entry0.key().toArray()).isEqualTo(key); + assertThat(new String(entry0.value().toArray(), StandardCharsets.UTF_8)).isEqualTo("value-0"); + + LogEntryView entry1 = iter.next(); + assertThat(entry1).isNotNull(); + assertThat(entry1.sequence()).isEqualTo(1); + assertThat(new String(entry1.value().toArray(), StandardCharsets.UTF_8)).isEqualTo("value-1"); + + LogEntryView entry2 = iter.next(); + assertThat(entry2).isNotNull(); + assertThat(entry2.sequence()).isEqualTo(2); + assertThat(new String(entry2.value().toArray(), StandardCharsets.UTF_8)).isEqualTo("value-2"); + + assertThat(iter.next()).isNull(); + } + } + } + + @Test + void shouldInvalidatePreviousEntryOnNext() { + try (LogDb log = LogDb.openInMemory()) { + byte[] key = "invalidate-key".getBytes(StandardCharsets.UTF_8); + log.tryAppend(key, "value-0".getBytes(StandardCharsets.UTF_8)); + log.tryAppend(key, "value-1".getBytes(StandardCharsets.UTF_8)); + + try (LogScanRawIterator iter = log.scanRaw(key, 0)) { + LogEntryView entry0 = iter.next(); + assertThat(entry0).isNotNull(); + Bytes key0 = entry0.key(); + Bytes value0 = entry0.value(); + + // Advance — previous entry's Bytes should be invalidated + iter.next(); + + assertThatThrownBy(key0::toArray) + .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(value0::toArray) + .isInstanceOf(IllegalStateException.class); + } + } + } + + @Test + void shouldInvalidateEntryOnClose() { + try (LogDb log = LogDb.openInMemory()) { + byte[] key = "close-key".getBytes(StandardCharsets.UTF_8); + log.tryAppend(key, "value-0".getBytes(StandardCharsets.UTF_8)); + + Bytes savedKey; + Bytes savedValue; + try (LogScanRawIterator iter = log.scanRaw(key, 0)) { + LogEntryView entry = iter.next(); + assertThat(entry).isNotNull(); + savedKey = entry.key(); + savedValue = entry.value(); + // Still valid before close + assertThat(savedKey.toArray()).isEqualTo(key); + } + // After close, Bytes should be invalidated + assertThatThrownBy(savedKey::toArray) + .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(savedValue::toArray) + .isInstanceOf(IllegalStateException.class); + } + } + + @Test + void shouldHandleEmptyValue() { + try (LogDb log = LogDb.openInMemory()) { + byte[] key = "empty-val-key".getBytes(StandardCharsets.UTF_8); + byte[] emptyValue = new byte[0]; + + log.tryAppend(key, emptyValue); + + try (LogScanIterator iter = log.scan(key, 0)) { + assertThat(iter.hasNext()).isTrue(); + LogEntry entry = iter.next(); + assertThat(entry.value()).isEqualTo(emptyValue); + assertThat(iter.hasNext()).isFalse(); + } + } + } + + @Test + void shouldReturnNullRepeatedlyAfterExhaustion() { + try (LogDb log = LogDb.openInMemory()) { + byte[] key = "exhaust-key".getBytes(StandardCharsets.UTF_8); + log.tryAppend(key, "only".getBytes(StandardCharsets.UTF_8)); + + try (LogScanRawIterator iter = log.scanRaw(key, 0)) { + assertThat(iter.next()).isNotNull(); + assertThat(iter.next()).isNull(); + assertThat(iter.next()).isNull(); + } + } + } + + @Test + void shouldTolerateDoubleCloseOnIterator() { + try (LogDb log = LogDb.openInMemory()) { + byte[] key = "dbl-close-key".getBytes(StandardCharsets.UTF_8); + log.tryAppend(key, "value".getBytes(StandardCharsets.UTF_8)); + + LogScanRawIterator iter = log.scanRaw(key, 0); + iter.close(); + iter.close(); // should not throw + } + } + + @Test + void shouldThrowWhenScanningClosedLog() { + LogDb log = LogDb.openInMemory(); + log.close(); + + byte[] key = "key".getBytes(StandardCharsets.UTF_8); + assertThatThrownBy(() -> log.scanRaw(key, 0)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("closed"); + } + + @Test + void shouldThrowWhenFlushingClosedLog() { + LogDb log = LogDb.openInMemory(); + log.close(); + + assertThatThrownBy(log::flush) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("closed"); + } + + @Test + void shouldTolerateDoubleClose() { + LogDb log = LogDb.openInMemory(); + log.close(); + log.close(); // should not throw + } + + @Test + void shouldAppendWithTimeout() { + try (LogDb log = LogDb.openInMemory()) { + byte[] key = "timeout-key".getBytes(StandardCharsets.UTF_8); + byte[] value = "timeout-value".getBytes(StandardCharsets.UTF_8); + + AppendResult result = log.appendTimeout(key, value, 5000); + assertThat(result.sequence()).isEqualTo(0); + + try (LogScanIterator iter = log.scan(key, 0)) { + List entries = collect(iter); + assertThat(entries).hasSize(1); + assertThat(entries.get(0).key()).isEqualTo(key); + assertThat(entries.get(0).value()).isEqualTo(value); + } + } + } + + @Test + void shouldFlushInMemoryLogWithoutError() { + try (LogDb log = LogDb.openInMemory()) { + byte[] key = "flush-key".getBytes(StandardCharsets.UTF_8); + log.tryAppend(key, "flush-value".getBytes(StandardCharsets.UTF_8)); + log.flush(); // should not throw } } @@ -322,9 +539,144 @@ void shouldOpenReaderWithCustomRefreshInterval(@TempDir Path tempDir) { // Read with LogDbReader using custom refresh interval try (LogDbReader reader = LogDbReader.open(readerConfig)) { - List entries = reader.scan(key, 0, 10); - assertThat(entries).hasSize(1); - assertThat(new String(entries.get(0).value(), StandardCharsets.UTF_8)).isEqualTo("value-0"); + try (LogScanIterator iter = reader.scan(key, 0)) { + List entries = collect(iter); + assertThat(entries).hasSize(1); + assertThat(new String(entries.get(0).value(), StandardCharsets.UTF_8)).isEqualTo("value-0"); + } + } + } + + @Test + void shouldThrowNoSuchElementWhenExhausted() { + try (LogDb log = LogDb.openInMemory()) { + byte[] key = "nosuch-key".getBytes(StandardCharsets.UTF_8); + log.tryAppend(key, "only".getBytes(StandardCharsets.UTF_8)); + + try (LogScanIterator iter = log.scan(key, 0)) { + iter.next(); // consume the single entry + assertThatThrownBy(iter::next) + .isInstanceOf(java.util.NoSuchElementException.class); + } + } + } + + @Test + void shouldAppendRecordBatchAndReadBack() { + try (LogDb log = LogDb.openInMemory()) { + byte[] key = "batch-key".getBytes(StandardCharsets.UTF_8); + + try (RecordBatch batch = RecordBatch.create()) { + batch.add(key, "batch-0".getBytes(StandardCharsets.UTF_8), 1000L); + batch.add(key, "batch-1".getBytes(StandardCharsets.UTF_8), 1001L); + batch.add(key, "batch-2".getBytes(StandardCharsets.UTF_8), 1002L); + + AppendResult result = log.tryAppend(batch); + assertThat(result.sequence()).isEqualTo(0); + } + + try (LogScanIterator iter = log.scan(key, 0)) { + List entries = collect(iter); + assertThat(entries).hasSize(3); + assertThat(entries.get(0).sequence()).isEqualTo(0); + assertThat(entries.get(1).sequence()).isEqualTo(1); + assertThat(entries.get(2).sequence()).isEqualTo(2); + assertThat(new String(entries.get(0).value(), StandardCharsets.UTF_8)).isEqualTo("batch-0"); + assertThat(new String(entries.get(1).value(), StandardCharsets.UTF_8)).isEqualTo("batch-1"); + assertThat(new String(entries.get(2).value(), StandardCharsets.UTF_8)).isEqualTo("batch-2"); + assertThat(entries.get(0).key()).isEqualTo(key); + } + } + } + + @Test + void shouldAppendRecordBatchWithTimeout() { + try (LogDb log = LogDb.openInMemory()) { + byte[] key = "batch-timeout-key".getBytes(StandardCharsets.UTF_8); + + try (RecordBatch batch = RecordBatch.create()) { + batch.add(key, "value-0".getBytes(StandardCharsets.UTF_8), 500L); + batch.add(key, "value-1".getBytes(StandardCharsets.UTF_8), 501L); + + AppendResult result = log.appendTimeout(batch, 5000); + assertThat(result.sequence()).isEqualTo(0); + } + + try (LogScanIterator iter = log.scan(key, 0)) { + List entries = collect(iter); + assertThat(entries).hasSize(2); + assertThat(new String(entries.get(0).value(), StandardCharsets.UTF_8)).isEqualTo("value-0"); + assertThat(new String(entries.get(1).value(), StandardCharsets.UTF_8)).isEqualTo("value-1"); + } + } + } + + @Test + void shouldPreserveTimestampThroughBatchRoundTrip() { + try (LogDb log = LogDb.openInMemory()) { + byte[] key = "batch-ts-key".getBytes(StandardCharsets.UTF_8); + long ts = 1234567890L; + + try (RecordBatch batch = RecordBatch.create()) { + batch.add(key, "ts-value".getBytes(StandardCharsets.UTF_8), ts); + log.tryAppend(batch); + } + + try (LogScanIterator iter = log.scan(key, 0)) { + List entries = collect(iter); + assertThat(entries).hasSize(1); + assertThat(entries.get(0).timestamp()).isEqualTo(ts); + } + } + } + + @Test + void shouldAssignContiguousSequencesAcrossBatches() { + try (LogDb log = LogDb.openInMemory()) { + byte[] key = "multi-batch-key".getBytes(StandardCharsets.UTF_8); + + try (RecordBatch batch1 = RecordBatch.create()) { + batch1.add(key, "a".getBytes(StandardCharsets.UTF_8), 100L); + batch1.add(key, "b".getBytes(StandardCharsets.UTF_8), 101L); + AppendResult r1 = log.tryAppend(batch1); + assertThat(r1.sequence()).isEqualTo(0); + } + + try (RecordBatch batch2 = RecordBatch.create()) { + batch2.add(key, "c".getBytes(StandardCharsets.UTF_8), 200L); + batch2.add(key, "d".getBytes(StandardCharsets.UTF_8), 201L); + AppendResult r2 = log.tryAppend(batch2); + assertThat(r2.sequence()).isEqualTo(2); + } + + try (LogScanIterator iter = log.scan(key, 0)) { + List entries = collect(iter); + assertThat(entries).hasSize(4); + assertThat(entries.get(0).sequence()).isEqualTo(0); + assertThat(entries.get(1).sequence()).isEqualTo(1); + assertThat(entries.get(2).sequence()).isEqualTo(2); + assertThat(entries.get(3).sequence()).isEqualTo(3); + } + } + } + + @Test + void shouldReturnSameResultForRepeatedHasNext() { + try (LogDb log = LogDb.openInMemory()) { + byte[] key = "idempotent-key".getBytes(StandardCharsets.UTF_8); + log.tryAppend(key, "value-0".getBytes(StandardCharsets.UTF_8)); + + try (LogScanIterator iter = log.scan(key, 0)) { + assertThat(iter.hasNext()).isTrue(); + assertThat(iter.hasNext()).isTrue(); + assertThat(iter.hasNext()).isTrue(); + + LogEntry entry = iter.next(); + assertThat(new String(entry.value(), StandardCharsets.UTF_8)).isEqualTo("value-0"); + + assertThat(iter.hasNext()).isFalse(); + assertThat(iter.hasNext()).isFalse(); + } } } diff --git a/log/src/test/java/dev/opendata/NativeInteropTest.java b/log/src/test/java/dev/opendata/NativeInteropTest.java new file mode 100644 index 0000000..67090ba --- /dev/null +++ b/log/src/test/java/dev/opendata/NativeInteropTest.java @@ -0,0 +1,32 @@ +package dev.opendata; + +import dev.opendata.common.AppendTimeoutException; +import dev.opendata.common.OpenDataNativeException; +import dev.opendata.common.QueueFullException; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class NativeInteropTest { + + @Test + void shouldMapQueueFullError() { + RuntimeException ex = NativeInterop.mapError(5, "queue is full"); + assertThat(ex).isInstanceOf(QueueFullException.class) + .hasMessage("queue is full"); + } + + @Test + void shouldMapAppendTimeoutError() { + RuntimeException ex = NativeInterop.mapError(6, "timed out"); + assertThat(ex).isInstanceOf(AppendTimeoutException.class) + .hasMessage("timed out"); + } + + @Test + void shouldMapUnknownErrorToNativeException() { + RuntimeException ex = NativeInterop.mapError(99, "something broke"); + assertThat(ex).isInstanceOf(OpenDataNativeException.class) + .hasMessage("something broke"); + } +} diff --git a/log/src/test/java/dev/opendata/RecordBatchTest.java b/log/src/test/java/dev/opendata/RecordBatchTest.java new file mode 100644 index 0000000..6132bc8 --- /dev/null +++ b/log/src/test/java/dev/opendata/RecordBatchTest.java @@ -0,0 +1,154 @@ +package dev.opendata; + +import org.junit.jupiter.api.Test; + +import java.lang.foreign.ValueLayout; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RecordBatchTest { + + @Test + void shouldTrackCountAndTimestamp() { + try (RecordBatch batch = RecordBatch.create()) { + assertThat(batch.count()).isEqualTo(0); + assertThat(batch.isEmpty()).isTrue(); + assertThat(batch.firstTimestampMs()).isEqualTo(0); + + batch.add(new byte[]{1}, new byte[]{2}, 1000L); + assertThat(batch.count()).isEqualTo(1); + assertThat(batch.isEmpty()).isFalse(); + assertThat(batch.firstTimestampMs()).isEqualTo(1000L); + + batch.add(new byte[]{3}, new byte[]{4}, 2000L); + assertThat(batch.count()).isEqualTo(2); + assertThat(batch.firstTimestampMs()).isEqualTo(1000L); + } + } + + @Test + void shouldGrowKeySegmentWhenFull() { + // Start with tiny capacity to force growth + try (RecordBatch batch = RecordBatch.create(8, 2)) { + byte[] key = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + batch.add(key, new byte[0], 100L); + batch.add(key, new byte[0], 200L); + + assertThat(batch.count()).isEqualTo(2); + + // Verify key data is correct by checking lengths + assertThat(batch.keyLengths()[0]).isEqualTo(10); + assertThat(batch.keyLengths()[1]).isEqualTo(10); + } + } + + @Test + void shouldGrowValueSegmentWhenFull() { + // Start with tiny capacity to force growth (8 bytes for timestamp header alone) + try (RecordBatch batch = RecordBatch.create(8, 2)) { + byte[] value = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + batch.add(new byte[]{1}, value, 100L); + batch.add(new byte[]{2}, value, 200L); + + assertThat(batch.count()).isEqualTo(2); + // Value length = 8 (timestamp) + 10 (payload) + assertThat(batch.valueLengths()[0]).isEqualTo(18); + assertThat(batch.valueLengths()[1]).isEqualTo(18); + } + } + + @Test + void shouldGrowRecordArrays() { + try (RecordBatch batch = RecordBatch.create(4096, 2)) { + for (int i = 0; i < 10; i++) { + batch.add(new byte[]{(byte) i}, new byte[]{(byte) i}, 100L + i); + } + assertThat(batch.count()).isEqualTo(10); + } + } + + @Test + void shouldThrowOnAddAfterClose() { + RecordBatch batch = RecordBatch.create(); + batch.close(); + + assertThatThrownBy(() -> batch.add(new byte[]{1}, new byte[]{2})) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("closed"); + } + + @Test + void shouldRejectNullKey() { + try (RecordBatch batch = RecordBatch.create()) { + assertThatThrownBy(() -> batch.add(null, new byte[0])) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("key"); + } + } + + @Test + void shouldRejectNullValue() { + try (RecordBatch batch = RecordBatch.create()) { + assertThatThrownBy(() -> batch.add(new byte[0], null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("value"); + } + } + + @Test + void shouldStoreTimestampHeaderForEmptyValue() { + try (RecordBatch batch = RecordBatch.create()) { + batch.add(new byte[]{1}, new byte[0], 42L); + + // Value length should be exactly 8 (timestamp header only) + assertThat(batch.valueLengths()[0]).isEqualTo(8); + + // Verify the timestamp is correctly stored as big-endian + long stored = Long.reverseBytes( + batch.valuesData().get(ValueLayout.JAVA_LONG_UNALIGNED, batch.valueOffsets()[0])); + assertThat(stored).isEqualTo(42L); + } + } + + @Test + void shouldUseCurrentTimeForDefaultTimestamp() { + try (RecordBatch batch = RecordBatch.create()) { + long before = System.currentTimeMillis(); + batch.add(new byte[]{1}, new byte[]{2}); + long after = System.currentTimeMillis(); + + assertThat(batch.firstTimestampMs()) + .isGreaterThanOrEqualTo(before) + .isLessThanOrEqualTo(after); + } + } + + @Test + void shouldTolerateDoubleClose() { + RecordBatch batch = RecordBatch.create(); + batch.close(); + batch.close(); // should not throw + } + + @Test + void shouldStoreContiguousKeyData() { + try (RecordBatch batch = RecordBatch.create()) { + batch.add(new byte[]{10, 20}, new byte[]{1}, 100L); + batch.add(new byte[]{30, 40, 50}, new byte[]{2}, 200L); + + assertThat(batch.keyOffsets()[0]).isEqualTo(0); + assertThat(batch.keyLengths()[0]).isEqualTo(2); + assertThat(batch.keyOffsets()[1]).isEqualTo(2); + assertThat(batch.keyLengths()[1]).isEqualTo(3); + + // Verify actual bytes + byte b0 = batch.keysData().get(ValueLayout.JAVA_BYTE, 0); + byte b1 = batch.keysData().get(ValueLayout.JAVA_BYTE, 1); + byte b2 = batch.keysData().get(ValueLayout.JAVA_BYTE, 2); + assertThat(b0).isEqualTo((byte) 10); + assertThat(b1).isEqualTo((byte) 20); + assertThat(b2).isEqualTo((byte) 30); + } + } +}