From 66d4aaa7de303e151f2fd43234e712ca40519123 Mon Sep 17 00:00:00 2001 From: Jurriaan Mous Date: Sun, 5 Oct 2025 13:12:34 +0200 Subject: [PATCH] Add support for new RocksDB classes and methods, including BackupEngineOptions, CompactionJobInfo, ConfigOptions, CompressionOptions, and related APIs. --- build.gradle.kts | 7 +- gradle.properties | 34 +- .../maryk/rocksdb/BackupEngineOptions.kt | 89 +++++ .../kotlin/maryk/rocksdb/BloomFilter.kt | 8 + .../kotlin/maryk/rocksdb/CompactionJobInfo.kt | 51 +++ .../maryk/rocksdb/CompactionJobStats.kt | 60 +++ .../kotlin/maryk/rocksdb/CompactionOptions.kt | 32 ++ .../maryk/rocksdb/CompressionOptions.kt | 12 + .../kotlin/maryk/rocksdb/ConfigOptions.kt | 12 + .../kotlin/maryk/rocksdb/DBOptions.kt | 14 + .../kotlin/maryk/rocksdb/EventListener.kt | 19 + .../rocksdb/ExternalFileIngestionInfo.kt | 19 + .../kotlin/maryk/rocksdb/FlushJobInfo.kt | 37 ++ .../kotlin/maryk/rocksdb/FlushOptions.kt | 11 + .../kotlin/maryk/rocksdb/FlushReason.kt | 26 ++ .../rocksdb/ImportColumnFamilyOptions.kt | 14 + .../kotlin/maryk/rocksdb/LiveFileMetaData.kt | 16 + .../kotlin/maryk/rocksdb/Options.kt | 3 + .../kotlin/maryk/rocksdb/OptionsUtil.kt | 25 ++ .../kotlin/maryk/rocksdb/PerfContext.kt | 109 ++++++ .../kotlin/maryk/rocksdb/PerfLevel.kt | 18 + .../kotlin/maryk/rocksdb/RateLimiter.kt | 10 + .../kotlin/maryk/rocksdb/RocksDB.kt | 42 +++ .../kotlin/maryk/rocksdb/SanityLevel.kt | 10 + .../kotlin/maryk/rocksdb/SstFileManager.kt | 26 ++ .../kotlin/maryk/rocksdb/SstFileMetaData.kt | 2 +- .../kotlin/maryk/rocksdb/Statistics.kt | 17 + .../maryk/rocksdb/StatisticsCollector.kt | 19 + .../kotlin/maryk/rocksdb/TableProperties.kt | 30 ++ .../maryk/rocksdb/TransactionLogIterator.kt | 36 ++ .../kotlin/maryk/rocksdb/WALRecoveryMode.kt | 15 + .../maryk/rocksdb/WriteBufferManager.kt | 12 + .../maryk/rocksdb/BackupEngineOptionsTest.kt | 168 +++++++++ .../rocksdb/CompactionOptionsFIFOTest.kt | 31 ++ .../maryk/rocksdb/CompactionOptionsTest.kt | 43 +++ .../rocksdb/CompactionOptionsUniversalTest.kt | 64 ++++ .../maryk/rocksdb/CompressionOptionsTest.kt | 46 +++ .../rocksdb/ImportColumnFamilyOptionsTest.kt | 21 ++ .../maryk/rocksdb/SstFileManagerTest.kt | 44 +++ .../maryk/rocksdb/StatisticsCollectorTest.kt | 73 ++++ .../kotlin/maryk/rocksdb/StatisticsTest.kt | 182 +++++++++ .../maryk/rocksdb/WriteBufferManagerTest.kt | 24 ++ .../kotlin/maryk/rocksdb/util/Sleep.kt | 3 + .../maryk/rocksdb/util/ThreadSafeCounter.kt | 6 + .../kotlin/maryk/rocksdb/BloomFilter.kt | 3 + .../kotlin/maryk/rocksdb/CompactionJobInfo.kt | 56 +++ .../kotlin/maryk/rocksdb/CompactionOptions.kt | 5 + .../maryk/rocksdb/CompressionOptions.kt | 3 + .../kotlin/maryk/rocksdb/ConfigOptions.kt | 3 + src/jvmMain/kotlin/maryk/rocksdb/DBOptions.kt | 15 + .../kotlin/maryk/rocksdb/EventListener.kt | 42 +++ .../rocksdb/ExternalFileIngestionInfo.kt | 16 + .../kotlin/maryk/rocksdb/FlushJobInfo.kt | 27 ++ .../kotlin/maryk/rocksdb/FlushOptions.kt | 3 + .../kotlin/maryk/rocksdb/FlushReason.kt | 9 + .../rocksdb/ImportColumnFamilyOptions.kt | 3 + .../kotlin/maryk/rocksdb/LiveFileMetaData.kt | 3 + .../kotlin/maryk/rocksdb/OptionsUtil.kt | 38 ++ .../kotlin/maryk/rocksdb/PerfContext.kt | 3 + src/jvmMain/kotlin/maryk/rocksdb/PerfLevel.kt | 5 + .../kotlin/maryk/rocksdb/RateLimiter.kt | 3 + .../kotlin/maryk/rocksdb/SanityLevel.kt | 3 + .../kotlin/maryk/rocksdb/SstFileManager.kt | 3 + .../kotlin/maryk/rocksdb/Statistics.kt | 13 + .../maryk/rocksdb/StatisticsCollector.kt | 82 ++++ .../kotlin/maryk/rocksdb/TableProperties.kt | 32 ++ .../maryk/rocksdb/TransactionLogIterator.kt | 4 + .../kotlin/maryk/rocksdb/WALRecoveryMode.kt | 6 + .../maryk/rocksdb/WriteBufferManager.kt | 11 + .../maryk/rocksdb/AdvancedOptionsTest.kt | 242 ++++++++++++ .../kotlin/maryk/rocksdb/EventListenerTest.kt | 127 +++++++ .../kotlin/maryk/rocksdb/FlushOptionsTest.kt | 84 +++++ .../kotlin/maryk/rocksdb/util/Sleep.kt | 9 + .../maryk/rocksdb/util/ThreadSafeCounter.kt | 13 + .../maryk/rocksdb/BackupEngineOptions.kt | 154 +++++++- .../kotlin/maryk/rocksdb/BloomFilter.kt | 32 ++ .../maryk/rocksdb/ColumnFamilyOptions.kt | 10 + .../kotlin/maryk/rocksdb/CompactionJobInfo.kt | 124 ++++++ .../kotlin/maryk/rocksdb/CompactionOptions.kt | 165 ++++++++ .../kotlin/maryk/rocksdb/CompactionReason.kt | 4 + .../maryk/rocksdb/CompressionOptions.kt | 59 +++ .../kotlin/maryk/rocksdb/CompressionType.kt | 2 +- .../kotlin/maryk/rocksdb/ConfigOptions.kt | 58 +++ .../kotlin/maryk/rocksdb/DBOptions.kt | 23 ++ .../kotlin/maryk/rocksdb/EventListener.kt | 124 ++++++ .../rocksdb/ExternalFileIngestionInfo.kt | 51 +++ .../kotlin/maryk/rocksdb/FlushJobInfo.kt | 88 +++++ .../kotlin/maryk/rocksdb/FlushOptions.kt | 43 +++ .../kotlin/maryk/rocksdb/FlushReason.kt | 25 ++ .../rocksdb/ImportColumnFamilyOptions.kt | 38 ++ .../kotlin/maryk/rocksdb/LiveFileMetaData.kt | 15 + .../kotlin/maryk/rocksdb/Options.kt | 7 + .../kotlin/maryk/rocksdb/OptionsUtil.kt | 108 ++++++ .../kotlin/maryk/rocksdb/PerfContext.kt | 356 ++++++++++++++++++ .../kotlin/maryk/rocksdb/PerfLevel.kt | 17 + .../kotlin/maryk/rocksdb/RateLimiter.kt | 40 ++ .../kotlin/maryk/rocksdb/RocksDB.kt | 106 +++++- .../kotlin/maryk/rocksdb/SanityLevel.kt | 7 + .../kotlin/maryk/rocksdb/SstFileManager.kt | 63 ++++ .../kotlin/maryk/rocksdb/SstFileMetaData.kt | 38 +- .../kotlin/maryk/rocksdb/Statistics.kt | 99 ++++- .../maryk/rocksdb/StatisticsCollector.kt | 170 +++++++++ .../kotlin/maryk/rocksdb/TableProperties.kt | 142 +++++++ .../maryk/rocksdb/TransactionLogIterator.kt | 58 +++ .../kotlin/maryk/rocksdb/WALRecoveryMode.kt | 18 + .../maryk/rocksdb/WriteBufferManager.kt | 40 ++ .../kotlin/maryk/rocksdb/util/Sleep.kt | 16 + .../maryk/rocksdb/util/ThreadSafeCounter.kt | 16 + 108 files changed, 4666 insertions(+), 56 deletions(-) create mode 100644 src/commonMain/kotlin/maryk/rocksdb/BloomFilter.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/CompactionJobInfo.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/CompactionJobStats.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/CompactionOptions.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/CompressionOptions.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/ConfigOptions.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/EventListener.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/ExternalFileIngestionInfo.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/FlushJobInfo.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/FlushOptions.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/FlushReason.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/ImportColumnFamilyOptions.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/LiveFileMetaData.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/OptionsUtil.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/PerfContext.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/PerfLevel.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/RateLimiter.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/SanityLevel.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/SstFileManager.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/StatisticsCollector.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/TableProperties.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/TransactionLogIterator.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/WALRecoveryMode.kt create mode 100644 src/commonMain/kotlin/maryk/rocksdb/WriteBufferManager.kt create mode 100644 src/commonTest/kotlin/maryk/rocksdb/BackupEngineOptionsTest.kt create mode 100644 src/commonTest/kotlin/maryk/rocksdb/CompactionOptionsFIFOTest.kt create mode 100644 src/commonTest/kotlin/maryk/rocksdb/CompactionOptionsTest.kt create mode 100644 src/commonTest/kotlin/maryk/rocksdb/CompactionOptionsUniversalTest.kt create mode 100644 src/commonTest/kotlin/maryk/rocksdb/CompressionOptionsTest.kt create mode 100644 src/commonTest/kotlin/maryk/rocksdb/ImportColumnFamilyOptionsTest.kt create mode 100644 src/commonTest/kotlin/maryk/rocksdb/SstFileManagerTest.kt create mode 100644 src/commonTest/kotlin/maryk/rocksdb/StatisticsCollectorTest.kt create mode 100644 src/commonTest/kotlin/maryk/rocksdb/StatisticsTest.kt create mode 100644 src/commonTest/kotlin/maryk/rocksdb/WriteBufferManagerTest.kt create mode 100644 src/commonTest/kotlin/maryk/rocksdb/util/Sleep.kt create mode 100644 src/commonTest/kotlin/maryk/rocksdb/util/ThreadSafeCounter.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/BloomFilter.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/CompactionJobInfo.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/CompactionOptions.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/CompressionOptions.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/ConfigOptions.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/EventListener.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/ExternalFileIngestionInfo.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/FlushJobInfo.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/FlushOptions.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/FlushReason.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/ImportColumnFamilyOptions.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/LiveFileMetaData.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/OptionsUtil.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/PerfContext.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/PerfLevel.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/RateLimiter.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/SanityLevel.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/SstFileManager.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/StatisticsCollector.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/TableProperties.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/TransactionLogIterator.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/WALRecoveryMode.kt create mode 100644 src/jvmMain/kotlin/maryk/rocksdb/WriteBufferManager.kt create mode 100644 src/jvmTest/kotlin/maryk/rocksdb/AdvancedOptionsTest.kt create mode 100644 src/jvmTest/kotlin/maryk/rocksdb/EventListenerTest.kt create mode 100644 src/jvmTest/kotlin/maryk/rocksdb/FlushOptionsTest.kt create mode 100644 src/jvmTest/kotlin/maryk/rocksdb/util/Sleep.kt create mode 100644 src/jvmTest/kotlin/maryk/rocksdb/util/ThreadSafeCounter.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/BloomFilter.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/CompactionJobInfo.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/CompactionOptions.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/CompressionOptions.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/ConfigOptions.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/EventListener.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/ExternalFileIngestionInfo.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/FlushJobInfo.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/FlushOptions.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/FlushReason.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/ImportColumnFamilyOptions.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/LiveFileMetaData.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/OptionsUtil.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/PerfContext.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/PerfLevel.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/RateLimiter.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/SanityLevel.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/SstFileManager.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/StatisticsCollector.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/TableProperties.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/TransactionLogIterator.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/WALRecoveryMode.kt create mode 100644 src/nativeMain/kotlin/maryk/rocksdb/WriteBufferManager.kt create mode 100644 src/nativeTest/kotlin/maryk/rocksdb/util/Sleep.kt create mode 100644 src/nativeTest/kotlin/maryk/rocksdb/util/ThreadSafeCounter.kt diff --git a/build.gradle.kts b/build.gradle.kts index a9a5dc2..bb62ba3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,6 +47,7 @@ val rocksDBJVMVersion = "10.4.2" val rocksDBAndroidVersion = "10.4.2" val kotlinXDateTimeVersion = "0.7.1" +val kotlinXCoroutinesVersion = "1.10.2" val rocksdbPrebuiltBaseUrlValue = providers.gradleProperty("rocksdbPrebuiltBaseUrl").orElse("https://github.com/marykdb/build-rocksdb/releases/download").get() val rocksdbPrebuiltVersionValue = providers.gradleProperty("rocksdbPrebuiltVersion").get() val rocksdbSupportedNativeTargets = setOf( @@ -466,7 +467,11 @@ kotlin { optIn("kotlinx.cinterop.BetaInteropApi") } } - commonMain {} + nativeMain { + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinXCoroutinesVersion") + } + } commonTest { dependencies { implementation(kotlin("test")) diff --git a/gradle.properties b/gradle.properties index 221c2cb..5a449e4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,20 +7,20 @@ kotlin.mpp.stability.nowarn=true kotlin.mpp.androidSourceSetLayoutVersion=2 rocksdbPrebuiltBaseUrl=https://github.com/marykdb/build-rocksdb/releases/download -rocksdbPrebuiltVersion=rocksdb-10.4.2-20251003T152203Z -rocksdbPrebuiltSha.androidNativeArm32=2fffb2241b268162220fac8d421f2180825ba8d8ff1de0642fc6dc7c94d0fd03 -rocksdbPrebuiltSha.androidNativeArm64=94af5ca2ec2f2b96c7bd131eb856b954c219f42a906e6964ec84bbe69bb5adda -rocksdbPrebuiltSha.androidNativeX64=e741bcaee362e8b80602418e4002aae2c47a7fcc34ca232855223d96ed15dfde -rocksdbPrebuiltSha.androidNativeX86=5d460f5ef17d22347fbb1c3687487ebe248f6faa2ac4803257030c75f94934e2 -rocksdbPrebuiltSha.iosArm64=48d9afa60ede390b4a542c48b2bebec84ca0824395af718992664313208a224a -rocksdbPrebuiltSha.iosSimulatorArm64=4ae02136f5a6ca82eac861cdfd1ea6798278769981438cccfac89fa671cecc01 -rocksdbPrebuiltSha.linuxArm64=5e1c044744c3562d46dc6f46d3b62a76269a38a15079109173dfedae3fc4d90a -rocksdbPrebuiltSha.linuxX64=cc26a9808f5c16eaa9f1e1cd5f848c4b1db6690f65b287643ea860661276cd32 -rocksdbPrebuiltSha.macosArm64=1b55fdb0b7a2de0950d50c1a4b6dc9b8ba1b7d51d766c35b481a5db05247ec99 -rocksdbPrebuiltSha.macosX64=cf38d8de6f8e93cbeda7b90d070fc840a91a4abf1455b25d9be567be9ea37dba -rocksdbPrebuiltSha.mingwX64=a273f4700b3a51c33c07e6513ac3fb4a3a1a8a62087e251a8436a91e79316051 -rocksdbPrebuiltSha.tvosArm64=5238480990e42bfd3c7d64dba816a1aa9843d2dc53cbb70ad4c35a63a8a1324f -rocksdbPrebuiltSha.tvosSimulatorArm64=ac66c0da022ae779776a2198d8c43d4367cc1cc98a9eec565095638461236475 -rocksdbPrebuiltSha.watchosArm64=c72dd92b7134a1c0889e4e42008c6f2a789f9dfe2ec8a7c93b6fc8a7458568e3 -rocksdbPrebuiltSha.watchosDeviceArm64=671548911290e2e155d55b291693d82e1d7083e579e30d0a7700badd7a84ecfb -rocksdbPrebuiltSha.watchosSimulatorArm64=bf3e16bbfbcc7d26790058efb8b9b503245ad3f37a9e5f9db755587637ce402b +rocksdbPrebuiltVersion=rocksdb-10.4.2-20251006T132725Z +rocksdbPrebuiltSha.androidNativeArm32=086741b47085232a8463c54aba807c66a8e6c50ebf66b75a7f5539f634cb3f0a +rocksdbPrebuiltSha.androidNativeArm64=3abe3546d0ac14f1fcc85acdc165e8b7f38a695b7b6b41e8216feee014e55d3b +rocksdbPrebuiltSha.androidNativeX64=43a0bb7e99d8f5c3a4f9a08660b1fcb8de302240ef04f4a2fe9a3f7064d57d4e +rocksdbPrebuiltSha.androidNativeX86=51051ec151d80b0340e3f8877e3b8407b5d330cf8633ca34204552fbd7d5ec6a +rocksdbPrebuiltSha.iosArm64=236dfb211fafa2dd815c95f4829afd06e96a4560b9b53dd037a6ba6f946ce6c8 +rocksdbPrebuiltSha.iosSimulatorArm64=07eb51903f6d6ff65651dbdd86f46e3c8c9d73636acde28a439d03ced3304478 +rocksdbPrebuiltSha.linuxArm64=b15d05d0c698f451e4f95a6fe2dd621b969fc6b5d2cbd703a64918300af896eb +rocksdbPrebuiltSha.linuxX64=eb78ad46d9b8cdfe654ee2a9ab94bbfad34a7fb658c3fc24383e411d7ea047c5 +rocksdbPrebuiltSha.macosArm64=79cf3a19649f731c5b0bc7fc86d2a3c9d869a5e4c89a19ac346928c308516542 +rocksdbPrebuiltSha.macosX64=8aac542c00e7d391318948cec489b9ee5c02616d54c29934b47f16e2c144cd4e +rocksdbPrebuiltSha.mingwX64=5da2b842f30d8e97bb8c4320399f4199c86c087af0af6e2969a95c31dffa0272 +rocksdbPrebuiltSha.tvosArm64=1c21646cf19c1eb821fdde1e9fc3f04383d2dc1072ec4a7a72205cf09d8e611b +rocksdbPrebuiltSha.tvosSimulatorArm64=01d72237647a908284c5cc60ab33e64dd98c991fca0398d9087d0e9eb20bf1c7 +rocksdbPrebuiltSha.watchosArm64=f9f6c5913761bc1ae79a00e0334b8389cd66878ad68be70f419dda40fe2f286f +rocksdbPrebuiltSha.watchosDeviceArm64=10f3ff980f5e143826a7aeb6a2048cf6d0a224fd1b2791e2c15c7e7e5d8e0149 +rocksdbPrebuiltSha.watchosSimulatorArm64=5613374c51237bcd38db901519478802cc3a13dc10cb4234cfbaab329ad94d00 diff --git a/src/commonMain/kotlin/maryk/rocksdb/BackupEngineOptions.kt b/src/commonMain/kotlin/maryk/rocksdb/BackupEngineOptions.kt index 3e0d0db..a54b38f 100644 --- a/src/commonMain/kotlin/maryk/rocksdb/BackupEngineOptions.kt +++ b/src/commonMain/kotlin/maryk/rocksdb/BackupEngineOptions.kt @@ -21,4 +21,93 @@ expect class BackupEngineOptions : RocksObject { /** Returns the path to the BackupableDB directory. */ fun backupDir(): String + + /** + * Backup Env object. It will be used for backup file I/O. If it's + * null, backups will be written out using DBs Env. Otherwise, + * backup's I/O will be performed using this object. + * + * Default: null + * + * @param env The environment to use + * @return instance of current BackupEngineOptions. + */ + fun setBackupEnv(env: Env): BackupEngineOptions + + /** Returns the environment used for backup I/O. */ + fun backupEnv(): Env? + + /** Share table files between backups. */ + fun setShareTableFiles(shareTableFiles: Boolean): BackupEngineOptions + + /** Returns true if SST files are shared between backups. */ + fun shareTableFiles(): Boolean + + /** Enable or disable synchronous backups. */ + fun setSync(sync: Boolean): BackupEngineOptions + + /** Returns true if synchronous backups are enabled. */ + fun sync(): Boolean + + /** Configure whether existing backups should be destroyed. */ + fun setDestroyOldData(destroyOldData: Boolean): BackupEngineOptions + + /** Returns true if old backup data will be destroyed. */ + fun destroyOldData(): Boolean + + /** + * Configure whether log files are copied into the backup. + */ + fun setBackupLogFiles(backupLogFiles: Boolean): BackupEngineOptions + + /** Returns true if log files are backed up. */ + fun backupLogFiles(): Boolean + + /** + * Limit the rate at which data is copied during a backup. A value of 0 + * disables throttling. + */ + fun setBackupRateLimit(backupRateLimit: Long): BackupEngineOptions + + /** Returns the configured backup rate limit in bytes per second. */ + fun backupRateLimit(): Long + + /** Attach a rate limiter that governs backup throughput. */ + fun setBackupRateLimiter(rateLimiter: RateLimiter): BackupEngineOptions + + /** Returns the configured backup rate limiter, if any. */ + fun backupRateLimiter(): RateLimiter? + + /** Configure the restore rate limit in bytes per second. */ + fun setRestoreRateLimit(restoreRateLimit: Long): BackupEngineOptions + + /** Returns the configured restore rate limit in bytes per second. */ + fun restoreRateLimit(): Long + + /** Attach a rate limiter that governs restore throughput. */ + fun setRestoreRateLimiter(rateLimiter: RateLimiter): BackupEngineOptions + + /** Returns the configured restore rate limiter, if any. */ + fun restoreRateLimiter(): RateLimiter? + + /** Configure whether SSTs are identified by checksum metadata. */ + fun setShareFilesWithChecksum(shareFilesWithChecksum: Boolean): BackupEngineOptions + + /** Returns true if file sharing is keyed by checksum metadata. */ + fun shareFilesWithChecksum(): Boolean + + /** Sets the number of background threads used for backup/restore. */ + fun setMaxBackgroundOperations(maxBackgroundOperations: Int): BackupEngineOptions + + /** Returns the number of background threads used for backup/restore. */ + fun maxBackgroundOperations(): Int + + /** + * Configures how many bytes are copied between callbacks when a + * progress listener is attached. + */ + fun setCallbackTriggerIntervalSize(callbackTriggerIntervalSize: Long): BackupEngineOptions + + /** Returns the current callback trigger interval size. */ + fun callbackTriggerIntervalSize(): Long } diff --git a/src/commonMain/kotlin/maryk/rocksdb/BloomFilter.kt b/src/commonMain/kotlin/maryk/rocksdb/BloomFilter.kt new file mode 100644 index 0000000..d728ac5 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/BloomFilter.kt @@ -0,0 +1,8 @@ +package maryk.rocksdb + +/** Bloom filter wrapper used by [BlockBasedTableConfig]. */ +expect class BloomFilter : FilterPolicy { + constructor() + constructor(bitsPerKey: Double) + constructor(bitsPerKey: Double, useBlockBasedBuilder: Boolean) +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/CompactionJobInfo.kt b/src/commonMain/kotlin/maryk/rocksdb/CompactionJobInfo.kt new file mode 100644 index 0000000..e7b6549 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/CompactionJobInfo.kt @@ -0,0 +1,51 @@ +package maryk.rocksdb + +/** + * Snapshot of metadata describing a compaction job. + */ +expect class CompactionJobInfo { + /** Column family name the compaction operated on. */ + fun columnFamilyName(): ByteArray + + /** Smallest input level participating in the compaction. */ + fun baseInputLevel(): Int + + /** Target output level for the compaction results. */ + fun outputLevel(): Int + + /** Absolute paths of SST files consumed by the compaction. */ + fun inputFiles(): List + + /** Absolute paths of SST files generated by the compaction. */ + fun outputFiles(): List + + /** Wall clock duration of the job in microseconds. */ + fun elapsedMicros(): Long + + /** Number of corrupt keys encountered. */ + fun numCorruptKeys(): Long + + /** Logical records read from inputs. */ + fun inputRecords(): Long + + /** Logical records written to outputs. */ + fun outputRecords(): Long + + /** Total bytes of input SST data processed. */ + fun totalInputBytes(): Long + + /** Total bytes written to output SST files. */ + fun totalOutputBytes(): Long + + /** Reason RocksDB scheduled this compaction. */ + fun compactionReason(): CompactionReason + + /** Count of input files across all levels. */ + fun numInputFiles(): Long + + /** Count of input files already residing at the output level. */ + fun numInputFilesAtOutputLevel(): Long + + /** Aggregated statistics describing the compaction run. */ + fun compactionStats(): CompactionJobStats +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/CompactionJobStats.kt b/src/commonMain/kotlin/maryk/rocksdb/CompactionJobStats.kt new file mode 100644 index 0000000..21f1507 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/CompactionJobStats.kt @@ -0,0 +1,60 @@ +package maryk.rocksdb + +/** + * Aggregated statistics captured for a single compaction job. + */ +class CompactionJobStats internal constructor( + private val elapsedMicrosValue: Long, + private val numInputRecordsValue: Long, + private val numOutputRecordsValue: Long, + private val totalInputBytesValue: Long, + private val totalOutputBytesValue: Long, + private val numInputFilesValue: Long, + private val numInputFilesAtOutputLevelValue: Long, + private val numOutputFilesValue: Long, + private val numCorruptKeysValue: Long, +) { + /** Duration of the compaction in microseconds. */ + fun elapsedMicros(): Long = elapsedMicrosValue + + /** Number of logical records consumed from inputs. */ + fun numInputRecords(): Long = numInputRecordsValue + + /** Number of logical records produced in the outputs. */ + fun numOutputRecords(): Long = numOutputRecordsValue + + /** Total size of input SST data processed. */ + fun totalInputBytes(): Long = totalInputBytesValue + + /** Total size of SST data generated as output. */ + fun totalOutputBytes(): Long = totalOutputBytesValue + + /** Number of input files processed across all levels. */ + fun numInputFiles(): Long = numInputFilesValue + + /** Number of input files that already lived at the target level. */ + fun numInputFilesAtOutputLevel(): Long = numInputFilesAtOutputLevelValue + + /** Number of output files generated. */ + fun numOutputFiles(): Long = numOutputFilesValue + + /** Corrupt keys encountered while compacting. */ + fun numCorruptKeys(): Long = numCorruptKeysValue + + internal companion object { + internal fun empty( + numInputFilesFallback: Long, + numOutputFilesFallback: Long, + ) = CompactionJobStats( + elapsedMicrosValue = 0, + numInputRecordsValue = 0, + numOutputRecordsValue = 0, + totalInputBytesValue = 0, + totalOutputBytesValue = 0, + numInputFilesValue = numInputFilesFallback, + numInputFilesAtOutputLevelValue = 0, + numOutputFilesValue = numOutputFilesFallback, + numCorruptKeysValue = 0, + ) + } +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/CompactionOptions.kt b/src/commonMain/kotlin/maryk/rocksdb/CompactionOptions.kt new file mode 100644 index 0000000..1e89ef0 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/CompactionOptions.kt @@ -0,0 +1,32 @@ +package maryk.rocksdb + +expect class CompactionOptions() : RocksObject { + fun compression(): CompressionType + fun setCompression(compression: CompressionType): CompactionOptions + fun outputFileSizeLimit(): Long + fun setOutputFileSizeLimit(limit: Long): CompactionOptions + fun maxSubcompactions(): Int + fun setMaxSubcompactions(count: Int): CompactionOptions +} + +expect class CompactionOptionsFIFO() : RocksObject { + fun setMaxTableFilesSize(size: Long): CompactionOptionsFIFO + fun maxTableFilesSize(): Long + fun setAllowCompaction(allow: Boolean): CompactionOptionsFIFO + fun allowCompaction(): Boolean +} + +expect class CompactionOptionsUniversal() : RocksObject { + fun setSizeRatio(sizeRatio: Int): CompactionOptionsUniversal + fun sizeRatio(): Int + fun setMinMergeWidth(width: Int): CompactionOptionsUniversal + fun minMergeWidth(): Int + fun setMaxMergeWidth(width: Int): CompactionOptionsUniversal + fun maxMergeWidth(): Int + fun setMaxSizeAmplificationPercent(percent: Int): CompactionOptionsUniversal + fun maxSizeAmplificationPercent(): Int + fun setCompressionSizePercent(percent: Int): CompactionOptionsUniversal + fun compressionSizePercent(): Int + fun setStopStyle(stopStyle: CompactionStopStyle): CompactionOptionsUniversal + fun stopStyle(): CompactionStopStyle +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/CompressionOptions.kt b/src/commonMain/kotlin/maryk/rocksdb/CompressionOptions.kt new file mode 100644 index 0000000..ad086d3 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/CompressionOptions.kt @@ -0,0 +1,12 @@ +package maryk.rocksdb + +expect class CompressionOptions() : RocksObject { + fun setWindowBits(windowBits: Int): CompressionOptions + fun windowBits(): Int + fun setLevel(level: Int): CompressionOptions + fun level(): Int + fun setStrategy(strategy: Int): CompressionOptions + fun strategy(): Int + fun setMaxDictBytes(bytes: Int): CompressionOptions + fun maxDictBytes(): Int +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/ConfigOptions.kt b/src/commonMain/kotlin/maryk/rocksdb/ConfigOptions.kt new file mode 100644 index 0000000..6094cb6 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/ConfigOptions.kt @@ -0,0 +1,12 @@ +package maryk.rocksdb + +/** + * Options for configuring how RocksDB parses options files and strings. + */ +expect class ConfigOptions() : RocksObject { + fun setDelimiter(delimiter: String): ConfigOptions + fun setIgnoreUnknownOptions(ignore: Boolean): ConfigOptions + fun setEnv(env: Env): ConfigOptions + fun setInputStringsEscaped(escaped: Boolean): ConfigOptions + fun setSanityLevel(level: SanityLevel): ConfigOptions +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/DBOptions.kt b/src/commonMain/kotlin/maryk/rocksdb/DBOptions.kt index 672e743..c13d983 100644 --- a/src/commonMain/kotlin/maryk/rocksdb/DBOptions.kt +++ b/src/commonMain/kotlin/maryk/rocksdb/DBOptions.kt @@ -1,5 +1,9 @@ package maryk.rocksdb +expect fun DBOptions.addEventListener(listener: EventListener): DBOptions + +expect fun Options.addEventListener(listener: EventListener): Options + expect class DBOptions() : RocksObject { /** * If this value is set to true, then the database will be created @@ -267,4 +271,14 @@ expect class DBOptions() : RocksObject { * @see .walSizeLimitMB */ fun walSizeLimitMB(): Long + + /** + * Sets the recovery policy when replaying the WAL during open. + */ + fun setWalRecoveryMode(mode: WALRecoveryMode): DBOptions + + /** + * Returns the configured recovery policy for WAL replay. + */ + fun walRecoveryMode(): WALRecoveryMode } diff --git a/src/commonMain/kotlin/maryk/rocksdb/EventListener.kt b/src/commonMain/kotlin/maryk/rocksdb/EventListener.kt new file mode 100644 index 0000000..03bc451 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/EventListener.kt @@ -0,0 +1,19 @@ +package maryk.rocksdb + +/** Receives callbacks for background RocksDB operations. */ +expect abstract class EventListener : RocksCallbackObject { + /** Invoked before a flush job starts. */ + open fun onFlushBeginEvent(db: RocksDB, flushJobInfo: FlushJobInfo) + + /** Invoked after a flush completes. */ + open fun onFlushCompletedEvent(db: RocksDB, flushJobInfo: FlushJobInfo) + + /** Invoked before a compaction job begins. */ + open fun onCompactionBeginEvent(db: RocksDB, compactionJobInfo: CompactionJobInfo) + + /** Invoked when a compaction job finishes. */ + open fun onCompactionCompletedEvent(db: RocksDB, compactionJobInfo: CompactionJobInfo) + + /** Invoked after an external SST file has been ingested into the database. */ + open fun onExternalFileIngested(db: RocksDB, ingestionInfo: ExternalFileIngestionInfo) +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/ExternalFileIngestionInfo.kt b/src/commonMain/kotlin/maryk/rocksdb/ExternalFileIngestionInfo.kt new file mode 100644 index 0000000..8504d56 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/ExternalFileIngestionInfo.kt @@ -0,0 +1,19 @@ +package maryk.rocksdb + +/** Metadata describing an external SST file that was ingested into a RocksDB instance. */ +expect class ExternalFileIngestionInfo { + /** Column family the file was ingested into. */ + fun columnFamilyName(): String + + /** Absolute path of the file before ingestion. */ + fun externalFilePath(): String + + /** Relative path of the file within the RocksDB data directory. */ + fun internalFilePath(): String + + /** Global sequence number assigned to keys contained in the file. */ + fun globalSequenceNumber(): Long + + /** Table properties associated with the ingested file. */ + fun tableProperties(): TableProperties? +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/FlushJobInfo.kt b/src/commonMain/kotlin/maryk/rocksdb/FlushJobInfo.kt new file mode 100644 index 0000000..4583079 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/FlushJobInfo.kt @@ -0,0 +1,37 @@ +package maryk.rocksdb + +/** Metadata describing a flush job. */ +expect class FlushJobInfo { + /** Identifier of the column family that produced the SST file. */ + fun columnFamilyId(): Long + + /** Name of the column family that produced the SST file. */ + fun columnFamilyName(): String + + /** Absolute path of the flushed SST file. */ + fun filePath(): String + + /** Thread identifier that executed the flush. */ + fun threadId(): Long + + /** Job identifier that is unique within the thread. */ + fun jobId(): Int + + /** Whether the flush was triggered because writes were slowed down. */ + fun triggeredWritesSlowdown(): Boolean + + /** Whether writes were stopped while waiting for this flush. */ + fun triggeredWritesStop(): Boolean + + /** Smallest sequence number contained in the flushed file. */ + fun smallestSeqno(): Long + + /** Largest sequence number contained in the flushed file. */ + fun largestSeqno(): Long + + /** Table properties associated with the flushed SST file. */ + fun tableProperties(): TableProperties + + /** Reason why the flush was initiated. */ + fun flushReason(): FlushReason +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/FlushOptions.kt b/src/commonMain/kotlin/maryk/rocksdb/FlushOptions.kt new file mode 100644 index 0000000..b1c595f --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/FlushOptions.kt @@ -0,0 +1,11 @@ +package maryk.rocksdb + +/** + * Options that configure manual flush behaviour when calling [RocksDB.flush]. + */ +expect class FlushOptions() : RocksObject { + fun setWaitForFlush(wait: Boolean): FlushOptions + fun waitForFlush(): Boolean + fun setAllowWriteStall(allow: Boolean): FlushOptions + fun allowWriteStall(): Boolean +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/FlushReason.kt b/src/commonMain/kotlin/maryk/rocksdb/FlushReason.kt new file mode 100644 index 0000000..3bd0870 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/FlushReason.kt @@ -0,0 +1,26 @@ +package maryk.rocksdb + +/** Reasons that can trigger a memtable flush. */ +expect enum class FlushReason { + OTHERS, + GET_LIVE_FILES, + SHUTDOWN, + EXTERNAL_FILE_INGESTION, + MANUAL_COMPACTION, + WRITE_BUFFER_MANAGER, + WRITE_BUFFER_FULL, + TEST, + DELETE_FILES, + AUTO_COMPACTION, + MANUAL_FLUSH, + ERROR_RECOVERY, + ERROR_RECOVERY_RETRY_FLUSH, + WAL_FULL, + CATCH_UP_AFTER_ERROR_RECOVERY, +} + +/** Encoded value used by RocksDB internals. */ +expect fun FlushReason.value(): Byte + +/** Resolve a [FlushReason] from its encoded value. */ +expect fun flushReasonFromValue(value: Byte): FlushReason diff --git a/src/commonMain/kotlin/maryk/rocksdb/ImportColumnFamilyOptions.kt b/src/commonMain/kotlin/maryk/rocksdb/ImportColumnFamilyOptions.kt new file mode 100644 index 0000000..cb6d5ad --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/ImportColumnFamilyOptions.kt @@ -0,0 +1,14 @@ +package maryk.rocksdb + +/** + * Options controlling how external SST files are imported when creating column families. + */ +expect class ImportColumnFamilyOptions() : RocksObject { + /** + * Whether the importer should move files instead of copying them. + */ + fun setMoveFiles(moveFiles: Boolean): ImportColumnFamilyOptions + + /** Returns whether files will be moved rather than copied during import. */ + fun moveFiles(): Boolean +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/LiveFileMetaData.kt b/src/commonMain/kotlin/maryk/rocksdb/LiveFileMetaData.kt new file mode 100644 index 0000000..c66e12a --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/LiveFileMetaData.kt @@ -0,0 +1,16 @@ +package maryk.rocksdb + +/** + * Metadata describing an SST file that is currently live in the database. + */ +expect class LiveFileMetaData : SstFileMetaData { + /** + * Returns the name of the column family this file belongs to. + */ + fun columnFamilyName(): ByteArray + + /** + * Returns the level at which the file resides in the LSM tree. + */ + fun level(): Int +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/Options.kt b/src/commonMain/kotlin/maryk/rocksdb/Options.kt index 8b1b983..4690e4d 100644 --- a/src/commonMain/kotlin/maryk/rocksdb/Options.kt +++ b/src/commonMain/kotlin/maryk/rocksdb/Options.kt @@ -6,6 +6,9 @@ expect class Options() : RocksObject { */ fun setTableFormatConfig(tableFormatConfig: TableFormatConfig): Options + /** Attach an [SstFileManager] so RocksDB can coordinate file deletion with other instances. */ + fun setSstFileManager(sstFileManager: SstFileManager): Options + /** * Number of open files that can be used by the DB. You may need to * increase this if your database has a large working set. Value -1 means diff --git a/src/commonMain/kotlin/maryk/rocksdb/OptionsUtil.kt b/src/commonMain/kotlin/maryk/rocksdb/OptionsUtil.kt new file mode 100644 index 0000000..1b93a55 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/OptionsUtil.kt @@ -0,0 +1,25 @@ +package maryk.rocksdb + +/** + * Helpers for reading RocksDB options files into live option instances. + */ +expect object OptionsUtil { + @Throws(RocksDBException::class) + fun loadLatestOptions( + configOptions: ConfigOptions, + dbPath: String, + dbOptions: DBOptions, + columnFamilyDescriptors: MutableList + ) + + @Throws(RocksDBException::class) + fun loadOptionsFromFile( + configOptions: ConfigOptions, + optionsFilePath: String, + dbOptions: DBOptions, + columnFamilyDescriptors: MutableList + ) + + @Throws(RocksDBException::class) + fun getLatestOptionsFileName(dbPath: String, env: Env): String +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/PerfContext.kt b/src/commonMain/kotlin/maryk/rocksdb/PerfContext.kt new file mode 100644 index 0000000..590731c --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/PerfContext.kt @@ -0,0 +1,109 @@ +package maryk.rocksdb + +/** + * Snapshot of RocksDB perf context counters. + */ +expect class PerfContext : RocksObject { + fun reset() + + fun getUserKeyComparisonCount(): Long + fun getBlockCacheHitCount(): Long + fun getBlockReadCount(): Long + fun getBlockReadByte(): Long + fun getBlockReadTime(): Long + fun getBlockReadCpuTime(): Long + fun getBlockCacheIndexHitCount(): Long + fun getBlockCacheStandaloneHandleCount(): Long + fun getBlockCacheRealHandleCount(): Long + fun getIndexBlockReadCount(): Long + fun getBlockCacheFilterHitCount(): Long + fun getFilterBlockReadCount(): Long + fun getCompressionDictBlockReadCount(): Long + fun getSecondaryCacheHitCount(): Long + fun getCompressedSecCacheInsertRealCount(): Long + fun getCompressedSecCacheInsertDummyCount(): Long + fun getCompressedSecCacheUncompressedBytes(): Long + fun getCompressedSecCacheCompressedBytes(): Long + fun getBlockChecksumTime(): Long + fun getBlockDecompressTime(): Long + fun getReadBytes(): Long + fun getMultigetReadBytes(): Long + fun getIterReadBytes(): Long + fun getBlobCacheHitCount(): Long + fun getBlobReadCount(): Long + fun getBlobReadByte(): Long + fun getBlobReadTime(): Long + fun getBlobChecksumTime(): Long + fun getBlobDecompressTime(): Long + fun getInternalKeySkippedCount(): Long + fun getInternalDeleteSkippedCount(): Long + fun getInternalRecentSkippedCount(): Long + fun getInternalMergeCount(): Long + fun getInternalMergePointLookupCount(): Long + fun getInternalRangeDelReseekCount(): Long + fun getSnapshotTime(): Long + fun getFromMemtableTime(): Long + fun getFromMemtableCount(): Long + fun getPostProcessTime(): Long + fun getFromOutputFilesTime(): Long + fun getSeekOnMemtableTime(): Long + fun getSeekOnMemtableCount(): Long + fun getNextOnMemtableCount(): Long + fun getPrevOnMemtableCount(): Long + fun getSeekChildSeekTime(): Long + fun getSeekChildSeekCount(): Long + fun getSeekMinHeapTime(): Long + fun getSeekMaxHeapTime(): Long + fun getSeekInternalSeekTime(): Long + fun getFindNextUserEntryTime(): Long + fun getWriteWalTime(): Long + fun getWriteMemtableTime(): Long + fun getWriteDelayTime(): Long + fun getWriteSchedulingFlushesCompactionsTime(): Long + fun getWritePreAndPostProcessTime(): Long + fun getWriteThreadWaitNanos(): Long + fun getDbMutexLockNanos(): Long + fun getDbConditionWaitNanos(): Long + fun getMergeOperatorTimeNanos(): Long + fun getReadIndexBlockNanos(): Long + fun getReadFilterBlockNanos(): Long + fun getNewTableBlockIterNanos(): Long + fun getNewTableIteratorNanos(): Long + fun getBlockSeekNanos(): Long + fun getFindTableNanos(): Long + fun getBloomMemtableHitCount(): Long + fun getBloomMemtableMissCount(): Long + fun getBloomSstHitCount(): Long + fun getBloomSstMissCount(): Long + fun getKeyLockWaitTime(): Long + fun getKeyLockWaitCount(): Long + fun getEnvNewSequentialFileNanos(): Long + fun getEnvNewRandomAccessFileNanos(): Long + fun getEnvNewWritableFileNanos(): Long + fun getEnvReuseWritableFileNanos(): Long + fun getEnvNewRandomRwFileNanos(): Long + fun getEnvNewDirectoryNanos(): Long + fun getEnvFileExistsNanos(): Long + fun getEnvGetChildrenNanos(): Long + fun getEnvGetChildrenFileAttributesNanos(): Long + fun getEnvDeleteFileNanos(): Long + fun getEnvCreateDirNanos(): Long + fun getEnvCreateDirIfMissingNanos(): Long + fun getEnvDeleteDirNanos(): Long + fun getEnvGetFileSizeNanos(): Long + fun getEnvGetFileModificationTimeNanos(): Long + fun getEnvRenameFileNanos(): Long + fun getEnvLinkFileNanos(): Long + fun getEnvLockFileNanos(): Long + fun getEnvUnlockFileNanos(): Long + fun getEnvNewLoggerNanos(): Long + fun getGetCpuNanos(): Long + fun getIterNextCpuNanos(): Long + fun getIterPrevCpuNanos(): Long + fun getIterSeekCpuNanos(): Long + fun getEncryptDataNanos(): Long + fun getDecryptDataNanos(): Long + fun getNumberAsyncSeek(): Long + + fun toString(excludeZeroCounters: Boolean): String +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/PerfLevel.kt b/src/commonMain/kotlin/maryk/rocksdb/PerfLevel.kt new file mode 100644 index 0000000..fa64d1d --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/PerfLevel.kt @@ -0,0 +1,18 @@ +package maryk.rocksdb + +/** + * Controls which performance counters RocksDB collects. + */ +expect enum class PerfLevel { + UNINITIALIZED, + DISABLE, + ENABLE_COUNT, + ENABLE_TIME_EXCEPT_FOR_MUTEX, + ENABLE_TIME_AND_CPU_TIME_EXCEPT_FOR_MUTEX, + ENABLE_TIME, + OUT_OF_BOUNDS; + + fun getValue(): Byte +} + +expect fun perfLevelFromValue(value: Byte): PerfLevel diff --git a/src/commonMain/kotlin/maryk/rocksdb/RateLimiter.kt b/src/commonMain/kotlin/maryk/rocksdb/RateLimiter.kt new file mode 100644 index 0000000..473a3af --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/RateLimiter.kt @@ -0,0 +1,10 @@ +package maryk.rocksdb + +expect class RateLimiter : RocksObject { + constructor(rateBytesPerSecond: Long) + constructor(rateBytesPerSecond: Long, refillPeriodMicros: Long) + constructor(rateBytesPerSecond: Long, refillPeriodMicros: Long, fairness: Int) + + fun setBytesPerSecond(rateBytesPerSecond: Long) + fun getBytesPerSecond(): Long +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/RocksDB.kt b/src/commonMain/kotlin/maryk/rocksdb/RocksDB.kt index 96dc6b9..6167383 100644 --- a/src/commonMain/kotlin/maryk/rocksdb/RocksDB.kt +++ b/src/commonMain/kotlin/maryk/rocksdb/RocksDB.kt @@ -1639,6 +1639,13 @@ expect open class RocksDB : RocksObject { */ fun continueBackgroundWork() + /** + * Requests RocksDB to cancel background work (flushes, compactions, etc.). + * + * @param waitForExit when true, waits for background threads to exit before returning. + */ + fun cancelAllBackgroundWork(waitForExit: Boolean) + /** * Enable automatic compactions for the given column * families if they were previously disabled. @@ -1723,6 +1730,34 @@ expect open class RocksDB : RocksObject { */ fun getEnv(): Env + /** + * Adjusts the perf level controlling which counters are collected. + */ + fun setPerfLevel(perfLevel: PerfLevel) + + /** + * Returns the currently configured perf level for this database handle. + */ + fun getPerfLevel(): PerfLevel + + /** + * Returns the perf context that exposes collected counters. + */ + fun getPerfContext(): PerfContext + + /** + * Returns metadata for the SST files that are currently live. + */ + fun getLiveFilesMetaData(): List + + /** + * Creates an iterator over the database write-ahead log starting at the + * specified sequence number. + * + * @param sequenceNumber the WAL sequence number to start streaming from. + */ + fun getUpdatesSince(sequenceNumber: Long): TransactionLogIterator + /** * Flush the WAL memory buffer to the file. If `sync` is true, * it calls [.syncWal] afterwards. @@ -1831,6 +1866,13 @@ expect open class RocksDB : RocksObject { */ fun verifyChecksum() + /** + * When acting as a secondary instance, attempts to catch up with the primary. + * + * @throws RocksDBException if the catch-up operation fails. + */ + fun tryCatchUpWithPrimary() + /** * Gets the handle for the default column family * @return The handle of the default column family diff --git a/src/commonMain/kotlin/maryk/rocksdb/SanityLevel.kt b/src/commonMain/kotlin/maryk/rocksdb/SanityLevel.kt new file mode 100644 index 0000000..217a4c9 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/SanityLevel.kt @@ -0,0 +1,10 @@ +package maryk.rocksdb + +/** + * Sanity checks applied when loading options files. + */ +expect enum class SanityLevel { + NONE, + LOOSELY_COMPATIBLE, + EXACT_MATCH, +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/SstFileManager.kt b/src/commonMain/kotlin/maryk/rocksdb/SstFileManager.kt new file mode 100644 index 0000000..148f58b --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/SstFileManager.kt @@ -0,0 +1,26 @@ +package maryk.rocksdb + +/** + * Tracks SST files associated with a database and throttles deletion to avoid IO spikes. + */ +expect class SstFileManager(env: Env): AutoCloseable { + fun setMaxAllowedSpaceUsage(maxAllowedSpace: Long) + + fun setCompactionBufferSize(compactionBufferSize: Long) + + fun isMaxAllowedSpaceReached(): Boolean + + fun isMaxAllowedSpaceReachedIncludingCompactions(): Boolean + + fun getTotalSize(): Long + + fun getDeleteRateBytesPerSecond(): Long + + fun setDeleteRateBytesPerSecond(deleteRate: Long) + + fun getMaxTrashDBRatio(): Double + + fun setMaxTrashDBRatio(ratio: Double) + + override fun close() +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/SstFileMetaData.kt b/src/commonMain/kotlin/maryk/rocksdb/SstFileMetaData.kt index 9eed29e..8144ec9 100644 --- a/src/commonMain/kotlin/maryk/rocksdb/SstFileMetaData.kt +++ b/src/commonMain/kotlin/maryk/rocksdb/SstFileMetaData.kt @@ -1,6 +1,6 @@ package maryk.rocksdb -expect class SstFileMetaData { +expect open class SstFileMetaData { /** Get the name of the file. */ fun fileName(): String diff --git a/src/commonMain/kotlin/maryk/rocksdb/Statistics.kt b/src/commonMain/kotlin/maryk/rocksdb/Statistics.kt index 13d7f1b..9fa89d6 100644 --- a/src/commonMain/kotlin/maryk/rocksdb/Statistics.kt +++ b/src/commonMain/kotlin/maryk/rocksdb/Statistics.kt @@ -26,6 +26,14 @@ expect class Statistics : RocksObject { */ fun getTickerCount(tickerType: TickerType): Long + /** + * Returns the ticker count and atomically resets it to 0. + * + * @param tickerType The ticker to query. + * @return The value prior to reset. + */ + fun getAndResetTickerCount(tickerType: TickerType): Long + /** * Gets the histogram data for a particular histogram. * @@ -35,10 +43,19 @@ expect class Statistics : RocksObject { */ fun getHistogramData(histogramType: HistogramType): HistogramData + /** + * Returns the human readable histogram summary for the given histogram. + */ + fun getHistogramString(histogramType: HistogramType): String + /** * Resets all ticker and histogram stats. * * @throws RocksDBException if an error occurs when resetting the statistics. */ fun reset() + + override fun toString(): String } + +expect fun createStatistics(enabledHistograms: Set): Statistics diff --git a/src/commonMain/kotlin/maryk/rocksdb/StatisticsCollector.kt b/src/commonMain/kotlin/maryk/rocksdb/StatisticsCollector.kt new file mode 100644 index 0000000..7fe30b5 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/StatisticsCollector.kt @@ -0,0 +1,19 @@ +package maryk.rocksdb + +data class StatsCollectorInput( + val statistics: Statistics, + val callback: StatisticsCollectorCallback +) + +interface StatisticsCollectorCallback { + fun tickerCallback(tickerType: TickerType, tickerCount: Long) + fun histogramCallback(histogramType: HistogramType, histogramData: HistogramData) +} + +expect class StatisticsCollector( + statsCollectorInputList: List, + statsCollectionIntervalInMilliSeconds: Int +) { + fun start() + fun shutDown(shutdownTimeout: Int) +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/TableProperties.kt b/src/commonMain/kotlin/maryk/rocksdb/TableProperties.kt new file mode 100644 index 0000000..7d31fa9 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/TableProperties.kt @@ -0,0 +1,30 @@ +package maryk.rocksdb + +/** + * Read-only metadata exported for a flushed or ingested SST table. + */ +expect class TableProperties { + fun dataSize(): Long + fun indexSize(): Long + fun filterSize(): Long + fun rawKeySize(): Long + fun rawValueSize(): Long + fun numDataBlocks(): Long + fun numEntries(): Long + fun numDeletions(): Long + fun numMergeOperands(): Long + fun numRangeDeletions(): Long + fun formatVersion(): Long + fun columnFamilyId(): Long + fun columnFamilyName(): String? + fun creationTime(): Long + fun oldestKeyTime(): Long + fun slowCompressionEstimatedDataSize(): Long + fun fastCompressionEstimatedDataSize(): Long + fun filterPolicyName(): String? + fun comparatorName(): String? + fun mergeOperatorName(): String? + fun prefixExtractorName(): String? + fun propertyCollectorsNames(): String? + fun compressionName(): String? +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/TransactionLogIterator.kt b/src/commonMain/kotlin/maryk/rocksdb/TransactionLogIterator.kt new file mode 100644 index 0000000..665b3d6 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/TransactionLogIterator.kt @@ -0,0 +1,36 @@ +package maryk.rocksdb + +/** + * Streams batches from RocksDB's write-ahead log. + */ +expect class TransactionLogIterator : RocksObject { + /** + * @return true when the iterator currently points at a valid batch. + */ + fun isValid(): Boolean + + /** + * Advances the iterator to the next WAL batch. + */ + fun next() + + /** + * Throws a [RocksDBException] if the iterator encountered an error. + */ + @Throws(RocksDBException::class) + fun status() + + /** + * Returns the current batch. + */ + fun getBatch(): TransactionLogBatchResult +} + +/** + * Result containing the sequence number and write batch for a WAL entry. + */ +expect class TransactionLogBatchResult { + fun sequenceNumber(): Long + + fun writeBatch(): WriteBatch +} diff --git a/src/commonMain/kotlin/maryk/rocksdb/WALRecoveryMode.kt b/src/commonMain/kotlin/maryk/rocksdb/WALRecoveryMode.kt new file mode 100644 index 0000000..d67b8c3 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/WALRecoveryMode.kt @@ -0,0 +1,15 @@ +package maryk.rocksdb + +/** + * Recovery modes used when replaying the write-ahead log during open. + */ +expect enum class WALRecoveryMode { + TolerateCorruptedTailRecords, + AbsoluteConsistency, + PointInTimeRecovery, + SkipAnyCorruptedRecords; + + fun getValue(): Byte +} + +expect fun walRecoveryModeFromValue(value: Byte): WALRecoveryMode diff --git a/src/commonMain/kotlin/maryk/rocksdb/WriteBufferManager.kt b/src/commonMain/kotlin/maryk/rocksdb/WriteBufferManager.kt new file mode 100644 index 0000000..76b9a28 --- /dev/null +++ b/src/commonMain/kotlin/maryk/rocksdb/WriteBufferManager.kt @@ -0,0 +1,12 @@ +package maryk.rocksdb + +/** + * Coordinates write buffer memory usage across column families and instances. + */ +expect class WriteBufferManager : RocksObject { + constructor(bufferSize: Long, cache: Cache, allowStall: Boolean) + + constructor(bufferSize: Long, cache: Cache) + + fun allowStall(): Boolean +} diff --git a/src/commonTest/kotlin/maryk/rocksdb/BackupEngineOptionsTest.kt b/src/commonTest/kotlin/maryk/rocksdb/BackupEngineOptionsTest.kt new file mode 100644 index 0000000..359dae3 --- /dev/null +++ b/src/commonTest/kotlin/maryk/rocksdb/BackupEngineOptionsTest.kt @@ -0,0 +1,168 @@ +package maryk.rocksdb + +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue +import maryk.createFolder +import maryk.rocksdb.util.createTestDBFolder + +class BackupEngineOptionsTest { + init { + loadRocksDBLibrary() + } + + private fun newBackupDir(suffix: String): String { + val path = createTestDBFolder("BackupEngineOptionsTest-$suffix") + createFolder(path) + return path + } + + @Test + fun backupDir() { + val path = newBackupDir("backupDir") + BackupEngineOptions(path).use { options -> + assertEquals(path, options.backupDir()) + } + } + + @Test + fun backupEnvRoundTrip() { + val path = newBackupDir("env") + val env = getDefaultEnv() + BackupEngineOptions(path).use { options -> + options.setBackupEnv(env) + assertSame(env, options.backupEnv()) + } + } + + @Test + fun shareTableFilesRoundTrip() { + val path = newBackupDir("shareTableFiles") + BackupEngineOptions(path).use { options -> + options.setShareTableFiles(true) + assertTrue(options.shareTableFiles()) + + options.setShareTableFiles(false) + assertFalse(options.shareTableFiles()) + } + } + + @Test + fun syncRoundTrip() { + val path = newBackupDir("sync") + BackupEngineOptions(path).use { options -> + options.setSync(true) + assertTrue(options.sync()) + + options.setSync(false) + assertFalse(options.sync()) + } + } + + @Test + fun destroyOldDataRoundTrip() { + val path = newBackupDir("destroyOldData") + BackupEngineOptions(path).use { options -> + options.setDestroyOldData(true) + assertTrue(options.destroyOldData()) + + options.setDestroyOldData(false) + assertFalse(options.destroyOldData()) + } + } + + @Test + fun backupLogFilesRoundTrip() { + val path = newBackupDir("backupLogFiles") + BackupEngineOptions(path).use { options -> + options.setBackupLogFiles(true) + assertTrue(options.backupLogFiles()) + + options.setBackupLogFiles(false) + assertFalse(options.backupLogFiles()) + } + } + + @Test + fun backupRateLimit() { + val path = newBackupDir("backupRateLimit") + val rate = kotlin.math.abs(Random.nextLong()) + BackupEngineOptions(path).use { options -> + options.setBackupRateLimit(rate) + assertEquals(rate, options.backupRateLimit()) + + options.setBackupRateLimit(-1) + assertEquals(0, options.backupRateLimit()) + } + } + + @Test + fun backupRateLimiterReference() { + val path = newBackupDir("backupRateLimiter") + BackupEngineOptions(path).use { options -> + RateLimiter(1_000).use { limiter -> + options.setBackupRateLimiter(limiter) + assertSame(limiter, options.backupRateLimiter()) + } + } + } + + @Test + fun restoreRateLimit() { + val path = newBackupDir("restoreRateLimit") + val rate = kotlin.math.abs(Random.nextLong()) + BackupEngineOptions(path).use { options -> + options.setRestoreRateLimit(rate) + assertEquals(rate, options.restoreRateLimit()) + + options.setRestoreRateLimit(-42) + assertEquals(0, options.restoreRateLimit()) + } + } + + @Test + fun restoreRateLimiterReference() { + val path = newBackupDir("restoreRateLimiter") + BackupEngineOptions(path).use { options -> + RateLimiter(2_000).use { limiter -> + options.setRestoreRateLimiter(limiter) + assertSame(limiter, options.restoreRateLimiter()) + } + } + } + + @Test + fun shareFilesWithChecksumRoundTrip() { + val path = newBackupDir("shareFilesWithChecksum") + BackupEngineOptions(path).use { options -> + options.setShareFilesWithChecksum(true) + assertTrue(options.shareFilesWithChecksum()) + + options.setShareFilesWithChecksum(false) + assertFalse(options.shareFilesWithChecksum()) + } + } + + @Test + fun maxBackgroundOperationsRoundTrip() { + val path = newBackupDir("maxBackground") + val value = Random.nextInt() + BackupEngineOptions(path).use { options -> + options.setMaxBackgroundOperations(value) + assertEquals(value, options.maxBackgroundOperations()) + } + } + + @Test + fun callbackTriggerIntervalSizeRoundTrip() { + val path = newBackupDir("callbackInterval") + val value = Random.nextLong() + BackupEngineOptions(path).use { options -> + options.setCallbackTriggerIntervalSize(value) + assertEquals(value, options.callbackTriggerIntervalSize()) + } + } +} diff --git a/src/commonTest/kotlin/maryk/rocksdb/CompactionOptionsFIFOTest.kt b/src/commonTest/kotlin/maryk/rocksdb/CompactionOptionsFIFOTest.kt new file mode 100644 index 0000000..f23440c --- /dev/null +++ b/src/commonTest/kotlin/maryk/rocksdb/CompactionOptionsFIFOTest.kt @@ -0,0 +1,31 @@ +package maryk.rocksdb + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class CompactionOptionsFIFOTest { + init { + loadRocksDBLibrary() + } + + @Test + fun maxTableFilesSize() { + val size = 500L * 1024 * 1026 + CompactionOptionsFIFO().use { options -> + options.setMaxTableFilesSize(size) + assertEquals(size, options.maxTableFilesSize()) + } + } + + @Test + fun allowCompactionFlag() { + CompactionOptionsFIFO().use { options -> + assertFalse(options.allowCompaction()) + + options.setAllowCompaction(true) + assertTrue(options.allowCompaction()) + } + } +} diff --git a/src/commonTest/kotlin/maryk/rocksdb/CompactionOptionsTest.kt b/src/commonTest/kotlin/maryk/rocksdb/CompactionOptionsTest.kt new file mode 100644 index 0000000..d261b12 --- /dev/null +++ b/src/commonTest/kotlin/maryk/rocksdb/CompactionOptionsTest.kt @@ -0,0 +1,43 @@ +package maryk.rocksdb + +import kotlin.test.Test +import kotlin.test.assertEquals + +class CompactionOptionsTest { + init { + loadRocksDBLibrary() + } + + @Test + fun compressionRoundTrip() { + CompactionOptions().use { options -> + val original = options.compression() + options.setCompression(CompressionType.NO_COMPRESSION) + assertEquals(CompressionType.NO_COMPRESSION, options.compression()) + + options.setCompression(original) + assertEquals(original, options.compression()) + } + } + + @Test + fun outputFileSizeLimit() { + val limit = 250L * 1024 * 1024 + CompactionOptions().use { options -> + assertEquals(-1, options.outputFileSizeLimit()) + + options.setOutputFileSizeLimit(limit) + assertEquals(limit, options.outputFileSizeLimit()) + } + } + + @Test + fun maxSubcompactions() { + CompactionOptions().use { options -> + assertEquals(0, options.maxSubcompactions()) + + options.setMaxSubcompactions(9) + assertEquals(9, options.maxSubcompactions()) + } + } +} diff --git a/src/commonTest/kotlin/maryk/rocksdb/CompactionOptionsUniversalTest.kt b/src/commonTest/kotlin/maryk/rocksdb/CompactionOptionsUniversalTest.kt new file mode 100644 index 0000000..8a883b4 --- /dev/null +++ b/src/commonTest/kotlin/maryk/rocksdb/CompactionOptionsUniversalTest.kt @@ -0,0 +1,64 @@ +package maryk.rocksdb + +import kotlin.test.Test +import kotlin.test.assertEquals + +class CompactionOptionsUniversalTest { + init { + loadRocksDBLibrary() + } + + @Test + fun sizeRatio() { + val sizeRatio = 4 + CompactionOptionsUniversal().use { options -> + options.setSizeRatio(sizeRatio) + assertEquals(sizeRatio, options.sizeRatio()) + } + } + + @Test + fun minMergeWidth() { + val minMergeWidth = 3 + CompactionOptionsUniversal().use { options -> + options.setMinMergeWidth(minMergeWidth) + assertEquals(minMergeWidth, options.minMergeWidth()) + } + } + + @Test + fun maxMergeWidth() { + val maxMergeWidth = Int.MAX_VALUE - 1234 + CompactionOptionsUniversal().use { options -> + options.setMaxMergeWidth(maxMergeWidth) + assertEquals(maxMergeWidth, options.maxMergeWidth()) + } + } + + @Test + fun maxSizeAmplificationPercent() { + val amplification = 150 + CompactionOptionsUniversal().use { options -> + options.setMaxSizeAmplificationPercent(amplification) + assertEquals(amplification, options.maxSizeAmplificationPercent()) + } + } + + @Test + fun compressionSizePercent() { + val compressionSize = 500 + CompactionOptionsUniversal().use { options -> + options.setCompressionSizePercent(compressionSize) + assertEquals(compressionSize, options.compressionSizePercent()) + } + } + + @Test + fun stopStyle() { + val stopStyle = CompactionStopStyle.CompactionStopStyleSimilarSize + CompactionOptionsUniversal().use { options -> + options.setStopStyle(stopStyle) + assertEquals(stopStyle, options.stopStyle()) + } + } +} diff --git a/src/commonTest/kotlin/maryk/rocksdb/CompressionOptionsTest.kt b/src/commonTest/kotlin/maryk/rocksdb/CompressionOptionsTest.kt new file mode 100644 index 0000000..8ec3f49 --- /dev/null +++ b/src/commonTest/kotlin/maryk/rocksdb/CompressionOptionsTest.kt @@ -0,0 +1,46 @@ +package maryk.rocksdb + +import kotlin.test.Test +import kotlin.test.assertEquals + +class CompressionOptionsTest { + init { + loadRocksDBLibrary() + } + + @Test + fun windowBits() { + val windowBits = 7 + CompressionOptions().use { options -> + options.setWindowBits(windowBits) + assertEquals(windowBits, options.windowBits()) + } + } + + @Test + fun level() { + val level = 6 + CompressionOptions().use { options -> + options.setLevel(level) + assertEquals(level, options.level()) + } + } + + @Test + fun strategy() { + val strategy = 2 + CompressionOptions().use { options -> + options.setStrategy(strategy) + assertEquals(strategy, options.strategy()) + } + } + + @Test + fun maxDictBytes() { + val maxDictBytes = 999 + CompressionOptions().use { options -> + options.setMaxDictBytes(maxDictBytes) + assertEquals(maxDictBytes, options.maxDictBytes()) + } + } +} diff --git a/src/commonTest/kotlin/maryk/rocksdb/ImportColumnFamilyOptionsTest.kt b/src/commonTest/kotlin/maryk/rocksdb/ImportColumnFamilyOptionsTest.kt new file mode 100644 index 0000000..df5ac44 --- /dev/null +++ b/src/commonTest/kotlin/maryk/rocksdb/ImportColumnFamilyOptionsTest.kt @@ -0,0 +1,21 @@ +package maryk.rocksdb + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ImportColumnFamilyOptionsTest { + init { + loadRocksDBLibrary() + } + + @Test + fun moveFilesFlag() { + ImportColumnFamilyOptions().use { options -> + assertFalse(options.moveFiles()) + + options.setMoveFiles(true) + assertTrue(options.moveFiles()) + } + } +} diff --git a/src/commonTest/kotlin/maryk/rocksdb/SstFileManagerTest.kt b/src/commonTest/kotlin/maryk/rocksdb/SstFileManagerTest.kt new file mode 100644 index 0000000..e53dbb7 --- /dev/null +++ b/src/commonTest/kotlin/maryk/rocksdb/SstFileManagerTest.kt @@ -0,0 +1,44 @@ +package maryk.rocksdb + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +class SstFileManagerTest { + init { + loadRocksDBLibrary() + } + + @Test + fun configureLimitsAndRates() { + val maxSpace = 64L * 1024 * 1024 + val bufferSize = 10L * 1024 * 1024 + val deleteRate = 52L * 1024 * 1024 + val trashRatio = 0.2 + + getDefaultEnv().use { env -> + SstFileManager(env).use { manager -> + assertEquals(0, manager.getDeleteRateBytesPerSecond()) + manager.setMaxAllowedSpaceUsage(maxSpace) + manager.setCompactionBufferSize(bufferSize) + manager.setDeleteRateBytesPerSecond(deleteRate) + manager.setMaxTrashDBRatio(trashRatio) + + assertFalse(manager.isMaxAllowedSpaceReached()) + assertFalse(manager.isMaxAllowedSpaceReachedIncludingCompactions()) + assertEquals(deleteRate, manager.getDeleteRateBytesPerSecond()) + assertEquals(trashRatio, manager.getMaxTrashDBRatio(), 1e-6) + } + } + } + + @Test + fun totalSizeStartsAtZero() { + getDefaultEnv().use { env -> + SstFileManager(env).use { manager -> + assertEquals(0, manager.getTotalSize()) + assertEquals(0.25, manager.getMaxTrashDBRatio(), 1e-6) + } + } + } +} diff --git a/src/commonTest/kotlin/maryk/rocksdb/StatisticsCollectorTest.kt b/src/commonTest/kotlin/maryk/rocksdb/StatisticsCollectorTest.kt new file mode 100644 index 0000000..ffb49d8 --- /dev/null +++ b/src/commonTest/kotlin/maryk/rocksdb/StatisticsCollectorTest.kt @@ -0,0 +1,73 @@ +package maryk.rocksdb + +import kotlin.test.Test +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TimeSource +import maryk.rocksdb.util.ThreadSafeCounter +import maryk.rocksdb.util.createTestDBFolder +import maryk.rocksdb.util.sleepMillis + +class StatisticsCollectorTest { + init { + loadRocksDBLibrary() + } + + private fun createTestFolder() = createTestDBFolder("StatisticsCollectorTest") + + @Test + fun statisticsCollectorDeliversCallbacks() { + val dbPath = createTestFolder() + Statistics().use { statistics -> + statistics.setStatsLevel(StatsLevel.ALL) + + Options().setCreateIfMissing(true).use { options -> + options.setStatistics(statistics) + + openRocksDB(options, dbPath).use { db -> + val tickerCallbackCount = ThreadSafeCounter() + val histogramCallbackCount = ThreadSafeCounter() + + val callback = object : StatisticsCollectorCallback { + override fun tickerCallback(tickerType: TickerType, tickerCount: Long) { + tickerCallbackCount.increment() + } + + override fun histogramCallback( + histogramType: HistogramType, + histogramData: HistogramData + ) { + histogramCallbackCount.increment() + } + } + + val collector = StatisticsCollector( + listOf(StatsCollectorInput(statistics, callback)), + 100 + ) + collector.start() + + repeat(10) { index -> + val key = "stats-key-$index".encodeToByteArray() + val value = ByteArray(32) { index.toByte() } + db.put(key, value) + db.get(key) + } + + val timeout = 5.seconds + val timer = TimeSource.Monotonic.markNow() + while ((tickerCallbackCount.value() == 0 || histogramCallbackCount.value() == 0) && + timer.elapsedNow() < timeout + ) { + sleepMillis(50) + } + + collector.shutDown(1_000) + + assertTrue(tickerCallbackCount.value() > 0, "Ticker callbacks should be invoked") + assertTrue(histogramCallbackCount.value() > 0, "Histogram callbacks should be invoked") + } + } + } + } +} diff --git a/src/commonTest/kotlin/maryk/rocksdb/StatisticsTest.kt b/src/commonTest/kotlin/maryk/rocksdb/StatisticsTest.kt new file mode 100644 index 0000000..4e6bb8c --- /dev/null +++ b/src/commonTest/kotlin/maryk/rocksdb/StatisticsTest.kt @@ -0,0 +1,182 @@ +package maryk.rocksdb + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import maryk.rocksdb.util.createTestDBFolder + +class StatisticsTest { + init { + loadRocksDBLibrary() + } + + private fun newDbPath(suffix: String) = createTestDBFolder("StatisticsTest-$suffix") + + @Test + fun createStatistics() { + Statistics().use { statistics -> + statistics.setStatsLevel(StatsLevel.EXCEPT_DETAILED_TIMERS) + } + + createStatistics(setOf(HistogramType.DB_WRITE, HistogramType.COMPACTION_TIME)).use { statistics -> + statistics.reset() + } + } + + @Test + fun statsLevel() { + Statistics().use { statistics -> + statistics.setStatsLevel(StatsLevel.ALL) + assertEquals(StatsLevel.ALL, statistics.statsLevel()) + } + } + + @Test + fun getTickerCount() { + val dbPath = newDbPath("getTickerCount") + Statistics().use { statistics -> + Options().setCreateIfMissing(true).use { options -> + options.setStatistics(statistics) + + openRocksDB(options, dbPath).use { db -> + val key = "some-key".encodeToByteArray() + val value = "some-value".encodeToByteArray() + + db.put(key, value) + repeat(10) { + db.get(key) + } + + assertTrue( + statistics.getTickerCount(TickerType.BYTES_READ) > 0, + "Ticker count should increase after reads" + ) + } + } + } + } + + @Test + fun getAndResetTickerCount() { + val dbPath = newDbPath("getAndResetTickerCount") + Statistics().use { statistics -> + Options().setCreateIfMissing(true).use { options -> + options.setStatistics(statistics) + + openRocksDB(options, dbPath).use { db -> + val key = "some-key".encodeToByteArray() + val value = "some-value".encodeToByteArray() + + db.put(key, value) + repeat(10) { + db.get(key) + } + + val read = statistics.getAndResetTickerCount(TickerType.BYTES_READ) + assertTrue(read > 0, "Ticker count should be non-zero before reset") + + val afterReset = statistics.getTickerCount(TickerType.BYTES_READ) + assertTrue(afterReset < read, "Ticker count should drop after reset") + } + } + } + } + + @Test + fun getHistogramData() { + val dbPath = newDbPath("getHistogramData") + Statistics().use { statistics -> + Options().setCreateIfMissing(true).use { options -> + options.setStatistics(statistics) + + openRocksDB(options, dbPath).use { db -> + val key = "some-key".encodeToByteArray() + val value = "some-value".encodeToByteArray() + + db.put(key, value) + repeat(10) { + db.get(key) + } + + val histogramData = statistics.getHistogramData(HistogramType.BYTES_PER_READ) + assertNotNull(histogramData) + assertTrue(histogramData.getAverage() > 0) + assertTrue(histogramData.getMedian() > 0) + assertTrue(histogramData.getPercentile95() > 0) + assertTrue(histogramData.getPercentile99() > 0) + assertEquals(0.0, histogramData.getStandardDeviation()) + assertTrue(histogramData.getMax() > 0) + assertTrue(histogramData.getCount() > 0) + assertTrue(histogramData.getSum() > 0) + assertTrue(histogramData.getMin() > 0) + } + } + } + } + + @Test + fun getHistogramString() { + val dbPath = newDbPath("getHistogramString") + Statistics().use { statistics -> + Options().setCreateIfMissing(true).use { options -> + options.setStatistics(statistics) + + openRocksDB(options, dbPath).use { db -> + val key = "some-key".encodeToByteArray() + val value = "some-value".encodeToByteArray() + + repeat(10) { + db.put(key, value) + } + + val histogramString = statistics.getHistogramString(HistogramType.BYTES_PER_WRITE) + assertTrue(histogramString.isNotEmpty(), "Histogram string should not be empty") + } + } + } + } + + @Test + fun reset() { + val dbPath = newDbPath("reset") + Statistics().use { statistics -> + Options().setCreateIfMissing(true).use { options -> + options.setStatistics(statistics) + + openRocksDB(options, dbPath).use { db -> + val key = "some-key".encodeToByteArray() + val value = "some-value".encodeToByteArray() + + db.put(key, value) + repeat(10) { + db.get(key) + } + + val read = statistics.getTickerCount(TickerType.BYTES_READ) + assertTrue(read > 0) + + statistics.reset() + + val afterReset = statistics.getTickerCount(TickerType.BYTES_READ) + assertTrue(afterReset < read) + } + } + } + } + + @Test + fun statisticsToString() { + val dbPath = newDbPath("toString") + Statistics().use { statistics -> + Options().setCreateIfMissing(true).use { options -> + options.setStatistics(statistics) + + openRocksDB(options, dbPath).use { _ -> + assertNotEquals("", statistics.toString()) + } + } + } + } +} diff --git a/src/commonTest/kotlin/maryk/rocksdb/WriteBufferManagerTest.kt b/src/commonTest/kotlin/maryk/rocksdb/WriteBufferManagerTest.kt new file mode 100644 index 0000000..30b29ff --- /dev/null +++ b/src/commonTest/kotlin/maryk/rocksdb/WriteBufferManagerTest.kt @@ -0,0 +1,24 @@ +package maryk.rocksdb + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class WriteBufferManagerTest { + init { + loadRocksDBLibrary() + } + + @Test + fun allowStallFlagReflectsConstruction() { + LRUCache(4L * 1024 * 1024).use { cache -> + WriteBufferManager(2L * 1024 * 1024, cache).use { manager -> + assertFalse(manager.allowStall()) + } + + WriteBufferManager(2L * 1024 * 1024, cache, allowStall = true).use { manager -> + assertTrue(manager.allowStall()) + } + } + } +} diff --git a/src/commonTest/kotlin/maryk/rocksdb/util/Sleep.kt b/src/commonTest/kotlin/maryk/rocksdb/util/Sleep.kt new file mode 100644 index 0000000..df7f3dd --- /dev/null +++ b/src/commonTest/kotlin/maryk/rocksdb/util/Sleep.kt @@ -0,0 +1,3 @@ +package maryk.rocksdb.util + +expect fun sleepMillis(millis: Long) diff --git a/src/commonTest/kotlin/maryk/rocksdb/util/ThreadSafeCounter.kt b/src/commonTest/kotlin/maryk/rocksdb/util/ThreadSafeCounter.kt new file mode 100644 index 0000000..261282c --- /dev/null +++ b/src/commonTest/kotlin/maryk/rocksdb/util/ThreadSafeCounter.kt @@ -0,0 +1,6 @@ +package maryk.rocksdb.util + +expect class ThreadSafeCounter() { + fun increment() + fun value(): Int +} diff --git a/src/jvmMain/kotlin/maryk/rocksdb/BloomFilter.kt b/src/jvmMain/kotlin/maryk/rocksdb/BloomFilter.kt new file mode 100644 index 0000000..a6fd154 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/BloomFilter.kt @@ -0,0 +1,3 @@ +package maryk.rocksdb + +actual typealias BloomFilter = org.rocksdb.BloomFilter diff --git a/src/jvmMain/kotlin/maryk/rocksdb/CompactionJobInfo.kt b/src/jvmMain/kotlin/maryk/rocksdb/CompactionJobInfo.kt new file mode 100644 index 0000000..58fe70c --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/CompactionJobInfo.kt @@ -0,0 +1,56 @@ +package maryk.rocksdb + +import kotlin.use + +actual class CompactionJobInfo internal constructor( + internal val delegate: org.rocksdb.CompactionJobInfo +) { + private val stats: CompactionJobStats = delegate.stats()?.let { stats -> + stats.use { + CompactionJobStats( + elapsedMicrosValue = it.elapsedMicros(), + numInputRecordsValue = it.numInputRecords(), + numOutputRecordsValue = it.numOutputRecords(), + totalInputBytesValue = it.totalInputBytes(), + totalOutputBytesValue = it.totalOutputBytes(), + numInputFilesValue = it.numInputFiles(), + numInputFilesAtOutputLevelValue = it.numInputFilesAtOutputLevel(), + numOutputFilesValue = it.numOutputFiles(), + numCorruptKeysValue = it.numCorruptKeys(), + ) + } + } ?: CompactionJobStats.empty( + numInputFilesFallback = delegate.inputFiles().size.toLong(), + numOutputFilesFallback = delegate.outputFiles().size.toLong(), + ) + + actual fun columnFamilyName(): ByteArray = delegate.columnFamilyName() + + actual fun baseInputLevel(): Int = delegate.baseInputLevel() + + actual fun outputLevel(): Int = delegate.outputLevel() + + actual fun inputFiles(): List = delegate.inputFiles() + + actual fun outputFiles(): List = delegate.outputFiles() + + actual fun elapsedMicros(): Long = stats.elapsedMicros() + + actual fun numCorruptKeys(): Long = stats.numCorruptKeys() + + actual fun inputRecords(): Long = stats.numInputRecords() + + actual fun outputRecords(): Long = stats.numOutputRecords() + + actual fun totalInputBytes(): Long = stats.totalInputBytes() + + actual fun totalOutputBytes(): Long = stats.totalOutputBytes() + + actual fun compactionReason(): CompactionReason = delegate.compactionReason() + + actual fun numInputFiles(): Long = stats.numInputFiles() + + actual fun numInputFilesAtOutputLevel(): Long = stats.numInputFilesAtOutputLevel() + + actual fun compactionStats(): CompactionJobStats = stats +} diff --git a/src/jvmMain/kotlin/maryk/rocksdb/CompactionOptions.kt b/src/jvmMain/kotlin/maryk/rocksdb/CompactionOptions.kt new file mode 100644 index 0000000..dd3ed59 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/CompactionOptions.kt @@ -0,0 +1,5 @@ +package maryk.rocksdb + +actual typealias CompactionOptions = org.rocksdb.CompactionOptions +actual typealias CompactionOptionsFIFO = org.rocksdb.CompactionOptionsFIFO +actual typealias CompactionOptionsUniversal = org.rocksdb.CompactionOptionsUniversal diff --git a/src/jvmMain/kotlin/maryk/rocksdb/CompressionOptions.kt b/src/jvmMain/kotlin/maryk/rocksdb/CompressionOptions.kt new file mode 100644 index 0000000..8664ce3 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/CompressionOptions.kt @@ -0,0 +1,3 @@ +package maryk.rocksdb + +actual typealias CompressionOptions = org.rocksdb.CompressionOptions diff --git a/src/jvmMain/kotlin/maryk/rocksdb/ConfigOptions.kt b/src/jvmMain/kotlin/maryk/rocksdb/ConfigOptions.kt new file mode 100644 index 0000000..5275054 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/ConfigOptions.kt @@ -0,0 +1,3 @@ +package maryk.rocksdb + +actual typealias ConfigOptions = org.rocksdb.ConfigOptions diff --git a/src/jvmMain/kotlin/maryk/rocksdb/DBOptions.kt b/src/jvmMain/kotlin/maryk/rocksdb/DBOptions.kt index 882440e..eebc427 100644 --- a/src/jvmMain/kotlin/maryk/rocksdb/DBOptions.kt +++ b/src/jvmMain/kotlin/maryk/rocksdb/DBOptions.kt @@ -1,3 +1,18 @@ package maryk.rocksdb actual typealias DBOptions = org.rocksdb.DBOptions + +actual fun DBOptions.addEventListener(listener: EventListener): DBOptions { + val existing: MutableList = + listeners()?.toMutableList() ?: mutableListOf() + existing += listener + return setListeners(existing) +} + +actual fun Options.addEventListener(listener: EventListener): Options { + val existing: MutableList = + listeners()?.toMutableList() ?: mutableListOf() + existing += listener + setListeners(existing) + return this +} diff --git a/src/jvmMain/kotlin/maryk/rocksdb/EventListener.kt b/src/jvmMain/kotlin/maryk/rocksdb/EventListener.kt new file mode 100644 index 0000000..e083464 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/EventListener.kt @@ -0,0 +1,42 @@ +package maryk.rocksdb + +actual abstract class EventListener : org.rocksdb.AbstractEventListener() { + actual open fun onFlushBeginEvent(db: RocksDB, flushJobInfo: FlushJobInfo) {} + + actual open fun onFlushCompletedEvent(db: RocksDB, flushJobInfo: FlushJobInfo) {} + + actual open fun onCompactionBeginEvent(db: RocksDB, compactionJobInfo: CompactionJobInfo) {} + + actual open fun onCompactionCompletedEvent(db: RocksDB, compactionJobInfo: CompactionJobInfo) {} + + actual open fun onExternalFileIngested(db: RocksDB, ingestionInfo: ExternalFileIngestionInfo) {} + + final override fun onFlushBegin(db: org.rocksdb.RocksDB, flushJobInfo: org.rocksdb.FlushJobInfo) { + onFlushBeginEvent(db, FlushJobInfo(flushJobInfo)) + } + + final override fun onFlushCompleted(db: org.rocksdb.RocksDB, flushJobInfo: org.rocksdb.FlushJobInfo) { + onFlushCompletedEvent(db, FlushJobInfo(flushJobInfo)) + } + + final override fun onCompactionBegin( + db: org.rocksdb.RocksDB, + compactionJobInfo: org.rocksdb.CompactionJobInfo + ) { + onCompactionBeginEvent(db, CompactionJobInfo(compactionJobInfo)) + } + + final override fun onCompactionCompleted( + db: org.rocksdb.RocksDB, + compactionJobInfo: org.rocksdb.CompactionJobInfo + ) { + onCompactionCompletedEvent(db, CompactionJobInfo(compactionJobInfo)) + } + + final override fun onExternalFileIngested( + db: org.rocksdb.RocksDB, + ingestionInfo: org.rocksdb.ExternalFileIngestionInfo + ) { + onExternalFileIngested(db, ExternalFileIngestionInfo(ingestionInfo)) + } +} diff --git a/src/jvmMain/kotlin/maryk/rocksdb/ExternalFileIngestionInfo.kt b/src/jvmMain/kotlin/maryk/rocksdb/ExternalFileIngestionInfo.kt new file mode 100644 index 0000000..f504a20 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/ExternalFileIngestionInfo.kt @@ -0,0 +1,16 @@ +package maryk.rocksdb + +actual class ExternalFileIngestionInfo internal constructor( + internal val delegate: org.rocksdb.ExternalFileIngestionInfo +) { + actual fun columnFamilyName(): String = delegate.columnFamilyName + + actual fun externalFilePath(): String = delegate.externalFilePath + + actual fun internalFilePath(): String = delegate.internalFilePath + + actual fun globalSequenceNumber(): Long = delegate.globalSeqno + + actual fun tableProperties(): TableProperties? = + delegate.tableProperties?.let(::TableProperties) +} diff --git a/src/jvmMain/kotlin/maryk/rocksdb/FlushJobInfo.kt b/src/jvmMain/kotlin/maryk/rocksdb/FlushJobInfo.kt new file mode 100644 index 0000000..6ebb3de --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/FlushJobInfo.kt @@ -0,0 +1,27 @@ +package maryk.rocksdb + +actual class FlushJobInfo internal constructor( + internal val delegate: org.rocksdb.FlushJobInfo +) { + actual fun columnFamilyId(): Long = delegate.columnFamilyId + + actual fun columnFamilyName(): String = delegate.columnFamilyName + + actual fun filePath(): String = delegate.filePath + + actual fun threadId(): Long = delegate.threadId + + actual fun jobId(): Int = delegate.jobId + + actual fun triggeredWritesSlowdown(): Boolean = delegate.isTriggeredWritesSlowdown + + actual fun triggeredWritesStop(): Boolean = delegate.isTriggeredWritesStop + + actual fun smallestSeqno(): Long = delegate.smallestSeqno + + actual fun largestSeqno(): Long = delegate.largestSeqno + + actual fun tableProperties(): TableProperties = TableProperties(delegate.tableProperties) + + actual fun flushReason(): FlushReason = delegate.flushReason +} diff --git a/src/jvmMain/kotlin/maryk/rocksdb/FlushOptions.kt b/src/jvmMain/kotlin/maryk/rocksdb/FlushOptions.kt new file mode 100644 index 0000000..f04f3f8 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/FlushOptions.kt @@ -0,0 +1,3 @@ +package maryk.rocksdb + +actual typealias FlushOptions = org.rocksdb.FlushOptions diff --git a/src/jvmMain/kotlin/maryk/rocksdb/FlushReason.kt b/src/jvmMain/kotlin/maryk/rocksdb/FlushReason.kt new file mode 100644 index 0000000..bae20f3 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/FlushReason.kt @@ -0,0 +1,9 @@ +package maryk.rocksdb + +actual typealias FlushReason = org.rocksdb.FlushReason + +actual fun FlushReason.value(): Byte = ordinal.toByte() + +actual fun flushReasonFromValue(value: Byte): FlushReason = + FlushReason.values().firstOrNull { it.ordinal.toByte() == value } + ?: error("Unknown FlushReason value: $value") diff --git a/src/jvmMain/kotlin/maryk/rocksdb/ImportColumnFamilyOptions.kt b/src/jvmMain/kotlin/maryk/rocksdb/ImportColumnFamilyOptions.kt new file mode 100644 index 0000000..22c82ff --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/ImportColumnFamilyOptions.kt @@ -0,0 +1,3 @@ +package maryk.rocksdb + +actual typealias ImportColumnFamilyOptions = org.rocksdb.ImportColumnFamilyOptions diff --git a/src/jvmMain/kotlin/maryk/rocksdb/LiveFileMetaData.kt b/src/jvmMain/kotlin/maryk/rocksdb/LiveFileMetaData.kt new file mode 100644 index 0000000..747f0a1 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/LiveFileMetaData.kt @@ -0,0 +1,3 @@ +package maryk.rocksdb + +actual typealias LiveFileMetaData = org.rocksdb.LiveFileMetaData diff --git a/src/jvmMain/kotlin/maryk/rocksdb/OptionsUtil.kt b/src/jvmMain/kotlin/maryk/rocksdb/OptionsUtil.kt new file mode 100644 index 0000000..6c12f1f --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/OptionsUtil.kt @@ -0,0 +1,38 @@ +package maryk.rocksdb + +actual object OptionsUtil { + @Throws(RocksDBException::class) + actual fun loadLatestOptions( + configOptions: ConfigOptions, + dbPath: String, + dbOptions: DBOptions, + columnFamilyDescriptors: MutableList + ) { + org.rocksdb.OptionsUtil.loadLatestOptions( + configOptions, + dbPath, + dbOptions, + columnFamilyDescriptors + ) + } + + @Throws(RocksDBException::class) + actual fun loadOptionsFromFile( + configOptions: ConfigOptions, + optionsFilePath: String, + dbOptions: DBOptions, + columnFamilyDescriptors: MutableList + ) { + org.rocksdb.OptionsUtil.loadOptionsFromFile( + configOptions, + optionsFilePath, + dbOptions, + columnFamilyDescriptors + ) + } + + @Throws(RocksDBException::class) + actual fun getLatestOptionsFileName(dbPath: String, env: Env): String { + return org.rocksdb.OptionsUtil.getLatestOptionsFileName(dbPath, env) + } +} diff --git a/src/jvmMain/kotlin/maryk/rocksdb/PerfContext.kt b/src/jvmMain/kotlin/maryk/rocksdb/PerfContext.kt new file mode 100644 index 0000000..8de51d7 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/PerfContext.kt @@ -0,0 +1,3 @@ +package maryk.rocksdb + +actual typealias PerfContext = org.rocksdb.PerfContext diff --git a/src/jvmMain/kotlin/maryk/rocksdb/PerfLevel.kt b/src/jvmMain/kotlin/maryk/rocksdb/PerfLevel.kt new file mode 100644 index 0000000..fbae63c --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/PerfLevel.kt @@ -0,0 +1,5 @@ +package maryk.rocksdb + +actual typealias PerfLevel = org.rocksdb.PerfLevel + +actual fun perfLevelFromValue(value: Byte): PerfLevel = PerfLevel.getPerfLevel(value) diff --git a/src/jvmMain/kotlin/maryk/rocksdb/RateLimiter.kt b/src/jvmMain/kotlin/maryk/rocksdb/RateLimiter.kt new file mode 100644 index 0000000..2104b9e --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/RateLimiter.kt @@ -0,0 +1,3 @@ +package maryk.rocksdb + +actual typealias RateLimiter = org.rocksdb.RateLimiter diff --git a/src/jvmMain/kotlin/maryk/rocksdb/SanityLevel.kt b/src/jvmMain/kotlin/maryk/rocksdb/SanityLevel.kt new file mode 100644 index 0000000..ec494d1 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/SanityLevel.kt @@ -0,0 +1,3 @@ +package maryk.rocksdb + +actual typealias SanityLevel = org.rocksdb.SanityLevel diff --git a/src/jvmMain/kotlin/maryk/rocksdb/SstFileManager.kt b/src/jvmMain/kotlin/maryk/rocksdb/SstFileManager.kt new file mode 100644 index 0000000..2be57ab --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/SstFileManager.kt @@ -0,0 +1,3 @@ +package maryk.rocksdb + +actual typealias SstFileManager = org.rocksdb.SstFileManager diff --git a/src/jvmMain/kotlin/maryk/rocksdb/Statistics.kt b/src/jvmMain/kotlin/maryk/rocksdb/Statistics.kt index 9d4ca85..a92edf5 100644 --- a/src/jvmMain/kotlin/maryk/rocksdb/Statistics.kt +++ b/src/jvmMain/kotlin/maryk/rocksdb/Statistics.kt @@ -1,3 +1,16 @@ package maryk.rocksdb +import java.util.EnumSet + actual typealias Statistics = org.rocksdb.Statistics + +actual fun createStatistics(enabledHistograms: Set): Statistics { + val javaSet = when { + enabledHistograms.isEmpty() -> EnumSet.noneOf(HistogramType::class.java) + enabledHistograms is EnumSet<*> -> + @Suppress("UNCHECKED_CAST") + enabledHistograms as EnumSet + else -> EnumSet.copyOf(enabledHistograms) + } + return org.rocksdb.Statistics(javaSet) +} diff --git a/src/jvmMain/kotlin/maryk/rocksdb/StatisticsCollector.kt b/src/jvmMain/kotlin/maryk/rocksdb/StatisticsCollector.kt new file mode 100644 index 0000000..dd910a6 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/StatisticsCollector.kt @@ -0,0 +1,82 @@ +package maryk.rocksdb + +import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +actual class StatisticsCollector actual constructor( + private val statsCollectorInputList: List, + statsCollectionIntervalInMilliSeconds: Int +) { + private val intervalMillis = statsCollectionIntervalInMilliSeconds.coerceAtLeast(0) + private val started = AtomicBoolean(false) + private val executor: ScheduledExecutorService = + Executors.newSingleThreadScheduledExecutor { runnable -> + Thread(runnable, "RocksDB-StatisticsCollector").apply { isDaemon = true } + } + private var future: Future<*>? = null + + actual fun start() { + if (!started.compareAndSet(false, true)) { + return + } + future = if (intervalMillis == 0) { + executor.submit { loopUntilInterrupted() } + } else { + executor.scheduleAtFixedRate( + { collectOnce() }, + 0L, + intervalMillis.toLong(), + TimeUnit.MILLISECONDS + ) + } + } + + actual fun shutDown(shutdownTimeout: Int) { + if (!started.compareAndSet(true, false)) { + return + } + future?.cancel(true) + future = null + executor.shutdownNow() + val timeout = shutdownTimeout.toLong().coerceAtLeast(0L) + if (timeout > 0L) { + executor.awaitTermination(timeout, TimeUnit.MILLISECONDS) + } else { + executor.awaitTermination(0, TimeUnit.MILLISECONDS) + } + } + + private fun loopUntilInterrupted() { + while (!Thread.currentThread().isInterrupted) { + collectOnce() + if (Thread.currentThread().isInterrupted) { + break + } + Thread.yield() + } + } + + private fun collectOnce() { + for (input in statsCollectorInputList) { + val statistics = input.statistics + val callback = input.callback + + for (ticker in TickerType.entries) { + if (ticker != TickerType.TICKER_ENUM_MAX) { + val tickerValue = statistics.getTickerCount(ticker) + callback.tickerCallback(ticker, tickerValue) + } + } + + for (histogram in HistogramType.entries) { + if (histogram != HistogramType.HISTOGRAM_ENUM_MAX) { + val histogramData = statistics.getHistogramData(histogram) + callback.histogramCallback(histogram, histogramData) + } + } + } + } +} diff --git a/src/jvmMain/kotlin/maryk/rocksdb/TableProperties.kt b/src/jvmMain/kotlin/maryk/rocksdb/TableProperties.kt new file mode 100644 index 0000000..c1b6975 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/TableProperties.kt @@ -0,0 +1,32 @@ +package maryk.rocksdb + +actual class TableProperties internal constructor( + internal val delegate: org.rocksdb.TableProperties +) { + actual fun dataSize(): Long = delegate.dataSize + actual fun indexSize(): Long = delegate.indexSize + actual fun filterSize(): Long = delegate.filterSize + actual fun rawKeySize(): Long = delegate.rawKeySize + actual fun rawValueSize(): Long = delegate.rawValueSize + actual fun numDataBlocks(): Long = delegate.numDataBlocks + actual fun numEntries(): Long = delegate.numEntries + actual fun numDeletions(): Long = delegate.numDeletions + actual fun numMergeOperands(): Long = delegate.numMergeOperands + actual fun numRangeDeletions(): Long = delegate.numRangeDeletions + actual fun formatVersion(): Long = delegate.formatVersion + actual fun columnFamilyId(): Long = delegate.columnFamilyId + actual fun columnFamilyName(): String? = + delegate.columnFamilyName?.let { String(it) } + actual fun creationTime(): Long = delegate.creationTime + actual fun oldestKeyTime(): Long = delegate.oldestKeyTime + actual fun slowCompressionEstimatedDataSize(): Long = + delegate.slowCompressionEstimatedDataSize + actual fun fastCompressionEstimatedDataSize(): Long = + delegate.fastCompressionEstimatedDataSize + actual fun filterPolicyName(): String? = delegate.filterPolicyName + actual fun comparatorName(): String? = delegate.comparatorName + actual fun mergeOperatorName(): String? = delegate.mergeOperatorName + actual fun prefixExtractorName(): String? = delegate.prefixExtractorName + actual fun propertyCollectorsNames(): String? = delegate.propertyCollectorsNames + actual fun compressionName(): String? = delegate.compressionName +} diff --git a/src/jvmMain/kotlin/maryk/rocksdb/TransactionLogIterator.kt b/src/jvmMain/kotlin/maryk/rocksdb/TransactionLogIterator.kt new file mode 100644 index 0000000..9717d16 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/TransactionLogIterator.kt @@ -0,0 +1,4 @@ +package maryk.rocksdb + +actual typealias TransactionLogIterator = org.rocksdb.TransactionLogIterator +actual typealias TransactionLogBatchResult = org.rocksdb.TransactionLogIterator.BatchResult diff --git a/src/jvmMain/kotlin/maryk/rocksdb/WALRecoveryMode.kt b/src/jvmMain/kotlin/maryk/rocksdb/WALRecoveryMode.kt new file mode 100644 index 0000000..eddd440 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/WALRecoveryMode.kt @@ -0,0 +1,6 @@ +package maryk.rocksdb + +actual typealias WALRecoveryMode = org.rocksdb.WALRecoveryMode + +actual fun walRecoveryModeFromValue(value: Byte): WALRecoveryMode = + WALRecoveryMode.getWALRecoveryMode(value) diff --git a/src/jvmMain/kotlin/maryk/rocksdb/WriteBufferManager.kt b/src/jvmMain/kotlin/maryk/rocksdb/WriteBufferManager.kt new file mode 100644 index 0000000..4c2c335 --- /dev/null +++ b/src/jvmMain/kotlin/maryk/rocksdb/WriteBufferManager.kt @@ -0,0 +1,11 @@ +package maryk.rocksdb + +actual class WriteBufferManager : org.rocksdb.WriteBufferManager { + actual constructor(bufferSize: Long, cache: Cache, allowStall: Boolean) : + super(bufferSize, cache, allowStall) + + actual constructor(bufferSize: Long, cache: Cache) : + super(bufferSize, cache) + + actual override fun allowStall(): Boolean = super.allowStall() +} diff --git a/src/jvmTest/kotlin/maryk/rocksdb/AdvancedOptionsTest.kt b/src/jvmTest/kotlin/maryk/rocksdb/AdvancedOptionsTest.kt new file mode 100644 index 0000000..d29b63f --- /dev/null +++ b/src/jvmTest/kotlin/maryk/rocksdb/AdvancedOptionsTest.kt @@ -0,0 +1,242 @@ +package maryk.rocksdb + +import java.io.File +import kotlin.io.path.createTempDirectory +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class AdvancedOptionsTest { + private lateinit var dbDir: File + + @BeforeTest + fun setUp() { + loadRocksDBLibrary() + dbDir = createTempDirectory(prefix = "rocksdb-advanced").toFile() + } + + @AfterTest + fun tearDown() { + if (::dbDir.isInitialized) { + dbDir.deleteRecursively() + } + } + + @Test + fun walRecoveryModeRoundTrip() { + DBOptions().use { options -> + options.setWalRecoveryMode(WALRecoveryMode.SkipAnyCorruptedRecords) + assertEquals(WALRecoveryMode.SkipAnyCorruptedRecords, options.walRecoveryMode()) + + options.setWalRecoveryMode(WALRecoveryMode.AbsoluteConsistency) + assertEquals(WALRecoveryMode.AbsoluteConsistency, options.walRecoveryMode()) + } + } + + @Test + fun perfContextCollectsMetrics() { + Options().setCreateIfMissing(true).use { options -> + RocksDB.open(options, dbDir.absolutePath).use { db -> + db.setPerfLevel(PerfLevel.ENABLE_TIME_AND_CPU_TIME_EXCEPT_FOR_MUTEX) + + db.getPerfContext().use { context -> + context.reset() + + val key = "perf-key".encodeToByteArray() + val value = "value".encodeToByteArray() + repeat(5) { + db.put(key, value) + assertNotNull(db.get(key)) + } + + assertTrue(context.getUserKeyComparisonCount() >= 0) + assertTrue(context.getReadBytes() >= 0) + assertTrue(context.toString(false).contains("user_key_comparison_count")) + } + + assertEquals( + PerfLevel.ENABLE_TIME_AND_CPU_TIME_EXCEPT_FOR_MUTEX, + db.getPerfLevel() + ) + } + } + } + + @Test + fun liveFileMetadataIsExposed() { + Options().setCreateIfMissing(true).use { options -> + RocksDB.open(options, dbDir.absolutePath).use { db -> + val key = "meta".encodeToByteArray() + val value = ByteArray(1024) { 1 } + db.put(key, value) + + FlushOptions().setWaitForFlush(true).use { flushOptions -> + db.flush(flushOptions) + } + + val metadata = db.getLiveFilesMetaData() + assertTrue(metadata.isNotEmpty()) + val first = metadata.first() + assertTrue(first.level() >= 0) + assertTrue(first.fileName().isNotEmpty()) + assertContentEquals(defaultColumnFamily, first.columnFamilyName()) + } + } + } + + @Test + fun transactionLogIteratorStreamsWal() { + Options().setCreateIfMissing(true).use { options -> + RocksDB.open(options, dbDir.absolutePath).use { db -> + WriteOptions().use { writeOptions -> + writeOptions.setDisableWAL(false) + repeat(3) { index -> + val key = "wal-key-$index".encodeToByteArray() + val value = "wal-value-$index".encodeToByteArray() + db.put(writeOptions, key, value) + } + } + + db.getUpdatesSince(0).use { iterator: TransactionLogIterator -> + iterator.status() + + val sequences = mutableListOf() + while (iterator.isValid()) { + val batch = iterator.getBatch() + sequences += batch.sequenceNumber() + assertTrue(batch.writeBatch().count() > 0) + iterator.next() + } + + assertTrue(sequences.isNotEmpty()) + assertTrue(sequences.zipWithNext().all { (current, next) -> current <= next }) + } + } + } + } + + @Test + fun writeBufferManagerTracksAllowStall() { + LRUCache(4L * 1024 * 1024).use { lruCache -> + val sharedCache: Cache = lruCache + + WriteBufferManager(2L * 1024 * 1024, sharedCache).use { manager -> + assertFalse(manager.allowStall()) + } + + WriteBufferManager(2L * 1024 * 1024, sharedCache, allowStall = true).use { manager -> + assertTrue(manager.allowStall()) + } + } + } + + @Test + fun importColumnFamilyOptionsTracksMoveFiles() { + ImportColumnFamilyOptions().use { options -> + assertFalse(options.moveFiles()) + + options.setMoveFiles(true) + assertTrue(options.moveFiles()) + } + } + + @Test + fun sstFileManagerIntegratesWithOptions() { + val env = getDefaultEnv() + SstFileManager(env).use { manager -> + manager.setMaxAllowedSpaceUsage(1024L * 1024) + manager.setCompactionBufferSize(512L) + manager.setDeleteRateBytesPerSecond(2048) + manager.setMaxTrashDBRatio(0.5) + + assertFalse(manager.isMaxAllowedSpaceReached()) + assertFalse(manager.isMaxAllowedSpaceReachedIncludingCompactions()) + assertEquals(0.5, manager.getMaxTrashDBRatio(), 1e-6) + } + + val manager = SstFileManager(env) + try { + Options().setCreateIfMissing(true).use { options -> + options.setSstFileManager(manager) + + RocksDB.open(options, File(dbDir, "sst-manager").apply { mkdirs() }.absolutePath).use { db -> + db.put("manager".encodeToByteArray(), "value".encodeToByteArray()) + assertTrue(manager.getTotalSize() >= 0) + } + } + } finally { + manager.close() + } + } + + @Test + fun optionsUtilLoadsLatestAndFileOptions() { + val dbPath = File(dbDir, "options-util").apply { mkdirs() }.absolutePath + + Options().setCreateIfMissing(true).use { options -> + RocksDB.open(options, dbPath).use { db -> + db.put("util-key".encodeToByteArray(), "value".encodeToByteArray()) + } + } + + ConfigOptions().use { config -> + config + .setDelimiter(";") + .setIgnoreUnknownOptions(true) + .setInputStringsEscaped(false) + .setEnv(getDefaultEnv()) + .setSanityLevel(SanityLevel.LOOSELY_COMPATIBLE) + + DBOptions().use { dbOptions -> + val descriptors = mutableListOf() + OptionsUtil.loadLatestOptions(config, dbPath, dbOptions, descriptors) + + assertTrue(descriptors.isNotEmpty()) + val first = descriptors.first() + assertContentEquals(defaultColumnFamily, first.getName()) + assertTrue(first.getOptions().numLevels() >= 1) + + val latestOptionsFileName = OptionsUtil.getLatestOptionsFileName(dbPath, getDefaultEnv()) + assertTrue(latestOptionsFileName.contains("OPTIONS")) + val latestOptionsFile = File(latestOptionsFileName).let { file -> + if (file.isAbsolute) file else File(dbPath, latestOptionsFileName) + } + assertTrue(latestOptionsFile.exists()) + + val fileDescriptors = mutableListOf() + OptionsUtil.loadOptionsFromFile(config, latestOptionsFile.absolutePath, dbOptions, fileDescriptors) + + assertFalse(fileDescriptors.isEmpty()) + assertContentEquals(defaultColumnFamily, fileDescriptors.first().getName()) + } + } + } + + @Test + fun backgroundWorkControlsExposeNativeOperations() { + Options().setCreateIfMissing(true).use { options -> + RocksDB.open(options, File(dbDir, "background-work").apply { mkdirs() }.absolutePath).use { db -> + db.put("background".encodeToByteArray(), "value".encodeToByteArray()) + + // Ensure the new administrative helpers round-trip through the bindings. + db.cancelAllBackgroundWork(false) + + val catchUp = runCatching { db.tryCatchUpWithPrimary() } + catchUp.exceptionOrNull()?.let { throwable -> + if (throwable is RocksDBException) { + throwable.status?.let { status -> + assertEquals(StatusCode.NotSupported, status.code) + } + } else { + throw throwable + } + } + } + } + } +} diff --git a/src/jvmTest/kotlin/maryk/rocksdb/EventListenerTest.kt b/src/jvmTest/kotlin/maryk/rocksdb/EventListenerTest.kt new file mode 100644 index 0000000..dc15e81 --- /dev/null +++ b/src/jvmTest/kotlin/maryk/rocksdb/EventListenerTest.kt @@ -0,0 +1,127 @@ +package maryk.rocksdb + +import java.io.File +import kotlin.io.path.createTempDirectory +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class EventListenerTest { + private lateinit var dbDir: File + + @BeforeTest + fun setUp() { + loadRocksDBLibrary() + dbDir = createTempDirectory(prefix = "rocksdb-events").toFile() + } + + @AfterTest + fun tearDown() { + if (::dbDir.isInitialized) { + dbDir.deleteRecursively() + } + } + + @Test + fun flushAndCompactionCallbacksFire() { + val flushEvents = mutableListOf() + val compactionEvents = mutableListOf() + + val listener = object : EventListener() { + override fun onFlushCompletedEvent(db: RocksDB, flushJobInfo: FlushJobInfo) { + flushEvents += flushJobInfo + } + + override fun onCompactionCompletedEvent(db: RocksDB, compactionJobInfo: CompactionJobInfo) { + compactionEvents += compactionJobInfo + } + } + + Options().setCreateIfMissing(true).use { options -> + options.addEventListener(listener) + + RocksDB.open(options, dbDir.absolutePath).use { db -> + val keyPrefix = "listener-key".encodeToByteArray() + val value = ByteArray(1024) { 42 } + repeat(4) { index -> + val key = keyPrefix + index.toByte() + db.put(key, value) + } + + FlushOptions().setWaitForFlush(true).use { flushOptions -> + db.flush(flushOptions) + } + + db.compactRange() + } + } + + // Listener is owned by options once registered, closing it becomes a no-op but keeps API symmetry. + listener.close() + + assertTrue(flushEvents.isNotEmpty(), "Expected at least one flush event") + assertTrue(flushEvents.all { it.filePath().isNotEmpty() }) + + val flushInfo = flushEvents.first() + assertEquals(0L, flushInfo.columnFamilyId(), "default CF id should be zero") + assertEquals("default", flushInfo.columnFamilyName()) + assertTrue(flushInfo.threadId() > 0) + assertTrue(flushInfo.jobId() >= 0) + assertEquals(FlushReason.MANUAL_FLUSH, flushInfo.flushReason()) + + val tableProps = flushInfo.tableProperties() + assertNotNull(tableProps.compressionName()) + assertTrue(tableProps.numEntries() > 0) + assertEquals("default", tableProps.columnFamilyName()) + + assertTrue(compactionEvents.isNotEmpty(), "Expected at least one compaction event") + assertTrue(compactionEvents.any { it.totalOutputBytes() >= 0 }) + + val stats = compactionEvents.first().compactionStats() + assertTrue(stats.numOutputFiles() >= 0, "Expected non-negative output file count in compaction stats") + } + + @Test + fun externalFileIngestionCallbackProvidesMetadata() { + val ingestions = mutableListOf() + + val listener = object : EventListener() { + override fun onExternalFileIngested(db: RocksDB, ingestionInfo: ExternalFileIngestionInfo) { + ingestions += ingestionInfo + } + } + + Options().setCreateIfMissing(true).use { options -> + options.addEventListener(listener) + + RocksDB.open(options, dbDir.absolutePath).use { db -> + val sstFile = File(dbDir, "ingest.sst") + + EnvOptions().use { envOptions -> + SstFileWriter(envOptions, options).use { writer -> + writer.open(sstFile.absolutePath) + writer.put("ingest-key".encodeToByteArray(), "ingest-value".encodeToByteArray()) + writer.finish() + } + } + + IngestExternalFileOptions().use { ingestOptions -> + ingestOptions.setMoveFiles(true) + db.ingestExternalFile(listOf(sstFile.absolutePath), ingestOptions) + } + } + } + + listener.close() + + val info = ingestions.single() + assertEquals("default", info.columnFamilyName()) + assertTrue(info.externalFilePath().isNotEmpty()) + assertTrue(info.internalFilePath().isNotEmpty()) + assertTrue(info.globalSequenceNumber() >= 0) + assertNotNull(info.tableProperties()) + } +} diff --git a/src/jvmTest/kotlin/maryk/rocksdb/FlushOptionsTest.kt b/src/jvmTest/kotlin/maryk/rocksdb/FlushOptionsTest.kt new file mode 100644 index 0000000..c659bca --- /dev/null +++ b/src/jvmTest/kotlin/maryk/rocksdb/FlushOptionsTest.kt @@ -0,0 +1,84 @@ +package maryk.rocksdb + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class FlushOptionsTest { + init { + RocksDB.loadLibrary() + } + + @Test + fun flushOptionsRoundTrip() { + FlushOptions().use { options -> + options.setWaitForFlush(true) + options.setAllowWriteStall(false) + + assertTrue(options.waitForFlush()) + assertFalse(options.allowWriteStall()) + } + } + + @Test + fun bloomFilterConstructorsWork() { + BloomFilter().use { assertNotNull(it) } + BloomFilter(8.5).use { assertNotNull(it) } + BloomFilter(10.0, true).use { assertNotNull(it) } + } + + @Test + fun compactionOptionsExposeToggles() { + CompactionOptions().use { options -> + options.setCompression(CompressionType.ZSTD_COMPRESSION) + options.setOutputFileSizeLimit(16L * 1024 * 1024) + options.setMaxSubcompactions(3) + + assertEquals(CompressionType.ZSTD_COMPRESSION, options.compression()) + assertEquals(16L * 1024 * 1024, options.outputFileSizeLimit()) + assertEquals(3, options.maxSubcompactions()) + } + + CompactionOptionsFIFO().use { fifo -> + fifo.setMaxTableFilesSize(1_024) + fifo.setAllowCompaction(true) + assertEquals(1_024, fifo.maxTableFilesSize()) + assertTrue(fifo.allowCompaction()) + } + + CompactionOptionsUniversal().use { universal -> + universal.setSizeRatio(20) + universal.setMinMergeWidth(2) + universal.setMaxMergeWidth(5) + assertEquals(20, universal.sizeRatio()) + assertEquals(2, universal.minMergeWidth()) + assertEquals(5, universal.maxMergeWidth()) + } + } + + @Test + fun compressionOptionsRoundTrip() { + CompressionOptions().use { options -> + options.setWindowBits(32) + options.setLevel(4) + options.setStrategy(CompressionType.LZ4_COMPRESSION.value.toInt()) + options.setMaxDictBytes(2048) + + assertEquals(32, options.windowBits()) + assertEquals(4, options.level()) + assertEquals(CompressionType.LZ4_COMPRESSION.value.toInt(), options.strategy()) + assertEquals(2048, options.maxDictBytes()) + } + } + + @Test + fun rateLimiterAdjustsTarget() { + RateLimiter(10_000).use { limiter -> + assertEquals(10_000, limiter.getBytesPerSecond()) + limiter.setBytesPerSecond(50_000) + assertEquals(50_000, limiter.getBytesPerSecond()) + } + } +} diff --git a/src/jvmTest/kotlin/maryk/rocksdb/util/Sleep.kt b/src/jvmTest/kotlin/maryk/rocksdb/util/Sleep.kt new file mode 100644 index 0000000..15be982 --- /dev/null +++ b/src/jvmTest/kotlin/maryk/rocksdb/util/Sleep.kt @@ -0,0 +1,9 @@ +package maryk.rocksdb.util + +actual fun sleepMillis(millis: Long) { + if (millis <= 0L) { + Thread.yield() + } else { + Thread.sleep(millis) + } +} diff --git a/src/jvmTest/kotlin/maryk/rocksdb/util/ThreadSafeCounter.kt b/src/jvmTest/kotlin/maryk/rocksdb/util/ThreadSafeCounter.kt new file mode 100644 index 0000000..1c7ee78 --- /dev/null +++ b/src/jvmTest/kotlin/maryk/rocksdb/util/ThreadSafeCounter.kt @@ -0,0 +1,13 @@ +package maryk.rocksdb.util + +import java.util.concurrent.atomic.AtomicInteger + +actual class ThreadSafeCounter actual constructor() { + private val counter = AtomicInteger(0) + + actual fun increment() { + counter.incrementAndGet() + } + + actual fun value(): Int = counter.get() +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/BackupEngineOptions.kt b/src/nativeMain/kotlin/maryk/rocksdb/BackupEngineOptions.kt index 74d1420..0df211e 100644 --- a/src/nativeMain/kotlin/maryk/rocksdb/BackupEngineOptions.kt +++ b/src/nativeMain/kotlin/maryk/rocksdb/BackupEngineOptions.kt @@ -1,26 +1,168 @@ package maryk.rocksdb +import cnames.structs.rocksdb_backup_engine_options_t +import kotlinx.cinterop.CPointer +import maryk.toUByte import platform.posix.access import platform.posix.W_OK import rocksdb.rocksdb_backup_engine_options_create import rocksdb.rocksdb_backup_engine_options_destroy +import rocksdb.rocksdb_backup_engine_options_get_backup_log_files +import rocksdb.rocksdb_backup_engine_options_get_backup_rate_limit +import rocksdb.rocksdb_backup_engine_options_get_callback_trigger_interval_size +import rocksdb.rocksdb_backup_engine_options_get_destroy_old_data +import rocksdb.rocksdb_backup_engine_options_get_max_background_operations +import rocksdb.rocksdb_backup_engine_options_get_restore_rate_limit +import rocksdb.rocksdb_backup_engine_options_get_share_files_with_checksum_naming +import rocksdb.rocksdb_backup_engine_options_get_share_table_files +import rocksdb.rocksdb_backup_engine_options_get_sync +import rocksdb.rocksdb_backup_engine_options_set_backup_log_files +import rocksdb.rocksdb_backup_engine_options_set_backup_rate_limit +import rocksdb.rocksdb_backup_engine_options_set_backup_rate_limiter +import rocksdb.rocksdb_backup_engine_options_set_callback_trigger_interval_size +import rocksdb.rocksdb_backup_engine_options_set_destroy_old_data +import rocksdb.rocksdb_backup_engine_options_set_env +import rocksdb.rocksdb_backup_engine_options_set_max_background_operations +import rocksdb.rocksdb_backup_engine_options_set_restore_rate_limit +import rocksdb.rocksdb_backup_engine_options_set_restore_rate_limiter +import rocksdb.rocksdb_backup_engine_options_set_share_files_with_checksum_naming +import rocksdb.rocksdb_backup_engine_options_set_share_table_files +import rocksdb.rocksdb_backup_engine_options_set_sync actual class BackupEngineOptions -actual constructor(private val path: String) +actual constructor(path: String) : RocksObject() { + private val backupDirectory: String + internal val native: CPointer + private var backupEnvRef: Env? = null + private var backupRateLimiterRef: RateLimiter? = null + private var restoreRateLimiterRef: RateLimiter? = null init { - val pathToCheck = if (!path.endsWith('/')) "$path/" else path + val normalizedPath = if (path.endsWith('/')) path.dropLast(1) else path + backupDirectory = normalizedPath + val pathToCheck = "$normalizedPath/" // Use POSIX access function to check write permission - require(access(pathToCheck, W_OK) == 0) { "Path $path is not writable" } + require(access(pathToCheck, W_OK) == 0) { "Path $normalizedPath is not writable" } + native = requireNotNull(rocksdb_backup_engine_options_create(normalizedPath)) { + "Unable to allocate backup engine options" + } } - val native = rocksdb_backup_engine_options_create(path) - actual fun backupDir(): String { - return path + return backupDirectory + } + + actual fun setBackupEnv(env: Env): BackupEngineOptions { + rocksdb_backup_engine_options_set_env(native, env.native) + backupEnvRef = env + return this + } + + actual fun backupEnv(): Env? = backupEnvRef + + actual fun setShareTableFiles(shareTableFiles: Boolean): BackupEngineOptions { + rocksdb_backup_engine_options_set_share_table_files(native, shareTableFiles.toUByte()) + return this + } + + actual fun shareTableFiles(): Boolean = + rocksdb_backup_engine_options_get_share_table_files(native) != 0.toUByte() + + actual fun setSync(sync: Boolean): BackupEngineOptions { + rocksdb_backup_engine_options_set_sync(native, sync.toUByte()) + return this + } + + actual fun sync(): Boolean = + rocksdb_backup_engine_options_get_sync(native) != 0.toUByte() + + actual fun setDestroyOldData(destroyOldData: Boolean): BackupEngineOptions { + rocksdb_backup_engine_options_set_destroy_old_data(native, destroyOldData.toUByte()) + return this + } + + actual fun destroyOldData(): Boolean = + rocksdb_backup_engine_options_get_destroy_old_data(native) != 0.toUByte() + + actual fun setBackupLogFiles(backupLogFiles: Boolean): BackupEngineOptions { + rocksdb_backup_engine_options_set_backup_log_files(native, backupLogFiles.toUByte()) + return this + } + + actual fun backupLogFiles(): Boolean = + rocksdb_backup_engine_options_get_backup_log_files(native) != 0.toUByte() + + actual fun setBackupRateLimit(backupRateLimit: Long): BackupEngineOptions { + val sanitized = if (backupRateLimit <= 0) 0 else backupRateLimit + rocksdb_backup_engine_options_set_backup_rate_limit(native, sanitized.toULong()) + return this } + actual fun backupRateLimit(): Long = + rocksdb_backup_engine_options_get_backup_rate_limit(native).toLong() + + actual fun setBackupRateLimiter(rateLimiter: RateLimiter): BackupEngineOptions { + rocksdb_backup_engine_options_set_backup_rate_limiter(native, rateLimiter.native) + backupRateLimiterRef = rateLimiter + return this + } + + actual fun backupRateLimiter(): RateLimiter? = backupRateLimiterRef + + actual fun setRestoreRateLimit(restoreRateLimit: Long): BackupEngineOptions { + val sanitized = if (restoreRateLimit <= 0) 0 else restoreRateLimit + rocksdb_backup_engine_options_set_restore_rate_limit(native, sanitized.toULong()) + return this + } + + actual fun restoreRateLimit(): Long = + rocksdb_backup_engine_options_get_restore_rate_limit(native).toLong() + + actual fun setRestoreRateLimiter(rateLimiter: RateLimiter): BackupEngineOptions { + rocksdb_backup_engine_options_set_restore_rate_limiter(native, rateLimiter.native) + restoreRateLimiterRef = rateLimiter + return this + } + + actual fun restoreRateLimiter(): RateLimiter? = restoreRateLimiterRef + + actual fun setShareFilesWithChecksum( + shareFilesWithChecksum: Boolean, + ): BackupEngineOptions { + rocksdb_backup_engine_options_set_share_files_with_checksum_naming( + native, + if (shareFilesWithChecksum) 1 else 0, + ) + return this + } + + actual fun shareFilesWithChecksum(): Boolean = + rocksdb_backup_engine_options_get_share_files_with_checksum_naming(native) != 0 + + actual fun setMaxBackgroundOperations( + maxBackgroundOperations: Int, + ): BackupEngineOptions { + rocksdb_backup_engine_options_set_max_background_operations(native, maxBackgroundOperations) + return this + } + + actual fun maxBackgroundOperations(): Int = + rocksdb_backup_engine_options_get_max_background_operations(native) + + actual fun setCallbackTriggerIntervalSize( + callbackTriggerIntervalSize: Long, + ): BackupEngineOptions { + rocksdb_backup_engine_options_set_callback_trigger_interval_size( + native, + callbackTriggerIntervalSize.toULong(), + ) + return this + } + + actual fun callbackTriggerIntervalSize(): Long = + rocksdb_backup_engine_options_get_callback_trigger_interval_size(native).toLong() + override fun close() { if (isOwningHandle()) { rocksdb_backup_engine_options_destroy(native) diff --git a/src/nativeMain/kotlin/maryk/rocksdb/BloomFilter.kt b/src/nativeMain/kotlin/maryk/rocksdb/BloomFilter.kt new file mode 100644 index 0000000..f1eae21 --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/BloomFilter.kt @@ -0,0 +1,32 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package maryk.rocksdb + +import cnames.structs.rocksdb_filterpolicy_t +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import rocksdb.rocksdb_filterpolicy_create_bloom +import rocksdb.rocksdb_filterpolicy_create_bloom_full +import rocksdb.rocksdb_filterpolicy_destroy + +actual class BloomFilter internal constructor( + internal val native: CPointer +) : FilterPolicy() { + actual constructor() : this(rocksdb_filterpolicy_create_bloom(10.toDouble())!!) + + actual constructor(bitsPerKey: Double) : this( + rocksdb_filterpolicy_create_bloom(bitsPerKey)!! + ) + + actual constructor(bitsPerKey: Double, useBlockBasedBuilder: Boolean) : this( + // The value useBlockBasedBuilder is ignored in java implementation so ignore it here too. + rocksdb_filterpolicy_create_bloom_full(bitsPerKey)!! + ) + + override fun close() { + if (isOwningHandle()) { + rocksdb_filterpolicy_destroy(native) + super.close() + } + } +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/ColumnFamilyOptions.kt b/src/nativeMain/kotlin/maryk/rocksdb/ColumnFamilyOptions.kt index df8b3a7..ba3b745 100644 --- a/src/nativeMain/kotlin/maryk/rocksdb/ColumnFamilyOptions.kt +++ b/src/nativeMain/kotlin/maryk/rocksdb/ColumnFamilyOptions.kt @@ -62,6 +62,16 @@ actual class ColumnFamilyOptions private constructor( actual constructor() : this(rocksdb_options_create()!!) + companion object { + internal fun wrap(native: CPointer, owning: Boolean): ColumnFamilyOptions { + val options = ColumnFamilyOptions(native) + if (!owning) { + options.disownHandle() + } + return options + } + } + override fun close() { if (isOwningHandle()) { rocksdb_options_destroy(native) diff --git a/src/nativeMain/kotlin/maryk/rocksdb/CompactionJobInfo.kt b/src/nativeMain/kotlin/maryk/rocksdb/CompactionJobInfo.kt new file mode 100644 index 0000000..5801925 --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/CompactionJobInfo.kt @@ -0,0 +1,124 @@ +package maryk.rocksdb + +import cnames.structs.rocksdb_compactionjobinfo_t +import kotlinx.cinterop.ByteVar +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.value +import maryk.toByteArray +import platform.posix.size_tVar +import rocksdb.rocksdb_compactionjobinfo_base_input_level +import rocksdb.rocksdb_compactionjobinfo_compaction_reason +import rocksdb.rocksdb_compactionjobinfo_elapsed_micros +import rocksdb.rocksdb_compactionjobinfo_input_file_at +import rocksdb.rocksdb_compactionjobinfo_input_files_count +import rocksdb.rocksdb_compactionjobinfo_input_records +import rocksdb.rocksdb_compactionjobinfo_num_corrupt_keys +import rocksdb.rocksdb_compactionjobinfo_num_input_files +import rocksdb.rocksdb_compactionjobinfo_num_input_files_at_output_level +import rocksdb.rocksdb_compactionjobinfo_output_file_at +import rocksdb.rocksdb_compactionjobinfo_output_files_count +import rocksdb.rocksdb_compactionjobinfo_output_level +import rocksdb.rocksdb_compactionjobinfo_output_records +import rocksdb.rocksdb_compactionjobinfo_total_input_bytes +import rocksdb.rocksdb_compactionjobinfo_total_output_bytes +import rocksdb.rocksdb_compactionjobinfo_cf_name + +actual class CompactionJobInfo internal constructor( + private val columnFamilyNameValue: ByteArray, + private val baseInputLevelValue: Int, + private val outputLevelValue: Int, + private val inputFilesValue: List, + private val outputFilesValue: List, + elapsedMicrosValue: Long, + numCorruptKeysValue: Long, + inputRecordsValue: Long, + outputRecordsValue: Long, + totalInputBytesValue: Long, + totalOutputBytesValue: Long, + private val compactionReasonValue: CompactionReason, + numInputFilesValue: Long, + numInputFilesAtOutputLevelValue: Long, +) { + private val statsValue = CompactionJobStats( + elapsedMicrosValue = elapsedMicrosValue, + numInputRecordsValue = inputRecordsValue, + numOutputRecordsValue = outputRecordsValue, + totalInputBytesValue = totalInputBytesValue, + totalOutputBytesValue = totalOutputBytesValue, + numInputFilesValue = numInputFilesValue, + numInputFilesAtOutputLevelValue = numInputFilesAtOutputLevelValue, + numOutputFilesValue = outputFilesValue.size.toLong(), + numCorruptKeysValue = numCorruptKeysValue, + ) + + internal constructor(native: CPointer) : this( + columnFamilyNameValue = memScoped { + val length = alloc() + rocksdb_compactionjobinfo_cf_name(native, length.ptr)!! + .toByteArray(length.value) + }, + baseInputLevelValue = rocksdb_compactionjobinfo_base_input_level(native), + outputLevelValue = rocksdb_compactionjobinfo_output_level(native), + inputFilesValue = collectPaths(native, ::rocksdb_compactionjobinfo_input_files_count, ::rocksdb_compactionjobinfo_input_file_at), + outputFilesValue = collectPaths(native, ::rocksdb_compactionjobinfo_output_files_count, ::rocksdb_compactionjobinfo_output_file_at), + elapsedMicrosValue = rocksdb_compactionjobinfo_elapsed_micros(native).toLong(), + numCorruptKeysValue = rocksdb_compactionjobinfo_num_corrupt_keys(native).toLong(), + inputRecordsValue = rocksdb_compactionjobinfo_input_records(native).toLong(), + outputRecordsValue = rocksdb_compactionjobinfo_output_records(native).toLong(), + totalInputBytesValue = rocksdb_compactionjobinfo_total_input_bytes(native).toLong(), + totalOutputBytesValue = rocksdb_compactionjobinfo_total_output_bytes(native).toLong(), + compactionReasonValue = compactionReasonFromValue(rocksdb_compactionjobinfo_compaction_reason(native)), + numInputFilesValue = rocksdb_compactionjobinfo_num_input_files(native).toLong(), + numInputFilesAtOutputLevelValue = + rocksdb_compactionjobinfo_num_input_files_at_output_level(native).toLong(), + ) + + actual fun columnFamilyName(): ByteArray = columnFamilyNameValue + + actual fun baseInputLevel(): Int = baseInputLevelValue + + actual fun outputLevel(): Int = outputLevelValue + + actual fun inputFiles(): List = inputFilesValue + + actual fun outputFiles(): List = outputFilesValue + + actual fun elapsedMicros(): Long = statsValue.elapsedMicros() + + actual fun numCorruptKeys(): Long = statsValue.numCorruptKeys() + + actual fun inputRecords(): Long = statsValue.numInputRecords() + + actual fun outputRecords(): Long = statsValue.numOutputRecords() + + actual fun totalInputBytes(): Long = statsValue.totalInputBytes() + + actual fun totalOutputBytes(): Long = statsValue.totalOutputBytes() + + actual fun compactionReason(): CompactionReason = compactionReasonValue + + actual fun numInputFiles(): Long = statsValue.numInputFiles() + + actual fun numInputFilesAtOutputLevel(): Long = statsValue.numInputFilesAtOutputLevel() + + actual fun compactionStats(): CompactionJobStats = statsValue +} + +private fun collectPaths( + native: CPointer, + count: (CPointer) -> ULong, + fetch: (CPointer, ULong, CPointer) -> CPointer? +): List = buildList { + val total = count(native).toInt() + if (total == 0) return@buildList + memScoped { + val length = alloc() + repeat(total) { index -> + val ptr = fetch(native, index.toULong(), length.ptr) ?: return@repeat + add(ptr.toByteArray(length.value).decodeToString()) + } + } +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/CompactionOptions.kt b/src/nativeMain/kotlin/maryk/rocksdb/CompactionOptions.kt new file mode 100644 index 0000000..9a83152 --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/CompactionOptions.kt @@ -0,0 +1,165 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package maryk.rocksdb + +import cnames.structs.rocksdb_compactionoptions_fifo_t +import cnames.structs.rocksdb_compactionoptions_t +import cnames.structs.rocksdb_compactionoptions_universal_t +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import maryk.toBoolean +import maryk.toUByte +import rocksdb.rocksdb_compactionoptions_create +import rocksdb.rocksdb_compactionoptions_destroy +import rocksdb.rocksdb_compactionoptions_fifo_allow_compaction +import rocksdb.rocksdb_compactionoptions_fifo_create +import rocksdb.rocksdb_compactionoptions_fifo_destroy +import rocksdb.rocksdb_compactionoptions_fifo_max_table_files_size +import rocksdb.rocksdb_compactionoptions_fifo_set_allow_compaction +import rocksdb.rocksdb_compactionoptions_fifo_set_max_table_files_size +import rocksdb.rocksdb_compactionoptions_get_compression +import rocksdb.rocksdb_compactionoptions_get_max_subcompactions +import rocksdb.rocksdb_compactionoptions_get_output_file_size_limit +import rocksdb.rocksdb_compactionoptions_set_compression +import rocksdb.rocksdb_compactionoptions_set_max_subcompactions +import rocksdb.rocksdb_compactionoptions_set_output_file_size_limit +import rocksdb.rocksdb_compactionoptions_universal_create +import rocksdb.rocksdb_compactionoptions_universal_destroy +import rocksdb.rocksdb_compactionoptions_universal_max_merge_width +import rocksdb.rocksdb_compactionoptions_universal_max_size_amplification_percent +import rocksdb.rocksdb_compactionoptions_universal_min_merge_width +import rocksdb.rocksdb_compactionoptions_universal_set_compression_size_percent +import rocksdb.rocksdb_compactionoptions_universal_set_max_merge_width +import rocksdb.rocksdb_compactionoptions_universal_set_max_size_amplification_percent +import rocksdb.rocksdb_compactionoptions_universal_set_min_merge_width +import rocksdb.rocksdb_compactionoptions_universal_set_size_ratio +import rocksdb.rocksdb_compactionoptions_universal_set_stop_style +import rocksdb.rocksdb_compactionoptions_universal_size_ratio +import rocksdb.rocksdb_compactionoptions_universal_stop_style +import rocksdb.rocksdb_compactionoptions_universal_compression_size_percent + +actual class CompactionOptions internal constructor( + internal val native: CPointer +) : RocksObject() { + actual constructor() : this(rocksdb_compactionoptions_create()!!) + + override fun close() { + if (isOwningHandle()) { + rocksdb_compactionoptions_destroy(native) + super.close() + } + } + + actual fun compression(): CompressionType = + getCompressionType(rocksdb_compactionoptions_get_compression(native).toByte()) + + actual fun setCompression(compression: CompressionType): CompactionOptions { + rocksdb_compactionoptions_set_compression(native, compression.value.toInt()) + return this + } + + actual fun outputFileSizeLimit(): Long = + rocksdb_compactionoptions_get_output_file_size_limit(native).toLong() + + actual fun setOutputFileSizeLimit(limit: Long): CompactionOptions { + rocksdb_compactionoptions_set_output_file_size_limit(native, limit.toULong()) + return this + } + + actual fun maxSubcompactions(): Int = + rocksdb_compactionoptions_get_max_subcompactions(native).toInt() + + actual fun setMaxSubcompactions(count: Int): CompactionOptions { + rocksdb_compactionoptions_set_max_subcompactions(native, count.toUInt()) + return this + } +} + +actual class CompactionOptionsFIFO internal constructor( + internal val native: CPointer +) : RocksObject() { + actual constructor() : this(rocksdb_compactionoptions_fifo_create()!!) + + override fun close() { + if (isOwningHandle()) { + rocksdb_compactionoptions_fifo_destroy(native) + super.close() + } + } + + actual fun setMaxTableFilesSize(size: Long): CompactionOptionsFIFO { + rocksdb_compactionoptions_fifo_set_max_table_files_size(native, size.toULong()) + return this + } + + actual fun maxTableFilesSize(): Long = + rocksdb_compactionoptions_fifo_max_table_files_size(native).toLong() + + actual fun setAllowCompaction(allow: Boolean): CompactionOptionsFIFO { + rocksdb_compactionoptions_fifo_set_allow_compaction(native, allow.toUByte()) + return this + } + + actual fun allowCompaction(): Boolean = + rocksdb_compactionoptions_fifo_allow_compaction(native).toBoolean() +} + +actual class CompactionOptionsUniversal internal constructor( + internal val native: CPointer +) : RocksObject() { + actual constructor() : this(rocksdb_compactionoptions_universal_create()!!) + + override fun close() { + if (isOwningHandle()) { + rocksdb_compactionoptions_universal_destroy(native) + super.close() + } + } + + actual fun setSizeRatio(sizeRatio: Int): CompactionOptionsUniversal { + rocksdb_compactionoptions_universal_set_size_ratio(native, sizeRatio) + return this + } + + actual fun sizeRatio(): Int = rocksdb_compactionoptions_universal_size_ratio(native) + + actual fun setMinMergeWidth(width: Int): CompactionOptionsUniversal { + rocksdb_compactionoptions_universal_set_min_merge_width(native, width) + return this + } + + actual fun minMergeWidth(): Int = + rocksdb_compactionoptions_universal_min_merge_width(native) + + actual fun setMaxMergeWidth(width: Int): CompactionOptionsUniversal { + rocksdb_compactionoptions_universal_set_max_merge_width(native, width) + return this + } + + actual fun maxMergeWidth(): Int = + rocksdb_compactionoptions_universal_max_merge_width(native) + + actual fun setMaxSizeAmplificationPercent(percent: Int): CompactionOptionsUniversal { + rocksdb_compactionoptions_universal_set_max_size_amplification_percent(native, percent) + return this + } + + actual fun maxSizeAmplificationPercent(): Int = + rocksdb_compactionoptions_universal_max_size_amplification_percent(native) + + actual fun setCompressionSizePercent(percent: Int): CompactionOptionsUniversal { + rocksdb_compactionoptions_universal_set_compression_size_percent(native, percent) + return this + } + + actual fun compressionSizePercent(): Int = + rocksdb_compactionoptions_universal_compression_size_percent(native) + + actual fun setStopStyle(stopStyle: CompactionStopStyle): CompactionOptionsUniversal { + rocksdb_compactionoptions_universal_set_stop_style(native, stopStyle.value.toInt()) + return this + } + + actual fun stopStyle(): CompactionStopStyle = + getCompactionStopStyle(rocksdb_compactionoptions_universal_stop_style(native).toByte()) +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/CompactionReason.kt b/src/nativeMain/kotlin/maryk/rocksdb/CompactionReason.kt index 7e67f82..fadd677 100644 --- a/src/nativeMain/kotlin/maryk/rocksdb/CompactionReason.kt +++ b/src/nativeMain/kotlin/maryk/rocksdb/CompactionReason.kt @@ -19,3 +19,7 @@ actual enum class CompactionReason( kFlush(0x0C), kExternalSstIngestion(0x0D); } + +internal fun compactionReasonFromValue(value: UInt): CompactionReason = + CompactionReason.entries.firstOrNull { it.value.toUByte().toUInt() == value } + ?: CompactionReason.kUnknown diff --git a/src/nativeMain/kotlin/maryk/rocksdb/CompressionOptions.kt b/src/nativeMain/kotlin/maryk/rocksdb/CompressionOptions.kt new file mode 100644 index 0000000..b631f77 --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/CompressionOptions.kt @@ -0,0 +1,59 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package maryk.rocksdb + +import cnames.structs.rocksdb_compression_options_t +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import rocksdb.rocksdb_compression_options_create +import rocksdb.rocksdb_compression_options_destroy +import rocksdb.rocksdb_compression_options_get_level +import rocksdb.rocksdb_compression_options_get_max_dict_bytes +import rocksdb.rocksdb_compression_options_get_strategy +import rocksdb.rocksdb_compression_options_get_window_bits +import rocksdb.rocksdb_compression_options_set_level +import rocksdb.rocksdb_compression_options_set_max_dict_bytes +import rocksdb.rocksdb_compression_options_set_strategy +import rocksdb.rocksdb_compression_options_set_window_bits + +actual class CompressionOptions internal constructor( + internal val native: CPointer +) : RocksObject() { + actual constructor() : this(rocksdb_compression_options_create()!!) + + override fun close() { + if (isOwningHandle()) { + rocksdb_compression_options_destroy(native) + super.close() + } + } + + actual fun setWindowBits(windowBits: Int): CompressionOptions { + rocksdb_compression_options_set_window_bits(native, windowBits) + return this + } + + actual fun windowBits(): Int = rocksdb_compression_options_get_window_bits(native) + + actual fun setLevel(level: Int): CompressionOptions { + rocksdb_compression_options_set_level(native, level) + return this + } + + actual fun level(): Int = rocksdb_compression_options_get_level(native) + + actual fun setStrategy(strategy: Int): CompressionOptions { + rocksdb_compression_options_set_strategy(native, strategy) + return this + } + + actual fun strategy(): Int = rocksdb_compression_options_get_strategy(native) + + actual fun setMaxDictBytes(bytes: Int): CompressionOptions { + rocksdb_compression_options_set_max_dict_bytes(native, bytes.toUInt()) + return this + } + + actual fun maxDictBytes(): Int = + rocksdb_compression_options_get_max_dict_bytes(native).toInt() +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/CompressionType.kt b/src/nativeMain/kotlin/maryk/rocksdb/CompressionType.kt index a85349b..825635f 100644 --- a/src/nativeMain/kotlin/maryk/rocksdb/CompressionType.kt +++ b/src/nativeMain/kotlin/maryk/rocksdb/CompressionType.kt @@ -14,7 +14,7 @@ actual enum class CompressionType( LZ4HC_COMPRESSION(0x5, "lz4hc"), XPRESS_COMPRESSION(0x6, "xpress"), ZSTD_COMPRESSION(0x7, "zstd"), - DISABLE_COMPRESSION_OPTION(0x7f, null); + DISABLE_COMPRESSION_OPTION(0xff.toByte(), null); // rocksdb::CompressionType::kDisableCompressionOption actual fun getLibraryName() = libraryName } diff --git a/src/nativeMain/kotlin/maryk/rocksdb/ConfigOptions.kt b/src/nativeMain/kotlin/maryk/rocksdb/ConfigOptions.kt new file mode 100644 index 0000000..281b988 --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/ConfigOptions.kt @@ -0,0 +1,58 @@ +@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) + +package maryk.rocksdb + +import cnames.structs.rocksdb_configoptions_t +import kotlinx.cinterop.CPointer +import maryk.toUByte +import rocksdb.rocksdb_configoptions_create +import rocksdb.rocksdb_configoptions_destroy +import rocksdb.rocksdb_configoptions_set_delimiter +import rocksdb.rocksdb_configoptions_set_env +import rocksdb.rocksdb_configoptions_set_ignore_unknown_options +import rocksdb.rocksdb_configoptions_set_input_strings_escaped +import rocksdb.rocksdb_configoptions_set_sanity_level + +actual class ConfigOptions internal constructor( + internal val native: CPointer?, +) : RocksObject() { + actual constructor() : this(rocksdb_configoptions_create()) + + override fun close() { + if (isOwningHandle()) { + rocksdb_configoptions_destroy(native) + super.close() + } + } + + actual fun setDelimiter(delimiter: String): ConfigOptions { + rocksdb_configoptions_set_delimiter(native, delimiter) + return this + } + + actual fun setIgnoreUnknownOptions(ignore: Boolean): ConfigOptions { + rocksdb_configoptions_set_ignore_unknown_options(native, ignore.toUByte()) + return this + } + + actual fun setEnv(env: Env): ConfigOptions { + rocksdb_configoptions_set_env(native, env.native) + return this + } + + actual fun setInputStringsEscaped(escaped: Boolean): ConfigOptions { + rocksdb_configoptions_set_input_strings_escaped(native, escaped.toUByte()) + return this + } + + actual fun setSanityLevel(level: SanityLevel): ConfigOptions { + rocksdb_configoptions_set_sanity_level(native, level.toNativeValue()) + return this + } +} + +private fun SanityLevel.toNativeValue(): UByte = when (this) { + SanityLevel.NONE -> 0u + SanityLevel.LOOSELY_COMPATIBLE -> 1u + SanityLevel.EXACT_MATCH -> 2u +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/DBOptions.kt b/src/nativeMain/kotlin/maryk/rocksdb/DBOptions.kt index 619698a..b74002f 100644 --- a/src/nativeMain/kotlin/maryk/rocksdb/DBOptions.kt +++ b/src/nativeMain/kotlin/maryk/rocksdb/DBOptions.kt @@ -4,6 +4,7 @@ import cnames.structs.rocksdb_options_t import kotlinx.cinterop.CPointer import maryk.toBoolean import maryk.toUByte +import rocksdb.rocksdb_options_add_eventlistener import rocksdb.rocksdb_options_create import rocksdb.rocksdb_options_destroy import rocksdb.rocksdb_options_get_WAL_size_limit_MB @@ -15,6 +16,7 @@ import rocksdb.rocksdb_options_get_keep_log_file_num import rocksdb.rocksdb_options_get_log_file_time_to_roll import rocksdb.rocksdb_options_get_max_log_file_size import rocksdb.rocksdb_options_get_paranoid_checks +import rocksdb.rocksdb_options_get_wal_recovery_mode import rocksdb.rocksdb_options_get_use_fsync import rocksdb.rocksdb_options_set_WAL_size_limit_MB import rocksdb.rocksdb_options_set_create_if_missing @@ -25,8 +27,21 @@ import rocksdb.rocksdb_options_set_keep_log_file_num import rocksdb.rocksdb_options_set_log_file_time_to_roll import rocksdb.rocksdb_options_set_max_log_file_size import rocksdb.rocksdb_options_set_paranoid_checks +import rocksdb.rocksdb_options_set_wal_recovery_mode import rocksdb.rocksdb_options_set_use_fsync +actual fun DBOptions.addEventListener(listener: EventListener): DBOptions { + rocksdb_options_add_eventlistener(native, listener.native) + listener.disownHandle() + return this +} + +actual fun Options.addEventListener(listener: EventListener): Options { + rocksdb_options_add_eventlistener(native, listener.native) + listener.disownHandle() + return this +} + actual class DBOptions internal constructor( internal val native: CPointer ) : RocksObject() { @@ -130,4 +145,12 @@ actual class DBOptions internal constructor( actual fun walSizeLimitMB(): Long = rocksdb_options_get_WAL_size_limit_MB(native).toLong() + + actual fun setWalRecoveryMode(mode: WALRecoveryMode): DBOptions { + rocksdb_options_set_wal_recovery_mode(native, mode.getValue().toInt()) + return this + } + + actual fun walRecoveryMode(): WALRecoveryMode = + walRecoveryModeFromValue(rocksdb_options_get_wal_recovery_mode(native).toByte()) } diff --git a/src/nativeMain/kotlin/maryk/rocksdb/EventListener.kt b/src/nativeMain/kotlin/maryk/rocksdb/EventListener.kt new file mode 100644 index 0000000..149f086 --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/EventListener.kt @@ -0,0 +1,124 @@ +package maryk.rocksdb + +import cnames.structs.rocksdb_compactionjobinfo_t +import cnames.structs.rocksdb_eventlistener_t +import cnames.structs.rocksdb_externalfileingestioninfo_t +import cnames.structs.rocksdb_flushjobinfo_t +import cnames.structs.rocksdb_t +import kotlinx.cinterop.COpaquePointer +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.StableRef +import kotlinx.cinterop.asStableRef +import kotlinx.cinterop.staticCFunction +import rocksdb.rocksdb_eventlistener_create +import rocksdb.rocksdb_eventlistener_destroy + +actual abstract class EventListener : RocksCallbackObject() { + internal val native: CPointer + private val stableRef = StableRef.create(this) + + init { + native = rocksdb_eventlistener_create( + stableRef.asCPointer(), + staticCFunction(::eventListenerDestructor), + staticCFunction(::eventListenerOnFlushBegin), + staticCFunction(::eventListenerOnFlushCompleted), + staticCFunction(::eventListenerOnCompactionBegin), + staticCFunction(::eventListenerOnCompactionCompleted), + null, + null, + staticCFunction(::eventListenerOnExternalFileIngested), + null, + null, + null + ) ?: error("Failed to allocate RocksDB event listener") + } + + actual open fun onFlushBeginEvent(db: RocksDB, flushJobInfo: FlushJobInfo) {} + + actual open fun onFlushCompletedEvent(db: RocksDB, flushJobInfo: FlushJobInfo) {} + + actual open fun onCompactionBeginEvent(db: RocksDB, compactionJobInfo: CompactionJobInfo) {} + + actual open fun onCompactionCompletedEvent(db: RocksDB, compactionJobInfo: CompactionJobInfo) {} + + actual open fun onExternalFileIngested(db: RocksDB, ingestionInfo: ExternalFileIngestionInfo) {} + + override fun close() { + if (isOwningHandle()) { + rocksdb_eventlistener_destroy(native) + super.close() + } + } +} + +private fun eventListenerDestructor(state: COpaquePointer?) { + state?.asStableRef()?.dispose() +} + +private fun eventListenerOnFlushBegin( + state: COpaquePointer?, + dbPtr: CPointer?, + infoPtr: CPointer? +) { + val listener = state?.asStableRef()?.get() ?: return + val db = dbPtr?.let(::wrapDb) ?: return + val info = infoPtr?.let(::FlushJobInfo) ?: return + listener.onFlushBeginEvent(db, info) + db.close() +} + +private fun eventListenerOnFlushCompleted( + state: COpaquePointer?, + dbPtr: CPointer?, + infoPtr: CPointer? +) { + val listener = state?.asStableRef()?.get() ?: return + val db = dbPtr?.let(::wrapDb) ?: return + val info = infoPtr?.let(::FlushJobInfo) ?: return + listener.onFlushCompletedEvent(db, info) + db.close() +} + +private fun eventListenerOnCompactionBegin( + state: COpaquePointer?, + dbPtr: CPointer?, + infoPtr: CPointer? +) { + val listener = state?.asStableRef()?.get() ?: return + val db = dbPtr?.let(::wrapDb) ?: return + val info = infoPtr?.let(::CompactionJobInfo) ?: return + listener.onCompactionBeginEvent(db, info) + db.close() +} + +private fun eventListenerOnCompactionCompleted( + state: COpaquePointer?, + dbPtr: CPointer?, + infoPtr: CPointer? +) { + val listener = state?.asStableRef()?.get() ?: return + val db = dbPtr?.let(::wrapDb) ?: return + val info = infoPtr?.let(::CompactionJobInfo) ?: return + listener.onCompactionCompletedEvent(db, info) + db.close() +} + + +private fun eventListenerOnExternalFileIngested( + state: COpaquePointer?, + dbPtr: CPointer?, + infoPtr: CPointer? +) { + val listener = state?.asStableRef()?.get() ?: return + val db = dbPtr?.let(::wrapDb) ?: return + val info = infoPtr?.let(::ExternalFileIngestionInfo) ?: return + listener.onExternalFileIngested(db, info) + db.close() +} + +private fun wrapDb(native: CPointer): RocksDB { + val db = RocksDB(native) + db.disownHandle() + return db +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/ExternalFileIngestionInfo.kt b/src/nativeMain/kotlin/maryk/rocksdb/ExternalFileIngestionInfo.kt new file mode 100644 index 0000000..ec5f777 --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/ExternalFileIngestionInfo.kt @@ -0,0 +1,51 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package maryk.rocksdb + +import cnames.structs.rocksdb_externalfileingestioninfo_t +import kotlinx.cinterop.COpaquePointer +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.value +import maryk.toByteArray +import platform.posix.size_tVar +import kotlinx.cinterop.ByteVar +import rocksdb.rocksdb_externalfileingestioninfo_cf_name +import rocksdb.rocksdb_externalfileingestioninfo_external_file_path +import rocksdb.rocksdb_externalfileingestioninfo_global_seqno +import rocksdb.rocksdb_externalfileingestioninfo_internal_file_path +import rocksdb.rocksdb_externalfileingestioninfo_table_properties + +actual class ExternalFileIngestionInfo internal constructor( + internal val native: CPointer +) { + actual fun columnFamilyName(): String = memScoped { + val length = alloc() + rocksdb_externalfileingestioninfo_cf_name(native, length.ptr) + ?.toByteArray(length.value) + ?.decodeToString() ?: "" + } + + actual fun externalFilePath(): String = memScoped { + val length = alloc() + rocksdb_externalfileingestioninfo_external_file_path(native, length.ptr) + ?.toByteArray(length.value) + ?.decodeToString() ?: "" + } + + actual fun internalFilePath(): String = memScoped { + val length = alloc() + rocksdb_externalfileingestioninfo_internal_file_path(native, length.ptr) + ?.toByteArray(length.value) + ?.decodeToString() ?: "" + } + + actual fun globalSequenceNumber(): Long = + rocksdb_externalfileingestioninfo_global_seqno(native).toLong() + + actual fun tableProperties(): TableProperties? = + rocksdb_externalfileingestioninfo_table_properties(native)?.let(::TableProperties) +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/FlushJobInfo.kt b/src/nativeMain/kotlin/maryk/rocksdb/FlushJobInfo.kt new file mode 100644 index 0000000..e257b6f --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/FlushJobInfo.kt @@ -0,0 +1,88 @@ +package maryk.rocksdb + +import cnames.structs.rocksdb_flushjobinfo_t +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.value +import maryk.toBoolean +import maryk.toByteArray +import platform.posix.size_tVar +import rocksdb.rocksdb_flushjobinfo_cf_id +import rocksdb.rocksdb_flushjobinfo_cf_name +import rocksdb.rocksdb_flushjobinfo_file_path +import rocksdb.rocksdb_flushjobinfo_flush_reason +import rocksdb.rocksdb_flushjobinfo_job_id +import rocksdb.rocksdb_flushjobinfo_largest_seqno +import rocksdb.rocksdb_flushjobinfo_smallest_seqno +import rocksdb.rocksdb_flushjobinfo_table_properties +import rocksdb.rocksdb_flushjobinfo_thread_id +import rocksdb.rocksdb_flushjobinfo_triggered_writes_slowdown +import rocksdb.rocksdb_flushjobinfo_triggered_writes_stop + +actual class FlushJobInfo internal constructor( + private val columnFamilyIdValue: Long, + private val columnFamilyNameValue: String, + private val filePathValue: String, + private val threadIdValue: Long, + private val jobIdValue: Int, + private val triggeredWritesSlowdownValue: Boolean, + private val triggeredWritesStopValue: Boolean, + private val smallestSeqnoValue: Long, + private val largestSeqnoValue: Long, + private val tablePropertiesValue: TableProperties, + private val flushReasonValue: FlushReason, +) { + internal constructor(native: CPointer) : this( + columnFamilyIdValue = rocksdb_flushjobinfo_cf_id(native).toLong(), + columnFamilyNameValue = memScoped { + val length = alloc() + rocksdb_flushjobinfo_cf_name(native, length.ptr)!! + .toByteArray(length.value) + .decodeToString() + }, + filePathValue = memScoped { + val length = alloc() + rocksdb_flushjobinfo_file_path(native, length.ptr)!! + .toByteArray(length.value) + .decodeToString() + }, + threadIdValue = rocksdb_flushjobinfo_thread_id(native).toLong(), + jobIdValue = rocksdb_flushjobinfo_job_id(native), + triggeredWritesSlowdownValue = + rocksdb_flushjobinfo_triggered_writes_slowdown(native).toBoolean(), + triggeredWritesStopValue = + rocksdb_flushjobinfo_triggered_writes_stop(native).toBoolean(), + smallestSeqnoValue = rocksdb_flushjobinfo_smallest_seqno(native).toLong(), + largestSeqnoValue = rocksdb_flushjobinfo_largest_seqno(native).toLong(), + tablePropertiesValue = TableProperties( + rocksdb_flushjobinfo_table_properties(native)!! + ), + flushReasonValue = flushReasonFromValue( + rocksdb_flushjobinfo_flush_reason(native).toByte() + ), + ) + + actual fun columnFamilyId(): Long = columnFamilyIdValue + + actual fun columnFamilyName(): String = columnFamilyNameValue + + actual fun filePath(): String = filePathValue + + actual fun threadId(): Long = threadIdValue + + actual fun jobId(): Int = jobIdValue + + actual fun triggeredWritesSlowdown(): Boolean = triggeredWritesSlowdownValue + + actual fun triggeredWritesStop(): Boolean = triggeredWritesStopValue + + actual fun smallestSeqno(): Long = smallestSeqnoValue + + actual fun largestSeqno(): Long = largestSeqnoValue + + actual fun tableProperties(): TableProperties = tablePropertiesValue + + actual fun flushReason(): FlushReason = flushReasonValue +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/FlushOptions.kt b/src/nativeMain/kotlin/maryk/rocksdb/FlushOptions.kt new file mode 100644 index 0000000..e2a9f09 --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/FlushOptions.kt @@ -0,0 +1,43 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package maryk.rocksdb + +import cnames.structs.rocksdb_flushoptions_t +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import maryk.toUByte +import rocksdb.rocksdb_flushoptions_create +import rocksdb.rocksdb_flushoptions_destroy +import rocksdb.rocksdb_flushoptions_set_allow_write_stall +import rocksdb.rocksdb_flushoptions_set_wait + +actual class FlushOptions internal constructor( + internal val native: CPointer, + private var waitForFlushValue: Boolean, + private var allowWriteStallValue: Boolean +) : RocksObject() { + actual constructor() : this(rocksdb_flushoptions_create()!!, false, false) + + override fun close() { + if (isOwningHandle()) { + rocksdb_flushoptions_destroy(native) + super.close() + } + } + + actual fun setWaitForFlush(wait: Boolean): FlushOptions { + waitForFlushValue = wait + rocksdb_flushoptions_set_wait(native, wait.toUByte()) + return this + } + + actual fun waitForFlush(): Boolean = waitForFlushValue + + actual fun setAllowWriteStall(allow: Boolean): FlushOptions { + allowWriteStallValue = allow + rocksdb_flushoptions_set_allow_write_stall(native, allow.toUByte()) + return this + } + + actual fun allowWriteStall(): Boolean = allowWriteStallValue +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/FlushReason.kt b/src/nativeMain/kotlin/maryk/rocksdb/FlushReason.kt new file mode 100644 index 0000000..ef4a51e --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/FlushReason.kt @@ -0,0 +1,25 @@ +package maryk.rocksdb + +actual enum class FlushReason(internal val id: Byte) { + OTHERS(0x00), + GET_LIVE_FILES(0x01), + SHUTDOWN(0x02), + EXTERNAL_FILE_INGESTION(0x03), + MANUAL_COMPACTION(0x04), + WRITE_BUFFER_MANAGER(0x05), + WRITE_BUFFER_FULL(0x06), + TEST(0x07), + DELETE_FILES(0x08), + AUTO_COMPACTION(0x09), + MANUAL_FLUSH(0x0a), + ERROR_RECOVERY(0x0b), + ERROR_RECOVERY_RETRY_FLUSH(0x0c), + WAL_FULL(0x0d), + CATCH_UP_AFTER_ERROR_RECOVERY(0x0e); +} + +actual fun FlushReason.value(): Byte = id + +actual fun flushReasonFromValue(value: Byte): FlushReason = + FlushReason.entries.firstOrNull { it.id == value } + ?: error("Unknown FlushReason value: $value") diff --git a/src/nativeMain/kotlin/maryk/rocksdb/ImportColumnFamilyOptions.kt b/src/nativeMain/kotlin/maryk/rocksdb/ImportColumnFamilyOptions.kt new file mode 100644 index 0000000..0b6860f --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/ImportColumnFamilyOptions.kt @@ -0,0 +1,38 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package maryk.rocksdb + +import cnames.structs.rocksdb_import_column_family_options_t +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import maryk.toUByte +import rocksdb.rocksdb_import_column_family_options_create +import rocksdb.rocksdb_import_column_family_options_destroy +import rocksdb.rocksdb_import_column_family_options_get_move_files +import rocksdb.rocksdb_import_column_family_options_set_move_files + +actual class ImportColumnFamilyOptions internal constructor( + internal val native: CPointer, + private var moveFilesValue: Boolean, +) : RocksObject() { + + actual constructor() : this(rocksdb_import_column_family_options_create()!!, false) + + actual fun setMoveFiles(moveFiles: Boolean): ImportColumnFamilyOptions { + moveFilesValue = moveFiles + rocksdb_import_column_family_options_set_move_files(native, moveFiles.toUByte()) + return this + } + + actual fun moveFiles(): Boolean { + moveFilesValue = rocksdb_import_column_family_options_get_move_files(native) != 0.toUByte() + return moveFilesValue + } + + override fun close() { + if (isOwningHandle()) { + rocksdb_import_column_family_options_destroy(native) + super.close() + } + } +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/LiveFileMetaData.kt b/src/nativeMain/kotlin/maryk/rocksdb/LiveFileMetaData.kt new file mode 100644 index 0000000..8d0e745 --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/LiveFileMetaData.kt @@ -0,0 +1,15 @@ +package maryk.rocksdb + +actual class LiveFileMetaData internal constructor( + private val columnFamilyNameValue: ByteArray, + private val levelValue: Int, + fileName: String, + path: String, + size: ULong, + smallestKey: ByteArray, + largestKey: ByteArray, +) : SstFileMetaData(fileName, path, size, smallestKey, largestKey) { + actual fun columnFamilyName(): ByteArray = columnFamilyNameValue + + actual fun level(): Int = levelValue +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/Options.kt b/src/nativeMain/kotlin/maryk/rocksdb/Options.kt index 34a6af0..37b9b0e 100644 --- a/src/nativeMain/kotlin/maryk/rocksdb/Options.kt +++ b/src/nativeMain/kotlin/maryk/rocksdb/Options.kt @@ -3,6 +3,7 @@ package maryk.rocksdb import cnames.structs.rocksdb_options_t +import cnames.structs.rocksdb_sst_file_manager_t import kotlinx.cinterop.CPointer import maryk.rocksdb.util.BytewiseComparator import maryk.rocksdb.util.ReverseBytewiseComparator @@ -63,6 +64,7 @@ import rocksdb.rocksdb_options_set_plain_table_factory import rocksdb.rocksdb_options_set_num_levels import rocksdb.rocksdb_options_set_paranoid_checks import rocksdb.rocksdb_options_set_prefix_extractor +import rocksdb.rocksdb_options_set_sst_file_manager import rocksdb.rocksdb_options_set_target_file_size_base import rocksdb.rocksdb_options_set_target_file_size_multiplier import rocksdb.rocksdb_options_set_use_fsync @@ -94,6 +96,11 @@ actual class Options private constructor(val native: CPointer return this } + actual fun setSstFileManager(sstFileManager: SstFileManager): Options { + rocksdb_options_set_sst_file_manager(native, sstFileManager.native) + return this + } + actual fun setMaxOpenFiles(maxOpenFiles: Int): Options { rocksdb_options_set_max_open_files(native, maxOpenFiles) return this diff --git a/src/nativeMain/kotlin/maryk/rocksdb/OptionsUtil.kt b/src/nativeMain/kotlin/maryk/rocksdb/OptionsUtil.kt new file mode 100644 index 0000000..00df05e --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/OptionsUtil.kt @@ -0,0 +1,108 @@ +@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) + +package maryk.rocksdb + +import cnames.structs.rocksdb_configoptions_t +import cnames.structs.rocksdb_loaded_cf_options_t +import cnames.structs.rocksdb_options_t +import kotlinx.cinterop.ByteVar +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.CPointerVar +import kotlinx.cinterop.CValuesRef +import kotlinx.cinterop.ULongVar +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.readBytes +import kotlinx.cinterop.toKString +import kotlinx.cinterop.value +import maryk.wrapWithNullErrorThrower +import rocksdb.rocksdb_free +import rocksdb.rocksdb_optionsutil_descriptor_name +import rocksdb.rocksdb_optionsutil_descriptor_options +import rocksdb.rocksdb_optionsutil_descriptors_count +import rocksdb.rocksdb_optionsutil_descriptors_destroy +import rocksdb.rocksdb_optionsutil_get_latest_options_file_name +import rocksdb.rocksdb_optionsutil_load_latest_options +import rocksdb.rocksdb_optionsutil_load_options_from_file + +actual object OptionsUtil { + @Throws(RocksDBException::class) + actual fun loadLatestOptions( + configOptions: ConfigOptions, + dbPath: String, + dbOptions: DBOptions, + columnFamilyDescriptors: MutableList, + ) { + loadDescriptors(configOptions, dbPath, dbOptions, columnFamilyDescriptors) { config, path, options, error -> + rocksdb_optionsutil_load_latest_options(config, path, options, error) + } + } + + @Throws(RocksDBException::class) + actual fun loadOptionsFromFile( + configOptions: ConfigOptions, + optionsFilePath: String, + dbOptions: DBOptions, + columnFamilyDescriptors: MutableList, + ) { + loadDescriptors(configOptions, optionsFilePath, dbOptions, columnFamilyDescriptors) { config, path, options, error -> + rocksdb_optionsutil_load_options_from_file(config, path, options, error) + } + } + + @Throws(RocksDBException::class) + actual fun getLatestOptionsFileName(dbPath: String, env: Env): String { + val pointer = Unit.wrapWithNullErrorThrower { error -> + rocksdb_optionsutil_get_latest_options_file_name(dbPath, env.native, error) + } + val fileName = pointer?.toKString().orEmpty() + pointer?.let { rocksdb_free(it) } + return fileName + } + + private inline fun loadDescriptors( + configOptions: ConfigOptions, + path: String, + dbOptions: DBOptions, + columnFamilyDescriptors: MutableList, + crossinline loader: ( + config: CValuesRef?, + path: String?, + options: CPointer, + error: CValuesRef>, + ) -> CPointer?, + ) { + columnFamilyDescriptors.clear() + val descriptorSet = Unit.wrapWithNullErrorThrower { error -> + loader(configOptions.native, path, dbOptions.native, error) + } + descriptorSet?.let { bundle -> + try { + populateDescriptors(bundle, columnFamilyDescriptors) + } finally { + rocksdb_optionsutil_descriptors_destroy(bundle) + } + } + } + + private fun populateDescriptors( + bundle: CPointer?, + columnFamilyDescriptors: MutableList, + ) { + val count = rocksdb_optionsutil_descriptors_count(bundle).toInt() + if (count == 0) return + repeat(count) { index -> + memScoped { + val length = alloc() + val namePtr = rocksdb_optionsutil_descriptor_name(bundle, index.toULong(), length.ptr) + val name = namePtr?.readBytes(length.value.toInt()) ?: ByteArray(0) + namePtr?.let { rocksdb_free(it) } + val optionsPtr = rocksdb_optionsutil_descriptor_options(bundle, index.toULong()) + requireNotNull(optionsPtr) { "Column family options pointer was null" } + val cfOptions = ColumnFamilyOptions.wrap(optionsPtr, owning = true) + columnFamilyDescriptors += ColumnFamilyDescriptor(name, cfOptions) + } + } + } +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/PerfContext.kt b/src/nativeMain/kotlin/maryk/rocksdb/PerfContext.kt new file mode 100644 index 0000000..2b391f4 --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/PerfContext.kt @@ -0,0 +1,356 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package maryk.rocksdb + +import cnames.structs.rocksdb_perfcontext_t +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.toKString +import rocksdb.rocksdb_free +import rocksdb.rocksdb_perfcontext_create +import rocksdb.rocksdb_perfcontext_destroy +import rocksdb.rocksdb_perfcontext_metric +import rocksdb.rocksdb_perfcontext_report +import rocksdb.rocksdb_perfcontext_reset + +actual class PerfContext internal constructor( + internal val native: CPointer, +) : RocksObject() { + constructor() : this(rocksdb_perfcontext_create()!!) + + override fun close() { + if (isOwningHandle()) { + rocksdb_perfcontext_destroy(native) + super.close() + } + } + + actual fun reset() { + rocksdb_perfcontext_reset(native) + } + + private fun metric(metric: PerfContextMetric): Long = + rocksdb_perfcontext_metric(native, metric.id).toLong() + + actual fun getUserKeyComparisonCount(): Long = metric(PerfContextMetric.USER_KEY_COMPARISON_COUNT) + + actual fun getBlockCacheHitCount(): Long = metric(PerfContextMetric.BLOCK_CACHE_HIT_COUNT) + + actual fun getBlockReadCount(): Long = metric(PerfContextMetric.BLOCK_READ_COUNT) + + actual fun getBlockReadByte(): Long = metric(PerfContextMetric.BLOCK_READ_BYTE) + + actual fun getBlockReadTime(): Long = metric(PerfContextMetric.BLOCK_READ_TIME) + + actual fun getBlockReadCpuTime(): Long = metric(PerfContextMetric.BLOCK_READ_CPU_TIME) + + actual fun getBlockCacheIndexHitCount(): Long = metric(PerfContextMetric.BLOCK_CACHE_INDEX_HIT_COUNT) + + actual fun getBlockCacheStandaloneHandleCount(): Long = + metric(PerfContextMetric.BLOCK_CACHE_STANDALONE_HANDLE_COUNT) + + actual fun getBlockCacheRealHandleCount(): Long = + metric(PerfContextMetric.BLOCK_CACHE_REAL_HANDLE_COUNT) + + actual fun getIndexBlockReadCount(): Long = metric(PerfContextMetric.INDEX_BLOCK_READ_COUNT) + + actual fun getBlockCacheFilterHitCount(): Long = metric(PerfContextMetric.BLOCK_CACHE_FILTER_HIT_COUNT) + + actual fun getFilterBlockReadCount(): Long = metric(PerfContextMetric.FILTER_BLOCK_READ_COUNT) + + actual fun getCompressionDictBlockReadCount(): Long = + metric(PerfContextMetric.COMPRESSION_DICT_BLOCK_READ_COUNT) + + actual fun getSecondaryCacheHitCount(): Long = metric(PerfContextMetric.SECONDARY_CACHE_HIT_COUNT) + + actual fun getCompressedSecCacheInsertRealCount(): Long = + metric(PerfContextMetric.COMPRESSED_SEC_CACHE_INSERT_REAL_COUNT) + + actual fun getCompressedSecCacheInsertDummyCount(): Long = + metric(PerfContextMetric.COMPRESSED_SEC_CACHE_INSERT_DUMMY_COUNT) + + actual fun getCompressedSecCacheUncompressedBytes(): Long = + metric(PerfContextMetric.COMPRESSED_SEC_CACHE_UNCOMPRESSED_BYTES) + + actual fun getCompressedSecCacheCompressedBytes(): Long = + metric(PerfContextMetric.COMPRESSED_SEC_CACHE_COMPRESSED_BYTES) + + actual fun getBlockChecksumTime(): Long = metric(PerfContextMetric.BLOCK_CHECKSUM_TIME) + + actual fun getBlockDecompressTime(): Long = metric(PerfContextMetric.BLOCK_DECOMPRESS_TIME) + + actual fun getReadBytes(): Long = metric(PerfContextMetric.GET_READ_BYTES) + + actual fun getMultigetReadBytes(): Long = metric(PerfContextMetric.MULTIGET_READ_BYTES) + + actual fun getIterReadBytes(): Long = metric(PerfContextMetric.ITER_READ_BYTES) + + actual fun getBlobCacheHitCount(): Long = metric(PerfContextMetric.BLOB_CACHE_HIT_COUNT) + + actual fun getBlobReadCount(): Long = metric(PerfContextMetric.BLOB_READ_COUNT) + + actual fun getBlobReadByte(): Long = metric(PerfContextMetric.BLOB_READ_BYTE) + + actual fun getBlobReadTime(): Long = metric(PerfContextMetric.BLOB_READ_TIME) + + actual fun getBlobChecksumTime(): Long = metric(PerfContextMetric.BLOB_CHECKSUM_TIME) + + actual fun getBlobDecompressTime(): Long = metric(PerfContextMetric.BLOB_DECOMPRESS_TIME) + + actual fun getInternalKeySkippedCount(): Long = metric(PerfContextMetric.INTERNAL_KEY_SKIPPED_COUNT) + + actual fun getInternalDeleteSkippedCount(): Long = metric(PerfContextMetric.INTERNAL_DELETE_SKIPPED_COUNT) + + actual fun getInternalRecentSkippedCount(): Long = metric(PerfContextMetric.INTERNAL_RECENT_SKIPPED_COUNT) + + actual fun getInternalMergeCount(): Long = metric(PerfContextMetric.INTERNAL_MERGE_COUNT) + + actual fun getInternalMergePointLookupCount(): Long = + metric(PerfContextMetric.INTERNAL_MERGE_POINT_LOOKUP_COUNT) + + actual fun getInternalRangeDelReseekCount(): Long = + metric(PerfContextMetric.INTERNAL_RANGE_DEL_RESEEK_COUNT) + + actual fun getSnapshotTime(): Long = metric(PerfContextMetric.GET_SNAPSHOT_TIME) + + actual fun getFromMemtableTime(): Long = metric(PerfContextMetric.GET_FROM_MEMTABLE_TIME) + + actual fun getFromMemtableCount(): Long = metric(PerfContextMetric.GET_FROM_MEMTABLE_COUNT) + + actual fun getPostProcessTime(): Long = metric(PerfContextMetric.GET_POST_PROCESS_TIME) + + actual fun getFromOutputFilesTime(): Long = metric(PerfContextMetric.GET_FROM_OUTPUT_FILES_TIME) + + actual fun getSeekOnMemtableTime(): Long = metric(PerfContextMetric.SEEK_ON_MEMTABLE_TIME) + + actual fun getSeekOnMemtableCount(): Long = metric(PerfContextMetric.SEEK_ON_MEMTABLE_COUNT) + + actual fun getNextOnMemtableCount(): Long = metric(PerfContextMetric.NEXT_ON_MEMTABLE_COUNT) + + actual fun getPrevOnMemtableCount(): Long = metric(PerfContextMetric.PREV_ON_MEMTABLE_COUNT) + + actual fun getSeekChildSeekTime(): Long = metric(PerfContextMetric.SEEK_CHILD_SEEK_TIME) + + actual fun getSeekChildSeekCount(): Long = metric(PerfContextMetric.SEEK_CHILD_SEEK_COUNT) + + actual fun getSeekMinHeapTime(): Long = metric(PerfContextMetric.SEEK_MIN_HEAP_TIME) + + actual fun getSeekMaxHeapTime(): Long = metric(PerfContextMetric.SEEK_MAX_HEAP_TIME) + + actual fun getSeekInternalSeekTime(): Long = metric(PerfContextMetric.SEEK_INTERNAL_SEEK_TIME) + + actual fun getFindNextUserEntryTime(): Long = metric(PerfContextMetric.FIND_NEXT_USER_ENTRY_TIME) + + actual fun getWriteWalTime(): Long = metric(PerfContextMetric.WRITE_WAL_TIME) + + actual fun getWriteMemtableTime(): Long = metric(PerfContextMetric.WRITE_MEMTABLE_TIME) + + actual fun getWriteDelayTime(): Long = metric(PerfContextMetric.WRITE_DELAY_TIME) + + actual fun getWriteSchedulingFlushesCompactionsTime(): Long = + metric(PerfContextMetric.WRITE_SCHEDULING_FLUSHES_COMPACTIONS_TIME) + + actual fun getWritePreAndPostProcessTime(): Long = + metric(PerfContextMetric.WRITE_PRE_AND_POST_PROCESS_TIME) + + actual fun getWriteThreadWaitNanos(): Long = metric(PerfContextMetric.WRITE_THREAD_WAIT_NANOS) + + actual fun getDbMutexLockNanos(): Long = metric(PerfContextMetric.DB_MUTEX_LOCK_NANOS) + + actual fun getDbConditionWaitNanos(): Long = metric(PerfContextMetric.DB_CONDITION_WAIT_NANOS) + + actual fun getMergeOperatorTimeNanos(): Long = metric(PerfContextMetric.MERGE_OPERATOR_TIME_NANOS) + + actual fun getReadIndexBlockNanos(): Long = metric(PerfContextMetric.READ_INDEX_BLOCK_NANOS) + + actual fun getReadFilterBlockNanos(): Long = metric(PerfContextMetric.READ_FILTER_BLOCK_NANOS) + + actual fun getNewTableBlockIterNanos(): Long = metric(PerfContextMetric.NEW_TABLE_BLOCK_ITER_NANOS) + + actual fun getNewTableIteratorNanos(): Long = metric(PerfContextMetric.NEW_TABLE_ITERATOR_NANOS) + + actual fun getBlockSeekNanos(): Long = metric(PerfContextMetric.BLOCK_SEEK_NANOS) + + actual fun getFindTableNanos(): Long = metric(PerfContextMetric.FIND_TABLE_NANOS) + + actual fun getBloomMemtableHitCount(): Long = metric(PerfContextMetric.BLOOM_MEMTABLE_HIT_COUNT) + + actual fun getBloomMemtableMissCount(): Long = metric(PerfContextMetric.BLOOM_MEMTABLE_MISS_COUNT) + + actual fun getBloomSstHitCount(): Long = metric(PerfContextMetric.BLOOM_SST_HIT_COUNT) + + actual fun getBloomSstMissCount(): Long = metric(PerfContextMetric.BLOOM_SST_MISS_COUNT) + + actual fun getKeyLockWaitTime(): Long = metric(PerfContextMetric.KEY_LOCK_WAIT_TIME) + + actual fun getKeyLockWaitCount(): Long = metric(PerfContextMetric.KEY_LOCK_WAIT_COUNT) + + actual fun getEnvNewSequentialFileNanos(): Long = metric(PerfContextMetric.ENV_NEW_SEQUENTIAL_FILE_NANOS) + + actual fun getEnvNewRandomAccessFileNanos(): Long = + metric(PerfContextMetric.ENV_NEW_RANDOM_ACCESS_FILE_NANOS) + + actual fun getEnvNewWritableFileNanos(): Long = metric(PerfContextMetric.ENV_NEW_WRITABLE_FILE_NANOS) + + actual fun getEnvReuseWritableFileNanos(): Long = metric(PerfContextMetric.ENV_REUSE_WRITABLE_FILE_NANOS) + + actual fun getEnvNewRandomRwFileNanos(): Long = metric(PerfContextMetric.ENV_NEW_RANDOM_RW_FILE_NANOS) + + actual fun getEnvNewDirectoryNanos(): Long = metric(PerfContextMetric.ENV_NEW_DIRECTORY_NANOS) + + actual fun getEnvFileExistsNanos(): Long = metric(PerfContextMetric.ENV_FILE_EXISTS_NANOS) + + actual fun getEnvGetChildrenNanos(): Long = metric(PerfContextMetric.ENV_GET_CHILDREN_NANOS) + + actual fun getEnvGetChildrenFileAttributesNanos(): Long = + metric(PerfContextMetric.ENV_GET_CHILDREN_FILE_ATTRIBUTES_NANOS) + + actual fun getEnvDeleteFileNanos(): Long = metric(PerfContextMetric.ENV_DELETE_FILE_NANOS) + + actual fun getEnvCreateDirNanos(): Long = metric(PerfContextMetric.ENV_CREATE_DIR_NANOS) + + actual fun getEnvCreateDirIfMissingNanos(): Long = metric(PerfContextMetric.ENV_CREATE_DIR_IF_MISSING_NANOS) + + actual fun getEnvDeleteDirNanos(): Long = metric(PerfContextMetric.ENV_DELETE_DIR_NANOS) + + actual fun getEnvGetFileSizeNanos(): Long = metric(PerfContextMetric.ENV_GET_FILE_SIZE_NANOS) + + actual fun getEnvGetFileModificationTimeNanos(): Long = + metric(PerfContextMetric.ENV_GET_FILE_MODIFICATION_TIME_NANOS) + + actual fun getEnvRenameFileNanos(): Long = metric(PerfContextMetric.ENV_RENAME_FILE_NANOS) + + actual fun getEnvLinkFileNanos(): Long = metric(PerfContextMetric.ENV_LINK_FILE_NANOS) + + actual fun getEnvLockFileNanos(): Long = metric(PerfContextMetric.ENV_LOCK_FILE_NANOS) + + actual fun getEnvUnlockFileNanos(): Long = metric(PerfContextMetric.ENV_UNLOCK_FILE_NANOS) + + actual fun getEnvNewLoggerNanos(): Long = metric(PerfContextMetric.ENV_NEW_LOGGER_NANOS) + + actual fun getGetCpuNanos(): Long = metric(PerfContextMetric.GET_CPU_NANOS) + + actual fun getIterNextCpuNanos(): Long = metric(PerfContextMetric.ITER_NEXT_CPU_NANOS) + + actual fun getIterPrevCpuNanos(): Long = metric(PerfContextMetric.ITER_PREV_CPU_NANOS) + + actual fun getIterSeekCpuNanos(): Long = metric(PerfContextMetric.ITER_SEEK_CPU_NANOS) + + actual fun getEncryptDataNanos(): Long = metric(PerfContextMetric.ENCRYPT_DATA_NANOS) + + actual fun getDecryptDataNanos(): Long = metric(PerfContextMetric.DECRYPT_DATA_NANOS) + + actual fun getNumberAsyncSeek(): Long = metric(PerfContextMetric.NUMBER_ASYNC_SEEK) + + actual fun toString(excludeZeroCounters: Boolean): String { + val raw = rocksdb_perfcontext_report(native, if (excludeZeroCounters) 1u else 0u) + return raw?.toKString().also { rocksdb_free(raw) } ?: "" + } + + private enum class PerfContextMetric(val id: Int) { + USER_KEY_COMPARISON_COUNT(0), + BLOCK_CACHE_HIT_COUNT(1), + BLOCK_READ_COUNT(2), + BLOCK_READ_BYTE(3), + BLOCK_READ_TIME(4), + BLOCK_READ_CPU_TIME(5), + BLOCK_CACHE_INDEX_HIT_COUNT(6), + BLOCK_CACHE_STANDALONE_HANDLE_COUNT(7), + BLOCK_CACHE_REAL_HANDLE_COUNT(8), + INDEX_BLOCK_READ_COUNT(9), + BLOCK_CACHE_FILTER_HIT_COUNT(10), + FILTER_BLOCK_READ_COUNT(11), + COMPRESSION_DICT_BLOCK_READ_COUNT(12), + SECONDARY_CACHE_HIT_COUNT(17), + COMPRESSED_SEC_CACHE_INSERT_REAL_COUNT(18), + COMPRESSED_SEC_CACHE_INSERT_DUMMY_COUNT(19), + COMPRESSED_SEC_CACHE_UNCOMPRESSED_BYTES(20), + COMPRESSED_SEC_CACHE_COMPRESSED_BYTES(21), + BLOCK_CHECKSUM_TIME(22), + BLOCK_DECOMPRESS_TIME(23), + GET_READ_BYTES(24), + MULTIGET_READ_BYTES(25), + ITER_READ_BYTES(26), + BLOB_CACHE_HIT_COUNT(27), + BLOB_READ_COUNT(28), + BLOB_READ_BYTE(29), + BLOB_READ_TIME(30), + BLOB_CHECKSUM_TIME(31), + BLOB_DECOMPRESS_TIME(32), + INTERNAL_KEY_SKIPPED_COUNT(33), + INTERNAL_DELETE_SKIPPED_COUNT(34), + INTERNAL_RECENT_SKIPPED_COUNT(35), + INTERNAL_MERGE_COUNT(36), + INTERNAL_MERGE_POINT_LOOKUP_COUNT(37), + INTERNAL_RANGE_DEL_RESEEK_COUNT(38), + GET_SNAPSHOT_TIME(39), + GET_FROM_MEMTABLE_TIME(40), + GET_FROM_MEMTABLE_COUNT(41), + GET_POST_PROCESS_TIME(42), + GET_FROM_OUTPUT_FILES_TIME(43), + SEEK_ON_MEMTABLE_TIME(44), + SEEK_ON_MEMTABLE_COUNT(45), + NEXT_ON_MEMTABLE_COUNT(46), + PREV_ON_MEMTABLE_COUNT(47), + SEEK_CHILD_SEEK_TIME(48), + SEEK_CHILD_SEEK_COUNT(49), + SEEK_MIN_HEAP_TIME(50), + SEEK_MAX_HEAP_TIME(51), + SEEK_INTERNAL_SEEK_TIME(52), + FIND_NEXT_USER_ENTRY_TIME(53), + WRITE_WAL_TIME(54), + WRITE_MEMTABLE_TIME(55), + WRITE_DELAY_TIME(56), + WRITE_SCHEDULING_FLUSHES_COMPACTIONS_TIME(57), + WRITE_PRE_AND_POST_PROCESS_TIME(58), + WRITE_THREAD_WAIT_NANOS(59), + DB_MUTEX_LOCK_NANOS(60), + DB_CONDITION_WAIT_NANOS(61), + MERGE_OPERATOR_TIME_NANOS(62), + READ_INDEX_BLOCK_NANOS(63), + READ_FILTER_BLOCK_NANOS(64), + NEW_TABLE_BLOCK_ITER_NANOS(65), + NEW_TABLE_ITERATOR_NANOS(66), + BLOCK_SEEK_NANOS(67), + FIND_TABLE_NANOS(68), + BLOOM_MEMTABLE_HIT_COUNT(69), + BLOOM_MEMTABLE_MISS_COUNT(70), + BLOOM_SST_HIT_COUNT(71), + BLOOM_SST_MISS_COUNT(72), + KEY_LOCK_WAIT_TIME(73), + KEY_LOCK_WAIT_COUNT(74), + ENV_NEW_SEQUENTIAL_FILE_NANOS(75), + ENV_NEW_RANDOM_ACCESS_FILE_NANOS(76), + ENV_NEW_WRITABLE_FILE_NANOS(77), + ENV_REUSE_WRITABLE_FILE_NANOS(78), + ENV_NEW_RANDOM_RW_FILE_NANOS(79), + ENV_NEW_DIRECTORY_NANOS(80), + ENV_FILE_EXISTS_NANOS(81), + ENV_GET_CHILDREN_NANOS(82), + ENV_GET_CHILDREN_FILE_ATTRIBUTES_NANOS(83), + ENV_DELETE_FILE_NANOS(84), + ENV_CREATE_DIR_NANOS(85), + ENV_CREATE_DIR_IF_MISSING_NANOS(86), + ENV_DELETE_DIR_NANOS(87), + ENV_GET_FILE_SIZE_NANOS(88), + ENV_GET_FILE_MODIFICATION_TIME_NANOS(89), + ENV_RENAME_FILE_NANOS(90), + ENV_LINK_FILE_NANOS(91), + ENV_LOCK_FILE_NANOS(92), + ENV_UNLOCK_FILE_NANOS(93), + ENV_NEW_LOGGER_NANOS(94), + GET_CPU_NANOS(95), + ITER_NEXT_CPU_NANOS(96), + ITER_PREV_CPU_NANOS(97), + ITER_SEEK_CPU_NANOS(98), + ENCRYPT_DATA_NANOS(102), + DECRYPT_DATA_NANOS(103), + NUMBER_ASYNC_SEEK(104); + + companion object { + init { + require(values().distinctBy { it.id }.size == values().size) + } + } + } +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/PerfLevel.kt b/src/nativeMain/kotlin/maryk/rocksdb/PerfLevel.kt new file mode 100644 index 0000000..8db3287 --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/PerfLevel.kt @@ -0,0 +1,17 @@ +package maryk.rocksdb + +actual enum class PerfLevel(internal val value: Byte) { + UNINITIALIZED(0), + DISABLE(1), + ENABLE_COUNT(2), + ENABLE_TIME_EXCEPT_FOR_MUTEX(3), + ENABLE_TIME_AND_CPU_TIME_EXCEPT_FOR_MUTEX(4), + ENABLE_TIME(5), + OUT_OF_BOUNDS(6); + + actual fun getValue(): Byte = value +} + +actual fun perfLevelFromValue(value: Byte): PerfLevel = + PerfLevel.values().firstOrNull { it.value == value } + ?: throw IllegalArgumentException("Unknown PerfLevel constant $value") diff --git a/src/nativeMain/kotlin/maryk/rocksdb/RateLimiter.kt b/src/nativeMain/kotlin/maryk/rocksdb/RateLimiter.kt new file mode 100644 index 0000000..8418ec1 --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/RateLimiter.kt @@ -0,0 +1,40 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package maryk.rocksdb + +import cnames.structs.rocksdb_ratelimiter_t +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import rocksdb.rocksdb_ratelimiter_bytes_per_second +import rocksdb.rocksdb_ratelimiter_create +import rocksdb.rocksdb_ratelimiter_destroy +import rocksdb.rocksdb_ratelimiter_set_bytes_per_second + +actual class RateLimiter internal constructor( + internal val native: CPointer +) : RocksObject() { + actual constructor(rateBytesPerSecond: Long) : this( + rocksdb_ratelimiter_create(rateBytesPerSecond, 1000, 10)!! + ) + + actual constructor(rateBytesPerSecond: Long, refillPeriodMicros: Long) : this( + rocksdb_ratelimiter_create(rateBytesPerSecond, refillPeriodMicros, 10)!! + ) + + actual constructor(rateBytesPerSecond: Long, refillPeriodMicros: Long, fairness: Int) : this( + rocksdb_ratelimiter_create(rateBytesPerSecond, refillPeriodMicros, fairness)!! + ) + + override fun close() { + if (isOwningHandle()) { + rocksdb_ratelimiter_destroy(native) + super.close() + } + } + + actual fun setBytesPerSecond(rateBytesPerSecond: Long) { + rocksdb_ratelimiter_set_bytes_per_second(native, rateBytesPerSecond.toULong()) + } + + actual fun getBytesPerSecond(): Long = rocksdb_ratelimiter_bytes_per_second(native).toLong() +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/RocksDB.kt b/src/nativeMain/kotlin/maryk/rocksdb/RocksDB.kt index 72c5200..e6df365 100644 --- a/src/nativeMain/kotlin/maryk/rocksdb/RocksDB.kt +++ b/src/nativeMain/kotlin/maryk/rocksdb/RocksDB.kt @@ -29,6 +29,7 @@ import maryk.wrapWithMultiErrorThrower import maryk.wrapWithNullErrorThrower import platform.posix.size_tVar import platform.posix.uint64_tVar +import rocksdb.rocksdb_cancel_all_background_work import rocksdb.rocksdb_close import rocksdb.rocksdb_column_family_metadata_get_level_count import rocksdb.rocksdb_column_family_metadata_get_level_metadata @@ -38,6 +39,7 @@ import rocksdb.rocksdb_compact_range_cf_opt import rocksdb.rocksdb_create_column_family import rocksdb.rocksdb_create_iterator import rocksdb.rocksdb_create_iterator_cf +import rocksdb.rocksdb_get_updates_since import rocksdb.rocksdb_delete import rocksdb.rocksdb_delete_cf import rocksdb.rocksdb_delete_file_in_range_cf @@ -52,6 +54,7 @@ import rocksdb.rocksdb_get_cf import rocksdb.rocksdb_get_column_family_metadata import rocksdb.rocksdb_get_default_column_family_handle import rocksdb.rocksdb_get_latest_sequence_number +import rocksdb.rocksdb_free import rocksdb.rocksdb_ingest_external_file import rocksdb.rocksdb_ingest_external_file_cf import rocksdb.rocksdb_key_may_exist @@ -63,6 +66,15 @@ import rocksdb.rocksdb_level_metadata_get_size import rocksdb.rocksdb_level_metadata_get_sst_file_metadata import rocksdb.rocksdb_list_column_families import rocksdb.rocksdb_list_column_families_destroy +import rocksdb.rocksdb_livefiles +import rocksdb.rocksdb_livefiles_column_family_name +import rocksdb.rocksdb_livefiles_count +import rocksdb.rocksdb_livefiles_destroy +import rocksdb.rocksdb_livefiles_largestkey +import rocksdb.rocksdb_livefiles_level +import rocksdb.rocksdb_livefiles_name +import rocksdb.rocksdb_livefiles_size +import rocksdb.rocksdb_livefiles_smallestkey import rocksdb.rocksdb_merge import rocksdb.rocksdb_merge_cf import rocksdb.rocksdb_multi_get @@ -76,7 +88,9 @@ import rocksdb.rocksdb_sst_file_metadata_get_directory import rocksdb.rocksdb_sst_file_metadata_get_relative_filename import rocksdb.rocksdb_sst_file_metadata_get_size import rocksdb.rocksdb_sst_file_metadata_get_smallestkey +import rocksdb.rocksdb_set_perf_level import rocksdb.rocksdb_write +import rocksdb.rocksdb_try_catch_up_with_primary import kotlin.experimental.ExperimentalNativeApi import kotlin.math.min @@ -90,6 +104,7 @@ internal constructor( : RocksObject() { private val defaultReadOptions = ReadOptions() private val defaultWriteOptions = WriteOptions() + private var perfLevel: PerfLevel = PerfLevel.UNINITIALIZED actual fun getName(): String { return rocksdb.rocksdb_get_name(native)!!.let { name -> @@ -887,9 +902,14 @@ internal constructor( errs = error, ) - buildList(keys.size) { - keys.indices.forEach { index -> - this += valueList[index]?.toByteArray(valueListSizes[index]) + List(keys.size) { index -> + val pointer = valueList[index] + if (pointer != null) { + val bytes = pointer.toByteArray(valueListSizes[index]) + rocksdb_free(pointer) + bytes + } else { + null } } } @@ -937,9 +957,14 @@ internal constructor( errs = error, ) - buildList(keys.size) { - keys.indices.forEach { index -> - this += valueList[index]?.toByteArray(valueListSizes[index]) + List(keys.size) { index -> + val pointer = valueList[index] + if (pointer != null) { + val bytes = pointer.toByteArray(valueListSizes[index]) + rocksdb_free(pointer) + bytes + } else { + null } } } @@ -1323,6 +1348,10 @@ internal constructor( } } + actual fun cancelAllBackgroundWork(waitForExit: Boolean) { + rocksdb_cancel_all_background_work(native, waitForExit.toUByte()) + } + actual fun enableAutoCompaction(columnFamilyHandles: List) { wrapWithErrorThrower { error -> memScoped { @@ -1370,6 +1399,65 @@ internal constructor( return getDefaultEnv() } + actual fun setPerfLevel(perfLevel: PerfLevel) { + rocksdb_set_perf_level(perfLevel.value.toInt()) + this.perfLevel = perfLevel + } + + actual fun getPerfLevel(): PerfLevel = perfLevel + + actual fun getPerfContext(): PerfContext = PerfContext() + + actual fun getLiveFilesMetaData(): List = memScoped { + val liveFiles = rocksdb_livefiles(native) ?: return@memScoped emptyList() + try { + val count = rocksdb_livefiles_count(liveFiles) + buildList { + repeat(count) { index -> + val cfNamePtr = rocksdb_livefiles_column_family_name(liveFiles, index) + val cfName = cfNamePtr?.toKString()?.encodeToByteArray() ?: ByteArray(0) + + val namePtr = rocksdb_livefiles_name(liveFiles, index) + val name = namePtr?.toKString() ?: "" + + val smallestLen = alloc() + val largestLen = alloc() + val smallestPtr = rocksdb_livefiles_smallestkey(liveFiles, index, smallestLen.ptr) + val largestPtr = rocksdb_livefiles_largestkey(liveFiles, index, largestLen.ptr) + + val smallestKey = smallestPtr?.toByteArray(smallestLen.value) ?: ByteArray(0) + val largestKey = largestPtr?.toByteArray(largestLen.value) ?: ByteArray(0) + + add( + LiveFileMetaData( + columnFamilyNameValue = cfName, + levelValue = rocksdb_livefiles_level(liveFiles, index), + fileName = name, + path = name, + size = rocksdb_livefiles_size(liveFiles, index).toULong(), + smallestKey = smallestKey, + largestKey = largestKey, + ) + ) + + cfNamePtr?.let { rocksdb_free(it) } + namePtr?.let { rocksdb_free(it) } + smallestPtr?.let { rocksdb_free(it) } + largestPtr?.let { rocksdb_free(it) } + } + } + } finally { + rocksdb_livefiles_destroy(liveFiles) + } + } + + actual fun getUpdatesSince(sequenceNumber: Long): TransactionLogIterator = + wrapWithErrorThrower { error -> + TransactionLogIterator( + rocksdb_get_updates_since(native, sequenceNumber.toULong(), null, error)!!, + ) + } + actual fun flushWal(sync: Boolean) { wrapWithErrorThrower { error -> rocksdb_flush_wal(native, sync.toUByte(), error) @@ -1522,6 +1610,12 @@ internal constructor( } } + actual fun tryCatchUpWithPrimary() { + wrapWithErrorThrower { error -> + rocksdb_try_catch_up_with_primary(native, error) + } + } + actual fun getDefaultColumnFamily() = ColumnFamilyHandle( rocksdb_get_default_column_family_handle(native)!! ) diff --git a/src/nativeMain/kotlin/maryk/rocksdb/SanityLevel.kt b/src/nativeMain/kotlin/maryk/rocksdb/SanityLevel.kt new file mode 100644 index 0000000..d14a52e --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/SanityLevel.kt @@ -0,0 +1,7 @@ +package maryk.rocksdb + +actual enum class SanityLevel { + NONE, + LOOSELY_COMPATIBLE, + EXACT_MATCH; +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/SstFileManager.kt b/src/nativeMain/kotlin/maryk/rocksdb/SstFileManager.kt new file mode 100644 index 0000000..d0cbdf4 --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/SstFileManager.kt @@ -0,0 +1,63 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package maryk.rocksdb + +import cnames.structs.rocksdb_sst_file_manager_t +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import maryk.toBoolean +import rocksdb.rocksdb_sst_file_manager_create +import rocksdb.rocksdb_sst_file_manager_destroy +import rocksdb.rocksdb_sst_file_manager_get_delete_rate_bytes_per_second +import rocksdb.rocksdb_sst_file_manager_get_max_trash_db_ratio +import rocksdb.rocksdb_sst_file_manager_get_total_size +import rocksdb.rocksdb_sst_file_manager_is_max_allowed_space_reached +import rocksdb.rocksdb_sst_file_manager_is_max_allowed_space_reached_including_compactions +import rocksdb.rocksdb_sst_file_manager_set_compaction_buffer_size +import rocksdb.rocksdb_sst_file_manager_set_delete_rate_bytes_per_second +import rocksdb.rocksdb_sst_file_manager_set_max_allowed_space_usage +import rocksdb.rocksdb_sst_file_manager_set_max_trash_db_ratio + +actual class SstFileManager internal constructor( + internal val native: CPointer +) : RocksObject() { + + actual constructor(env: Env) : this(rocksdb_sst_file_manager_create(env.native)!!) + + actual fun setMaxAllowedSpaceUsage(maxAllowedSpace: Long) { + rocksdb_sst_file_manager_set_max_allowed_space_usage(native, maxAllowedSpace.toULong()) + } + + actual fun setCompactionBufferSize(compactionBufferSize: Long) { + rocksdb_sst_file_manager_set_compaction_buffer_size(native, compactionBufferSize.toULong()) + } + + actual fun isMaxAllowedSpaceReached(): Boolean = + rocksdb_sst_file_manager_is_max_allowed_space_reached(native) + + actual fun isMaxAllowedSpaceReachedIncludingCompactions(): Boolean = + rocksdb_sst_file_manager_is_max_allowed_space_reached_including_compactions(native) + + actual fun getTotalSize(): Long = rocksdb_sst_file_manager_get_total_size(native).toLong() + + actual fun getDeleteRateBytesPerSecond(): Long = + rocksdb_sst_file_manager_get_delete_rate_bytes_per_second(native).toLong() + + actual fun setDeleteRateBytesPerSecond(deleteRate: Long) { + rocksdb_sst_file_manager_set_delete_rate_bytes_per_second(native, deleteRate) + } + + actual fun getMaxTrashDBRatio(): Double = + rocksdb_sst_file_manager_get_max_trash_db_ratio(native) + + actual fun setMaxTrashDBRatio(ratio: Double) { + rocksdb_sst_file_manager_set_max_trash_db_ratio(native, ratio) + } + + actual override fun close() { + if (isOwningHandle()) { + rocksdb_sst_file_manager_destroy(native) + super.close() + } + } +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/SstFileMetaData.kt b/src/nativeMain/kotlin/maryk/rocksdb/SstFileMetaData.kt index eb2a018..f144d41 100644 --- a/src/nativeMain/kotlin/maryk/rocksdb/SstFileMetaData.kt +++ b/src/nativeMain/kotlin/maryk/rocksdb/SstFileMetaData.kt @@ -1,19 +1,33 @@ package maryk.rocksdb -actual class SstFileMetaData( - val fileName: String, - val path: String, - val size: ULong, - val smallestKey: ByteArray, - val largestKey: ByteArray, -) { - actual fun fileName() = fileName +actual open class SstFileMetaData protected constructor() { + private lateinit var fileNameValue: String + private lateinit var pathValue: String + private var sizeValue: ULong = 0u + private lateinit var smallestKeyValue: ByteArray + private lateinit var largestKeyValue: ByteArray - actual fun path() = path + internal constructor( + fileName: String, + path: String, + size: ULong, + smallestKey: ByteArray, + largestKey: ByteArray, + ) : this() { + fileNameValue = fileName + pathValue = path + sizeValue = size + smallestKeyValue = smallestKey + largestKeyValue = largestKey + } - actual fun size() = size.toLong() + actual fun fileName(): String = fileNameValue - actual fun smallestKey(): ByteArray = smallestKey + actual fun path(): String = pathValue - actual fun largestKey(): ByteArray = largestKey + actual fun size(): Long = sizeValue.toLong() + + actual fun smallestKey(): ByteArray = smallestKeyValue + + actual fun largestKey(): ByteArray = largestKeyValue } diff --git a/src/nativeMain/kotlin/maryk/rocksdb/Statistics.kt b/src/nativeMain/kotlin/maryk/rocksdb/Statistics.kt index 6f6c21a..a200a19 100644 --- a/src/nativeMain/kotlin/maryk/rocksdb/Statistics.kt +++ b/src/nativeMain/kotlin/maryk/rocksdb/Statistics.kt @@ -2,43 +2,87 @@ package maryk.rocksdb import cnames.structs.rocksdb_options_t import kotlinx.cinterop.CPointer +import kotlinx.cinterop.UIntVar +import kotlinx.cinterop.alloc +import kotlinx.cinterop.allocArray +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.set +import kotlinx.cinterop.value +import kotlinx.cinterop.toKString +import maryk.toByteArray import maryk.wrapWithErrorThrower +import platform.posix.size_tVar +import rocksdb.rocksdb_free import rocksdb.rocksdb_options_get_statistics_level import rocksdb.rocksdb_options_set_statistics_level +import rocksdb.rocksdb_options_statistics_get_and_reset_ticker_count import rocksdb.rocksdb_options_statistics_get_histogram_data +import rocksdb.rocksdb_options_statistics_get_histogram_string +import rocksdb.rocksdb_options_statistics_get_string import rocksdb.rocksdb_options_statistics_get_ticker_count +import rocksdb.rocksdb_options_statistics_set_histograms +import rocksdb.rocksdb_options_enable_statistics import rocksdb.rocksdb_statistics_histogram_data_create actual class Statistics internal constructor( - internal var native: CPointer? + internal var native: CPointer?, + private val enabledHistograms: Set, ) : RocksObject() { - actual constructor() : this(native = null) + actual constructor() : this(native = null, enabledHistograms = emptySet()) + + constructor(enabledHistograms: Set) : this( + native = null, + enabledHistograms = enabledHistograms + ) private var statsLevel: StatsLevel? = null internal fun connectWithNative(native: CPointer) { this.native = native + rocksdb_options_enable_statistics(native) + + if (enabledHistograms.isNotEmpty()) { + memScoped { + val histogramArray = allocArray(enabledHistograms.size) + enabledHistograms.forEachIndexed { index, histogramType -> + histogramArray[index] = histogramType.value + } + rocksdb_options_statistics_set_histograms( + native, + histogramArray, + enabledHistograms.size.toULong() + ) + } + } + statsLevel?.let { level -> setStatsLevel(level) } } actual fun statsLevel(): StatsLevel? = native?.let { - getStatsLevel(rocksdb_options_get_statistics_level(native).toUByte()) + getStatsLevel(rocksdb_options_get_statistics_level(it).toUByte()) } ?: statsLevel actual fun setStatsLevel(statsLevel: StatsLevel) { - if (native != null) { - rocksdb_options_set_statistics_level(native, statsLevel.value.toInt()) - } else { - statsLevel - } + this.statsLevel = statsLevel + native?.let { rocksdb_options_set_statistics_level(it, statsLevel.value.toInt()) } } actual fun getTickerCount(tickerType: TickerType): Long { - return rocksdb_options_statistics_get_ticker_count(native, tickerType.value).toLong() + val attachedNative = requireNative() + return rocksdb_options_statistics_get_ticker_count(attachedNative, tickerType.value).toLong() + } + + actual fun getAndResetTickerCount(tickerType: TickerType): Long { + val attachedNative = requireNative() + return rocksdb_options_statistics_get_and_reset_ticker_count(attachedNative, tickerType.value) + .toLong() } - actual fun getHistogramData(histogramType: HistogramType) :HistogramData { + actual fun getHistogramData(histogramType: HistogramType): HistogramData { + val attachedNative = requireNative() val histogramData = rocksdb_statistics_histogram_data_create() + rocksdb_options_statistics_get_histogram_data(attachedNative, histogramType.value, histogramData) return HistogramData( median = rocksdb.rocksdb_statistics_histogram_data_get_median(histogramData), p95 = rocksdb.rocksdb_statistics_histogram_data_get_p95(histogramData), @@ -54,9 +98,40 @@ actual class Statistics internal constructor( } } + actual fun getHistogramString(histogramType: HistogramType): String { + val attachedNative = requireNative() + return memScoped { + val lengthVar = alloc() + val raw = rocksdb_options_statistics_get_histogram_string( + attachedNative, + histogramType.value, + lengthVar.ptr + ) + raw?.let { + val stringValue = it.toByteArray(lengthVar.value).decodeToString() + rocksdb_free(it) + stringValue + } ?: "" + } + } + actual fun reset() { - wrapWithErrorThrower { error -> - rocksdb.rocksdb_options_statistics_reset(native, error) + native?.let { attached -> + wrapWithErrorThrower { error -> + rocksdb.rocksdb_options_statistics_reset(attached, error) + } } } + + actual override fun toString(): String { + val attachedNative = requireNative() + val raw = rocksdb_options_statistics_get_string(attachedNative) + return raw?.toKString().also { raw?.let { rocksdb_free(it) } } ?: "" + } + + private fun requireNative(): CPointer = + native ?: error("Statistics must be attached to Options before use") } + +actual fun createStatistics(enabledHistograms: Set): Statistics = + Statistics(enabledHistograms) diff --git a/src/nativeMain/kotlin/maryk/rocksdb/StatisticsCollector.kt b/src/nativeMain/kotlin/maryk/rocksdb/StatisticsCollector.kt new file mode 100644 index 0000000..8719d4e --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/StatisticsCollector.kt @@ -0,0 +1,170 @@ +@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) + +package maryk.rocksdb + +import cnames.structs.rocksdb_options_t +import kotlinx.cinterop.CPointer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.coroutines.yield +import rocksdb.rocksdb_options_statistics_get_histogram_data +import rocksdb.rocksdb_options_statistics_get_ticker_count +import rocksdb.rocksdb_statistics_histogram_data_create +import rocksdb.rocksdb_statistics_histogram_data_destroy +import rocksdb.rocksdb_statistics_histogram_data_get_average +import rocksdb.rocksdb_statistics_histogram_data_get_count +import rocksdb.rocksdb_statistics_histogram_data_get_max +import rocksdb.rocksdb_statistics_histogram_data_get_median +import rocksdb.rocksdb_statistics_histogram_data_get_min +import rocksdb.rocksdb_statistics_histogram_data_get_p95 +import rocksdb.rocksdb_statistics_histogram_data_get_p99 +import rocksdb.rocksdb_statistics_histogram_data_get_std_dev +import rocksdb.rocksdb_statistics_histogram_data_get_sum +import kotlin.concurrent.AtomicInt +import kotlin.concurrent.AtomicReference + +actual class StatisticsCollector actual constructor( + statsCollectorInputList: List, + statsCollectionIntervalInMilliSeconds: Int +) { + private val intervalMillis = statsCollectionIntervalInMilliSeconds.coerceAtLeast(0) + private val running = AtomicInt(0) + private val completed = AtomicInt(0) + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private val jobRef = AtomicReference(null) + + private val state: NativeCollectorState + + init { + require(statsCollectorInputList.isNotEmpty()) { "Statistics collector inputs must not be empty" } + val inputs = statsCollectorInputList.map { input -> + val nativeStatistics = input.statistics.native + ?: error("Statistics must be attached to Options before starting collection") + NativeCollectorInput(nativeStatistics, input.callback) + } + state = NativeCollectorState(inputs, intervalMillis, running, completed) + } + + actual fun start() { + if (!running.compareAndSet(0, 1)) { + return + } + completed.value = 0 + val collectorState = state + val job = scope.launch { + try { + collectorState.run() + } finally { + running.value = 0 + } + } + jobRef.value = job + job.invokeOnCompletion { + jobRef.compareAndSet(job, null) + } + } + + actual fun shutDown(shutdownTimeout: Int) { + if (!running.compareAndSet(1, 0)) { + return + } + val job = jobRef.value + if (job != null) { + jobRef.compareAndSet(job, null) + job.cancel() + val waitMillis = shutdownTimeout.coerceAtLeast(0).toLong() + runBlocking { + if (waitMillis <= 0) { + job.join() + } else { + withTimeoutOrNull(waitMillis) { job.join() } + } + } + } + completed.value = 0 + } +} + +private class NativeCollectorState( + private val inputs: List, + private val intervalMillis: Int, + private val running: AtomicInt, + private val completed: AtomicInt +) { + suspend fun run() { + try { + while (running.value == 1 && currentCoroutineContext().isActive) { + collectOnce() + if (running.value != 1 || !currentCoroutineContext().isActive) { + break + } + if (intervalMillis > 0) { + delay(intervalMillis.toLong()) + } else { + yield() + } + } + } finally { + completed.value = 1 + running.value = 0 + } + } + + private fun collectOnce() { + for (input in inputs) { + val statistics = input.statistics + val callback = input.callback + + for (ticker in TickerType.entries) { + if (ticker != TickerType.TICKER_ENUM_MAX) { + val tickerValue = rocksdb_options_statistics_get_ticker_count(statistics, ticker.value) + .toLong() + callback.tickerCallback(ticker, tickerValue) + } + } + + for (histogram in HistogramType.entries) { + if (histogram != HistogramType.HISTOGRAM_ENUM_MAX) { + val histogramData = readHistogram(statistics, histogram) + callback.histogramCallback(histogram, histogramData) + } + } + } + } + + private fun readHistogram( + statistics: CPointer, + histogram: HistogramType + ): HistogramData { + val histogramData = rocksdb_statistics_histogram_data_create() + try { + rocksdb_options_statistics_get_histogram_data(statistics, histogram.value, histogramData) + return HistogramData( + median = rocksdb_statistics_histogram_data_get_median(histogramData), + p95 = rocksdb_statistics_histogram_data_get_p95(histogramData), + p99 = rocksdb_statistics_histogram_data_get_p99(histogramData), + average = rocksdb_statistics_histogram_data_get_average(histogramData), + stdDev = rocksdb_statistics_histogram_data_get_std_dev(histogramData), + max = rocksdb_statistics_histogram_data_get_max(histogramData), + count = rocksdb_statistics_histogram_data_get_count(histogramData), + sum = rocksdb_statistics_histogram_data_get_sum(histogramData), + min = rocksdb_statistics_histogram_data_get_min(histogramData) + ) + } finally { + rocksdb_statistics_histogram_data_destroy(histogramData) + } + } +} + +private data class NativeCollectorInput( + val statistics: CPointer, + val callback: StatisticsCollectorCallback +) diff --git a/src/nativeMain/kotlin/maryk/rocksdb/TableProperties.kt b/src/nativeMain/kotlin/maryk/rocksdb/TableProperties.kt new file mode 100644 index 0000000..76ea031 --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/TableProperties.kt @@ -0,0 +1,142 @@ +package maryk.rocksdb + +import cnames.structs.rocksdb_tableproperties_t +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.value +import maryk.toByteArray +import platform.posix.size_tVar +import rocksdb.rocksdb_tableproperties_column_family_id +import rocksdb.rocksdb_tableproperties_column_family_name +import rocksdb.rocksdb_tableproperties_comparator_name +import rocksdb.rocksdb_tableproperties_compression_name +import rocksdb.rocksdb_tableproperties_creation_time +import rocksdb.rocksdb_tableproperties_data_size +import rocksdb.rocksdb_tableproperties_fast_compression_estimated_data_size +import rocksdb.rocksdb_tableproperties_filter_policy_name +import rocksdb.rocksdb_tableproperties_filter_size +import rocksdb.rocksdb_tableproperties_format_version +import rocksdb.rocksdb_tableproperties_index_size +import rocksdb.rocksdb_tableproperties_merge_operator_name +import rocksdb.rocksdb_tableproperties_num_data_blocks +import rocksdb.rocksdb_tableproperties_num_deletions +import rocksdb.rocksdb_tableproperties_num_entries +import rocksdb.rocksdb_tableproperties_num_merge_operands +import rocksdb.rocksdb_tableproperties_num_range_deletions +import rocksdb.rocksdb_tableproperties_oldest_key_time +import rocksdb.rocksdb_tableproperties_prefix_extractor_name +import rocksdb.rocksdb_tableproperties_property_collectors_names +import rocksdb.rocksdb_tableproperties_raw_key_size +import rocksdb.rocksdb_tableproperties_raw_value_size +import rocksdb.rocksdb_tableproperties_slow_compression_estimated_data_size + +actual class TableProperties internal constructor( + private val dataSizeValue: Long, + private val indexSizeValue: Long, + private val filterSizeValue: Long, + private val rawKeySizeValue: Long, + private val rawValueSizeValue: Long, + private val numDataBlocksValue: Long, + private val numEntriesValue: Long, + private val numDeletionsValue: Long, + private val numMergeOperandsValue: Long, + private val numRangeDeletionsValue: Long, + private val formatVersionValue: Long, + private val columnFamilyIdValue: Long, + private val columnFamilyNameValue: String?, + private val creationTimeValue: Long, + private val oldestKeyTimeValue: Long, + private val slowCompressionEstimatedDataSizeValue: Long, + private val fastCompressionEstimatedDataSizeValue: Long, + private val filterPolicyNameValue: String?, + private val comparatorNameValue: String?, + private val mergeOperatorNameValue: String?, + private val prefixExtractorNameValue: String?, + private val propertyCollectorsNamesValue: String?, + private val compressionNameValue: String?, +) { + internal constructor(native: CPointer?) : this( + dataSizeValue = rocksdb_tableproperties_data_size(native).toLong(), + indexSizeValue = rocksdb_tableproperties_index_size(native).toLong(), + filterSizeValue = rocksdb_tableproperties_filter_size(native).toLong(), + rawKeySizeValue = rocksdb_tableproperties_raw_key_size(native).toLong(), + rawValueSizeValue = rocksdb_tableproperties_raw_value_size(native).toLong(), + numDataBlocksValue = rocksdb_tableproperties_num_data_blocks(native).toLong(), + numEntriesValue = rocksdb_tableproperties_num_entries(native).toLong(), + numDeletionsValue = rocksdb_tableproperties_num_deletions(native).toLong(), + numMergeOperandsValue = rocksdb_tableproperties_num_merge_operands(native).toLong(), + numRangeDeletionsValue = rocksdb_tableproperties_num_range_deletions(native).toLong(), + formatVersionValue = rocksdb_tableproperties_format_version(native).toLong(), + columnFamilyIdValue = rocksdb_tableproperties_column_family_id(native).toLong(), + columnFamilyNameValue = memScoped { + val length = alloc() + rocksdb_tableproperties_column_family_name(native, length.ptr)?.toByteArray(length.value) + ?.decodeToString() + }, + creationTimeValue = rocksdb_tableproperties_creation_time(native).toLong(), + oldestKeyTimeValue = rocksdb_tableproperties_oldest_key_time(native).toLong(), + slowCompressionEstimatedDataSizeValue = + rocksdb_tableproperties_slow_compression_estimated_data_size(native).toLong(), + fastCompressionEstimatedDataSizeValue = + rocksdb_tableproperties_fast_compression_estimated_data_size(native).toLong(), + filterPolicyNameValue = memScoped { + val length = alloc() + rocksdb_tableproperties_filter_policy_name(native, length.ptr)?.toByteArray(length.value) + ?.decodeToString() + }, + comparatorNameValue = memScoped { + val length = alloc() + rocksdb_tableproperties_comparator_name(native, length.ptr)?.toByteArray(length.value) + ?.decodeToString() + }, + mergeOperatorNameValue = memScoped { + val length = alloc() + rocksdb_tableproperties_merge_operator_name(native, length.ptr)?.toByteArray(length.value) + ?.decodeToString() + }, + prefixExtractorNameValue = memScoped { + val length = alloc() + rocksdb_tableproperties_prefix_extractor_name(native, length.ptr)?.toByteArray(length.value) + ?.decodeToString() + }, + propertyCollectorsNamesValue = memScoped { + val length = alloc() + rocksdb_tableproperties_property_collectors_names(native, length.ptr) + ?.toByteArray(length.value) + ?.decodeToString() + }, + compressionNameValue = memScoped { + val length = alloc() + rocksdb_tableproperties_compression_name(native, length.ptr)?.toByteArray(length.value) + ?.decodeToString() + }, + ) + + actual fun dataSize(): Long = dataSizeValue + actual fun indexSize(): Long = indexSizeValue + actual fun filterSize(): Long = filterSizeValue + actual fun rawKeySize(): Long = rawKeySizeValue + actual fun rawValueSize(): Long = rawValueSizeValue + actual fun numDataBlocks(): Long = numDataBlocksValue + actual fun numEntries(): Long = numEntriesValue + actual fun numDeletions(): Long = numDeletionsValue + actual fun numMergeOperands(): Long = numMergeOperandsValue + actual fun numRangeDeletions(): Long = numRangeDeletionsValue + actual fun formatVersion(): Long = formatVersionValue + actual fun columnFamilyId(): Long = columnFamilyIdValue + actual fun columnFamilyName(): String? = columnFamilyNameValue + actual fun creationTime(): Long = creationTimeValue + actual fun oldestKeyTime(): Long = oldestKeyTimeValue + actual fun slowCompressionEstimatedDataSize(): Long = + slowCompressionEstimatedDataSizeValue + actual fun fastCompressionEstimatedDataSize(): Long = + fastCompressionEstimatedDataSizeValue + actual fun filterPolicyName(): String? = filterPolicyNameValue + actual fun comparatorName(): String? = comparatorNameValue + actual fun mergeOperatorName(): String? = mergeOperatorNameValue + actual fun prefixExtractorName(): String? = prefixExtractorNameValue + actual fun propertyCollectorsNames(): String? = propertyCollectorsNamesValue + actual fun compressionName(): String? = compressionNameValue +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/TransactionLogIterator.kt b/src/nativeMain/kotlin/maryk/rocksdb/TransactionLogIterator.kt new file mode 100644 index 0000000..c692f1b --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/TransactionLogIterator.kt @@ -0,0 +1,58 @@ +package maryk.rocksdb + +import cnames.structs.rocksdb_wal_iterator_t +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.value +import maryk.toBoolean +import maryk.wrapWithErrorThrower +import platform.posix.uint64_tVar +import rocksdb.rocksdb_wal_iter_destroy +import rocksdb.rocksdb_wal_iter_get_batch +import rocksdb.rocksdb_wal_iter_next +import rocksdb.rocksdb_wal_iter_status +import rocksdb.rocksdb_wal_iter_valid + +@OptIn(ExperimentalForeignApi::class) +actual class TransactionLogIterator internal constructor( + internal val native: CPointer, +) : RocksObject() { + actual fun isValid(): Boolean = rocksdb_wal_iter_valid(native).toBoolean() + + actual fun next() { + rocksdb_wal_iter_next(native) + } + + @Throws(RocksDBException::class) + actual fun status() { + wrapWithErrorThrower { error -> + rocksdb_wal_iter_status(native, error) + } + } + + actual fun getBatch(): TransactionLogBatchResult = memScoped { + val sequenceNumber = alloc() + val batchPointer = rocksdb_wal_iter_get_batch(native, sequenceNumber.ptr)!! + val batch = WriteBatch(batchPointer).also { it.disownHandle() } + TransactionLogBatchResult(sequenceNumber.value.toLong(), batch) + } + + override fun close() { + if (isOwningHandle()) { + rocksdb_wal_iter_destroy(native) + super.close() + } + } +} + +actual class TransactionLogBatchResult internal constructor( + private val sequenceNumberValue: Long, + private val writeBatchValue: WriteBatch, +) { + actual fun sequenceNumber(): Long = sequenceNumberValue + + actual fun writeBatch(): WriteBatch = writeBatchValue +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/WALRecoveryMode.kt b/src/nativeMain/kotlin/maryk/rocksdb/WALRecoveryMode.kt new file mode 100644 index 0000000..f1797ac --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/WALRecoveryMode.kt @@ -0,0 +1,18 @@ +package maryk.rocksdb + +actual enum class WALRecoveryMode(private val value: Byte) { + TolerateCorruptedTailRecords(0), + AbsoluteConsistency(1), + PointInTimeRecovery(2), + SkipAnyCorruptedRecords(3); + + actual fun getValue(): Byte = value +} + +actual fun walRecoveryModeFromValue(value: Byte): WALRecoveryMode = when (value.toInt()) { + 0 -> WALRecoveryMode.TolerateCorruptedTailRecords + 1 -> WALRecoveryMode.AbsoluteConsistency + 2 -> WALRecoveryMode.PointInTimeRecovery + 3 -> WALRecoveryMode.SkipAnyCorruptedRecords + else -> throw IllegalArgumentException("Unknown WALRecoveryMode value $value") +} diff --git a/src/nativeMain/kotlin/maryk/rocksdb/WriteBufferManager.kt b/src/nativeMain/kotlin/maryk/rocksdb/WriteBufferManager.kt new file mode 100644 index 0000000..c42f715 --- /dev/null +++ b/src/nativeMain/kotlin/maryk/rocksdb/WriteBufferManager.kt @@ -0,0 +1,40 @@ +package maryk.rocksdb + +import cnames.structs.rocksdb_write_buffer_manager_t +import kotlinx.cinterop.CPointer +import rocksdb.rocksdb_write_buffer_manager_create_with_cache +import rocksdb.rocksdb_write_buffer_manager_destroy + +actual class WriteBufferManager internal constructor( + internal val native: CPointer, + private val allowStallFlag: Boolean, +) : RocksObject() { + actual constructor( + bufferSize: Long, + cache: Cache, + allowStall: Boolean, + ) : this( + requireNotNull( + rocksdb_write_buffer_manager_create_with_cache( + bufferSize.toULong(), + cache.native, + allowStall, + ), + ), + allowStall, + ) + + actual constructor( + bufferSize: Long, + cache: Cache, + ) : this(bufferSize, cache, false) + + actual fun allowStall(): Boolean = allowStallFlag + + override fun close() { + if (isOwningHandle()) { + rocksdb_write_buffer_manager_destroy(native) + super.close() + } + } +} diff --git a/src/nativeTest/kotlin/maryk/rocksdb/util/Sleep.kt b/src/nativeTest/kotlin/maryk/rocksdb/util/Sleep.kt new file mode 100644 index 0000000..eb5caba --- /dev/null +++ b/src/nativeTest/kotlin/maryk/rocksdb/util/Sleep.kt @@ -0,0 +1,16 @@ +package maryk.rocksdb.util + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.TimeSource + +actual fun sleepMillis(millis: Long) { + if (millis <= 0L) { + return + } + val timeout: Duration = millis.milliseconds + val timer = TimeSource.Monotonic.markNow() + while (timer.elapsedNow() < timeout) { + // Busy wait placeholder for native sleep implementation. + } +} diff --git a/src/nativeTest/kotlin/maryk/rocksdb/util/ThreadSafeCounter.kt b/src/nativeTest/kotlin/maryk/rocksdb/util/ThreadSafeCounter.kt new file mode 100644 index 0000000..9d0b617 --- /dev/null +++ b/src/nativeTest/kotlin/maryk/rocksdb/util/ThreadSafeCounter.kt @@ -0,0 +1,16 @@ +package maryk.rocksdb.util + +import kotlin.concurrent.atomics.AtomicInt +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.concurrent.atomics.incrementAndFetch + +@OptIn(ExperimentalAtomicApi::class) +actual class ThreadSafeCounter actual constructor() { + private val counter = AtomicInt(0) + + actual fun increment() { + counter.incrementAndFetch() + } + + actual fun value(): Int = counter.load() +}