├── .circleci └── config.yml ├── .deepsource.toml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── README.md ├── benchmark.rb ├── image.png ├── lib ├── bitcask.rb └── bitcask │ ├── disk_store.rb │ └── serializer.rb └── spec ├── bitcask ├── disk_store_spec.rb └── serializer_spec.rb ├── fixtures ├── 1672042848_bitcask.db └── 1672070284_bitcask.db └── spec_helper.rb /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | codecov: codecov/codecov@3 4 | 5 | jobs: 6 | build: 7 | docker: 8 | - image: cimg/ruby:3.1 9 | steps: 10 | - checkout 11 | - run: 12 | name: Install dependencies 13 | command: bundle install 14 | - run: 15 | name: Run tests and collect coverage 16 | command: bundle exec rspec 17 | - codecov/upload 18 | 19 | workflow: 20 | version: 2.1 21 | build-test: 22 | jobs: 23 | - build -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = ["spec/**"] 4 | 5 | [[analyzers]] 6 | name = "ruby" 7 | enabled = true 8 | 9 | [[transformers]] 10 | name = "rubocop" 11 | enabled = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | # Ignore Byebug command history file. 17 | .byebug_history 18 | 19 | ## Specific to RubyMotion: 20 | .dat* 21 | .repl_history 22 | build/ 23 | *.bridgesupport 24 | build-iPhoneOS/ 25 | build-iPhoneSimulator/ 26 | 27 | ## Specific to RubyMotion (use of CocoaPods): 28 | # 29 | # We recommend against adding the Pods directory to your .gitignore. However 30 | # you should judge for yourself, the pros and cons are mentioned at: 31 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 32 | # 33 | # vendor/Pods/ 34 | 35 | ## Documentation cache and generated files: 36 | /.yardoc/ 37 | /_yardoc/ 38 | /doc/ 39 | /rdoc/ 40 | 41 | ## Environment normalization: 42 | /.bundle/ 43 | /vendor/bundle 44 | /lib/bundler/man/ 45 | 46 | # for a library or gem, you might want to ignore these files since the code is 47 | # intended to run in multiple environments; otherwise, check them in: 48 | # Gemfile.lock 49 | # .ruby-version 50 | # .ruby-gemset 51 | 52 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 53 | .rvmrc 54 | 55 | # Used by RuboCop. Remote config files pulled in from inherit_from directive. 56 | # .rubocop-https?--* 57 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-rspec 3 | 4 | AllCops: 5 | NewCops: enable 6 | Exclude: 7 | - vendor/**/* 8 | - bin/** 9 | TargetRubyVersion: 3.1.1 10 | 11 | RSpec: 12 | Enabled: true # enable rubocop-rspec cops 13 | 14 | Bundler/OrderedGems: 15 | Enabled: true 16 | 17 | Layout/ArrayAlignment: 18 | EnforcedStyle: with_first_element 19 | Enabled: true 20 | 21 | Layout/ArgumentAlignment: 22 | EnforcedStyle: with_first_argument 23 | Enabled: true 24 | 25 | Layout/ClassStructure: 26 | Enabled: true 27 | ExpectedOrder: 28 | - module_inclusion 29 | - constants 30 | - initializer 31 | - public_methods 32 | - protected_methods 33 | - private_methods 34 | 35 | Layout/MultilineMethodCallIndentation: 36 | EnforcedStyle: aligned 37 | Enabled: true 38 | 39 | Layout/ParameterAlignment: 40 | EnforcedStyle: with_first_parameter 41 | Enabled: true 42 | 43 | RSpec/ExampleLength: 44 | Max: 15 45 | CountAsOne: 46 | - array 47 | - hash 48 | - heredoc 49 | 50 | RSpec/MessageSpies: 51 | EnforcedStyle: receive 52 | Enabled: true 53 | 54 | RSpec/MultipleMemoizedHelpers: 55 | Max: 20 56 | 57 | RSpec/MultipleExpectations: 58 | Enabled: false 59 | 60 | RSpec/NamedSubject: 61 | Enabled: false 62 | 63 | RSpec/NestedGroups: 64 | Enabled: true 65 | Max: 8 66 | 67 | Style/BlockDelimiters: 68 | Enabled: true 69 | EnforcedStyle: semantic 70 | Exclude: 71 | - spec/factories/** 72 | - Gemfile 73 | 74 | Style/HashSyntax: 75 | EnforcedShorthandSyntax: never 76 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 6 | 7 | gem 'zlib', '~> 3.0' 8 | 9 | group :development, :test do 10 | gem 'benchmark-ips', '~> 2.10' 11 | gem 'faker', '~> 3.1' 12 | gem 'rspec', '~> 3.12' 13 | end 14 | 15 | group :test do 16 | gem 'byebug', '~> 11.1' 17 | gem 'rubocop', '~> 1.41', require: false 18 | gem 'rubocop-rspec', '~> 2.16', require: false 19 | gem 'simplecov', require: false 20 | gem 'simplecov-cobertura', '~> 2.1', require: false 21 | end 22 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | benchmark-ips (2.10.0) 6 | byebug (11.1.3) 7 | concurrent-ruby (1.1.10) 8 | diff-lcs (1.5.0) 9 | docile (1.4.0) 10 | faker (3.1.0) 11 | i18n (>= 1.8.11, < 2) 12 | i18n (1.12.0) 13 | concurrent-ruby (~> 1.0) 14 | json (2.6.3) 15 | parallel (1.22.1) 16 | parser (3.1.3.0) 17 | ast (~> 2.4.1) 18 | rainbow (3.1.1) 19 | regexp_parser (2.6.1) 20 | rexml (3.2.5) 21 | rspec (3.12.0) 22 | rspec-core (~> 3.12.0) 23 | rspec-expectations (~> 3.12.0) 24 | rspec-mocks (~> 3.12.0) 25 | rspec-core (3.12.0) 26 | rspec-support (~> 3.12.0) 27 | rspec-expectations (3.12.1) 28 | diff-lcs (>= 1.2.0, < 2.0) 29 | rspec-support (~> 3.12.0) 30 | rspec-mocks (3.12.1) 31 | diff-lcs (>= 1.2.0, < 2.0) 32 | rspec-support (~> 3.12.0) 33 | rspec-support (3.12.0) 34 | rubocop (1.41.1) 35 | json (~> 2.3) 36 | parallel (~> 1.10) 37 | parser (>= 3.1.2.1) 38 | rainbow (>= 2.2.2, < 4.0) 39 | regexp_parser (>= 1.8, < 3.0) 40 | rexml (>= 3.2.5, < 4.0) 41 | rubocop-ast (>= 1.23.0, < 2.0) 42 | ruby-progressbar (~> 1.7) 43 | unicode-display_width (>= 1.4.0, < 3.0) 44 | rubocop-ast (1.24.0) 45 | parser (>= 3.1.1.0) 46 | rubocop-rspec (2.16.0) 47 | rubocop (~> 1.33) 48 | ruby-progressbar (1.11.0) 49 | simplecov (0.21.2) 50 | docile (~> 1.1) 51 | simplecov-html (~> 0.11) 52 | simplecov_json_formatter (~> 0.1) 53 | simplecov-cobertura (2.1.0) 54 | rexml 55 | simplecov (~> 0.19) 56 | simplecov-html (0.12.3) 57 | simplecov_json_formatter (0.1.4) 58 | unicode-display_width (2.3.0) 59 | zlib (3.0.0) 60 | 61 | PLATFORMS 62 | ruby 63 | 64 | DEPENDENCIES 65 | benchmark-ips (~> 2.10) 66 | byebug (~> 11.1) 67 | faker (~> 3.1) 68 | rspec (~> 3.12) 69 | rubocop (~> 1.41) 70 | rubocop-rspec (~> 2.16) 71 | simplecov 72 | simplecov-cobertura (~> 2.1) 73 | zlib (~> 3.0) 74 | 75 | BUNDLED WITH 76 | 2.2.3 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dinesh Gowda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bitcask-rb: A Log-Structured Hash Table for Fast Key/Value Data 2 | 3 | [![Ruby](https://img.shields.io/badge/ruby-3.1.1-brightgreen)](https://www.ruby-lang.org/en/) 4 | [![BDD](https://img.shields.io/badge/rspec-3.1-green)](https://rspec.info/) 5 | [![Ruby Style Guide](https://img.shields.io/badge/code%20style-rubocop-red)](https://github.com/rubocop/rubocop) 6 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/dineshgowda24/bitcask-rb/tree/main.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/dineshgowda24/bitcask-rb/tree/main) 7 | [![codecov](https://codecov.io/gh/dineshgowda24/bitcask-rb/branch/main/graph/badge.svg?token=HY8IQSEKCA)](https://codecov.io/gh/dineshgowda24/bitcask-rb) 8 | [![DeepSource](https://deepsource.io/gh/dineshgowda24/bitcask-rb.svg/?label=active+issues&token=aISrLFG-Rwka_9MMiZixX_NT)](https://deepsource.io/gh/dineshgowda24/bitcask-rb/?ref=repository-badge) 9 | 10 | 11 | 12 | Fast, Persistent key/value store based on [bitcask paper](https://riak.com/assets/bitcask-intro.pdf) written in Ruby. 13 | An attempt to understand and build our persistent key/value store capable of storing data enormous than the RAM. This, in any way, is not intended for production. For simplicity, implementation has ignored a few specifications from the paper. 14 | 15 | ## Prerequisites 16 | 17 | - Ruby 18 | - bundler 19 | 20 | ## Setup 21 | 22 | ```shell 23 | bundle install 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```ruby 29 | db_store = Bitcask::DiskStore.new('bitcask.db') 30 | 31 | # Setting values in store using put or hash style 32 | db_store.put("Anime", "One Piece") 33 | db_store["Luffy"] = "Straw Hat" 34 | db_store[2020] = "Leap Year" 35 | db_store.put("pie", 3.1415) 36 | 37 | # Getting values from store using get or hash style 38 | db_store["Anime"] 39 | db_store.get("Luffy") 40 | db_store["pie"] 41 | db_store.get(2020) 42 | 43 | # Listing keys 44 | db_store.keys 45 | 46 | # Size of the store 47 | db_store.store 48 | ``` 49 | 50 | ## Tests 51 | 52 | ```shell 53 | bundle exec rspec 54 | ``` 55 | 56 | ## Advantages 57 | 58 | - Simple yet powerful 59 | - Easy to use 60 | - Store data enormous than RAM 61 | - Data file is OS agnostic 62 | - Portable 63 | - Faster read/writes 64 | - Minimal dependecies 65 | 66 | ## Features 67 | 68 | | Feature | Support | 69 | |---------------------------------------|--------------------| 70 | | Persisted in disk | :white_check_mark: | 71 | | Get API | :white_check_mark: | 72 | | Put API | :white_check_mark: | 73 | | Int, Float and String for k/v | :white_check_mark: | 74 | | CRC | :white_check_mark: | 75 | | Directory Support | :x: | 76 | | Delete API | :x: | 77 | | Files Merge and LSM Trees | :x: | 78 | 79 | ## Benchmarks 80 | 81 | ### CPU Cycles 82 | 83 | ```ruby 84 | Benchmarked with value_size of 78 bytes 85 | user system total real 86 | DiskStore.put : 10k records 0.026963 0.018983 0.045946 ( 0.046078) 87 | DiskStore.get : 10k records 0.031211 0.009727 0.040938 ( 0.041154) 88 | DiskStore.put : 100k records 0.367399 0.196536 0.563935 ( 0.566659) 89 | DiskStore.get : 100k records 0.313556 0.102338 0.415894 ( 0.416280) 90 | DiskStore.put : 1M records 4.649307 2.209731 6.859038 ( 6.887667) 91 | DiskStore.get : 1M records 3.357120 1.047637 4.404757 ( 4.409732) 92 | avg_put: 0.000005 0.000002 0.000007 ( 0.000007) 93 | avg_get: 0.000003 0.000001 0.000004 ( 0.000004) 94 | ``` 95 | 96 | ### Iterations Per Second 97 | 98 | ```ruby 99 | Warming up -------------------------------------- 100 | DiskStore.put : 100 records with data size: 702 Bytes 101 | 149.000 i/100ms 102 | DiskStore.get : 100 records, value size: 702 Bytes 103 | 2.389k i/100ms 104 | DiskStore.put : 100 records, value size: 6 Kb 105 | 22.000 i/100ms 106 | DiskStore.get : 100 records, value size: 6 Kb 107 | 2.098k i/100ms 108 | DiskStore.put : 100 records, value size: 660 Kb 109 | 1.000 i/100ms 110 | DiskStore.get : 100 records, value size: 660 Kb 111 | 148.000 i/100ms 112 | Calculating ------------------------------------- 113 | DiskStore.put : 100 records with data size: 702 Bytes 114 | 1.552k (±15.7%) i/s - 7.450k in 5.008519s 115 | DiskStore.get : 100 records, value size: 702 Bytes 116 | 24.100k (± 3.7%) i/s - 121.839k in 5.062195s 117 | DiskStore.put : 100 records, value size: 6 Kb 118 | 655.280 (±53.4%) i/s - 1.716k in 5.272456s 119 | DiskStore.get : 100 records, value size: 6 Kb 120 | 19.883k (± 7.1%) i/s - 100.704k in 5.090910s 121 | DiskStore.put : 100 records, value size: 660 Kb 122 | 4.479 (± 0.0%) i/s - 23.000 in 5.166651s 123 | DiskStore.get : 100 records, value size: 660 Kb 124 | 3.286k (±39.4%) i/s - 8.140k in 5.272424s 125 | ``` 126 | 127 | ## Contributing 128 | 129 | If you wish to contribute in any way, please fork the repo and raise a PR. 130 | 131 | [![CircleCI](https://dl.circleci.com/insights-snapshot/gh/dineshgowda24/bitcask-rb/main/workflow/badge.svg?window=30d)](https://app.circleci.com/insights/github/dineshgowda24/bitcask-rb/workflows/workflow/overview?branch=main&reporting-window=last-30-days&insights-snapshot=true) -------------------------------------------------------------------------------- /benchmark.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'benchmark' 4 | require 'benchmark/ips' 5 | require 'faker' 6 | require_relative 'lib/bitcask' 7 | 8 | include Benchmark 9 | 10 | disk_store = Bitcask::DiskStore.new('bitcask_benchmark.db') 11 | value = Faker::Lorem.sentence(word_count: 10) 12 | 13 | puts "Benchmarked with value_size of #{value.length} bytes" 14 | 15 | Benchmark.benchmark(CAPTION, 50, FORMAT, 'avg_put:', 'avg_get:') do |benchmark| 16 | tt_put_10k = benchmark.report('DiskStore.put : 10k records') { 17 | 10_000.times do |n_time| 18 | disk_store.put("10_000#{n_time}", value) 19 | end 20 | } 21 | 22 | tt_get_10k = benchmark.report('DiskStore.get : 10k records') { 23 | 10_000.times do 24 | disk_store.get("10_000#{rand(1..10_000)}") 25 | end 26 | } 27 | 28 | tt_put_100k = benchmark.report('DiskStore.put : 100k records') { 29 | 100_000.times do |n_time| 30 | disk_store.put("100_000#{n_time}", value) 31 | end 32 | } 33 | 34 | tt_get_100k = benchmark.report('DiskStore.get : 100k records') { 35 | 100_000.times do 36 | disk_store.get("100_000#{rand(1..100_000)}") 37 | end 38 | } 39 | 40 | tt_put_1M = benchmark.report('DiskStore.put : 1M records') { 41 | 1_000_000.times do |n_time| 42 | disk_store.put("1_000_000#{n_time}", value) 43 | end 44 | } 45 | 46 | tt_get_1M = benchmark.report('DiskStore.get : 1M records') { 47 | 1_000_000.times do 48 | disk_store.get("1_000_000#{rand(1..1_000_000)}") 49 | end 50 | } 51 | 52 | [(tt_put_10k + tt_put_100k + tt_put_1M) / (10_000 + 100_000 + 1_000_000).to_f, 53 | (tt_get_10k + tt_get_100k + tt_get_1M) / (10_000 + 100_000 + 1_000_000).to_f] 54 | end 55 | 56 | value_1 = Faker::Lorem.sentence(word_count: 100) 57 | value_2 = Faker::Lorem.sentence(word_count: 1000) 58 | value_3 = Faker::Lorem.sentence(word_count: 100_000) 59 | 60 | Benchmark.ips do |benchmark| 61 | benchmark.report("DiskStore.put : 100 records with data size: #{value_1.length} Bytes") do 62 | 100.times do |n_time| 63 | disk_store.put("10_000_#{value_1.length}#{n_time}", value_1) 64 | end 65 | end 66 | 67 | benchmark.report("DiskStore.get : 100 records, value size: #{value_1.length} Bytes") do 68 | 100.times do 69 | disk_store.get("10_000#{value_1.length}#{rand(1..10_000)}") 70 | end 71 | end 72 | 73 | benchmark.report("DiskStore.put : 100 records, value size: #{value_2.length / 1024} Kb") do 74 | 100.times do |n_time| 75 | disk_store.put("10_000#{value_2.length}#{n_time}", value_2) 76 | end 77 | end 78 | 79 | benchmark.report("DiskStore.get : 100 records, value size: #{value_2.length / 1024} Kb") do 80 | 100.times do 81 | disk_store.get("10_000#{value_2.length}#{rand(1..10_000)}") 82 | end 83 | end 84 | 85 | benchmark.report("DiskStore.put : 100 records, value size: #{value_3.length / 1024} Kb") do 86 | 100.times do |n_time| 87 | disk_store.put("10_000#{value_3.length}#{n_time}", value_3) 88 | end 89 | end 90 | 91 | benchmark.report("DiskStore.get : 100 records, value size: #{value_3.length / 1024} Kb") { 92 | 100.times do 93 | disk_store.get("10_000#{value_3.length}#{rand(1..10_000)}") 94 | end 95 | } 96 | end 97 | 98 | File.delete('bitcask_benchmark.db') 99 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dineshgowda24/bitcask-rb/63f1c3b701b68b679c5de8272c6dc039cb25d2f1/image.png -------------------------------------------------------------------------------- /lib/bitcask.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'bitcask/serializer' 4 | require_relative 'bitcask/disk_store' 5 | -------------------------------------------------------------------------------- /lib/bitcask/disk_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ostruct' 4 | 5 | module Bitcask 6 | ## 7 | # This class represents a persistant store via a database file 8 | class DiskStore 9 | include Serializer 10 | 11 | ## 12 | # Creates a new store described by a database file 13 | # 14 | # If the file already exists, then contents of key_dir will be populated. 15 | # A StandardError will be raised when the file is corrupted. 16 | def initialize(db_file = 'bitcask.db') 17 | @db_fh = File.open(db_file, 'a+b') 18 | @write_pos = 0 19 | @key_dir = {} 20 | 21 | init_key_dir 22 | end 23 | 24 | def [](key) 25 | get(key) 26 | end 27 | 28 | def []=(key, value) 29 | put(key, value) 30 | end 31 | 32 | # Get the value for the given key 33 | # When the key does not exist, it returns empty string 34 | # 35 | # @param [String] key 36 | # @return [Value] value of the given key 37 | def get(key) 38 | key_struct = @key_dir[key] 39 | return '' if key_struct.nil? 40 | 41 | @db_fh.seek(key_struct[:write_pos]) 42 | epoc, key, value = deserialize(@db_fh.read(key_struct[:log_size])) 43 | 44 | value 45 | end 46 | 47 | # Sets a new key value pair 48 | # 49 | # @param [String, String] key and value 50 | # @return [nil] 51 | def put(key, value) 52 | log_size, data = serialize(epoc: Time.now.to_i, key: key, value: value) 53 | 54 | @key_dir[key] = key_struct(@write_pos, log_size, key) 55 | persist(data) 56 | incr_write_pos(log_size) 57 | 58 | nil 59 | end 60 | 61 | def keys 62 | @key_dir.keys 63 | end 64 | 65 | def size 66 | @key_dir.length 67 | end 68 | 69 | def flush 70 | @db_fh.flush 71 | end 72 | 73 | def close 74 | flush 75 | @db_fh.close 76 | end 77 | 78 | private 79 | 80 | def persist(data) 81 | @db_fh.write(data) 82 | @db_fh.flush 83 | end 84 | 85 | def incr_write_pos(pos) 86 | @write_pos += pos 87 | end 88 | 89 | def key_struct(write_pos, log_size, key) 90 | { write_pos: write_pos, log_size: log_size, key: key } 91 | end 92 | 93 | def init_key_dir 94 | while (crc_and_header_bytes = @db_fh.read(crc32_header_offset)) 95 | 96 | header_bytes = crc_and_header_bytes[crc32_offset..] 97 | epoc, keysz, valuesz, key_type, value_type = deserialize_header(header_bytes) 98 | 99 | key_bytes = @db_fh.read(keysz) 100 | value_bytes = @db_fh.read(valuesz) 101 | 102 | key = unpack(key_bytes, key_type) 103 | value = unpack(value_bytes, value_type) 104 | 105 | crc = crc_and_header_bytes[..crc32_offset - 1] 106 | raise StandardError, 'file corrupted' unless crc32_valid?(desearlize_crc32(crc), 107 | header_bytes + key_bytes + value_bytes) 108 | 109 | log_size = crc32_header_offset + keysz + valuesz 110 | @key_dir[key] = key_struct(@write_pos, log_size, key) 111 | incr_write_pos(log_size) 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/bitcask/serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'zlib' 4 | 5 | module Bitcask 6 | ## 7 | # Serializer module encapsulates the complexties of serializing and deserializing arbitary data 8 | # to/from bytes stream 9 | module Serializer 10 | # Follwing are the header values stored 11 | # |epoc|keysz|valuesz|key_type|value_type| 12 | # | 4B | 4B | 4B | 2B | 2B | 13 | # A total of 16 Bytes 14 | # L< : unsiged 32 bit int with little endian byte order 15 | # S< " unsiged 12 bit int with little endian byte order 16 | # Endian order does not matter, its only used to keep consitent byte ordering to ensure that db file, 17 | # can be seemlessly interchanged in little/big endian machines 18 | HEADER_FORMAT = 'L :Integer, 33 | DATA_TYPE[:Float] => :Float, 34 | DATA_TYPE[:String] => :String 35 | }.freeze 36 | 37 | DATA_TYPE_DIRECTIVE = { 38 | # 64 bit signed long int with little endian byte order 39 | DATA_TYPE[:Integer] => 'q<', 40 | # 64 bit double with little endian byte order 41 | DATA_TYPE[:Float] => 'E' 42 | }.freeze 43 | 44 | # Serializes epoc, key, value with metadata 45 | # 46 | # @param [Integer, String, String] contents to serialize 47 | # @return [Integer, String] Size of the serialized binary string, binary seralized string 48 | def serialize(epoc:, key:, value:) 49 | key_type = type(key) 50 | value_type = type(value) 51 | 52 | key_bytes = pack(key, key_type) 53 | value_bytes = pack(value, value_type) 54 | 55 | header = serialize_header(epoc: epoc, keysz: key_bytes.length, key_type: key_type, value_type: value_type, 56 | valuesz: value_bytes.length) 57 | data = key_bytes + value_bytes 58 | 59 | [crc32_header_offset + data.length, crc32(header + data) + header + data] 60 | end 61 | 62 | # Deserializes byte string 63 | # 64 | # @param [String] byte string to deserialize 65 | # @return [Integer, String|Float|Integer, String|Float|Integer] Epoc, Key, Value 66 | def deserialize(data) 67 | return 0, '', '' unless crc32_valid?(desearlize_crc32(data[..crc32_offset - 1]), data[crc32_offset..]) 68 | 69 | epoc, keysz, valuesz, key_type, value_type = deserialize_header(data[crc32_offset..crc32_header_offset - 1]) 70 | key_bytes = data[crc32_header_offset..crc32_header_offset + keysz - 1] 71 | value_bytes = data[crc32_header_offset + keysz..] 72 | 73 | [epoc, unpack(key_bytes, key_type), unpack(value_bytes, value_type)] 74 | end 75 | 76 | # Serializes header 77 | # 78 | # @param [Integer, String, String, Symbol, Symbol] contents to serialize 79 | # @return [String] Byte string 80 | def serialize_header(epoc:, key_type:, keysz:, value_type:, valuesz:) 81 | [epoc, keysz, valuesz, DATA_TYPE[key_type], DATA_TYPE[value_type]].pack(HEADER_FORMAT) 82 | end 83 | 84 | # Derializes header 85 | # 86 | # @param [String] byte string to dserialize 87 | # @return [Integer, String, String, Symbol, Symbol] Epoc, keysz, valuesz, key_type, value_type 88 | def deserialize_header(header_data) 89 | header = header_data.unpack(HEADER_FORMAT) 90 | 91 | [header[0], header[1], header[2], DATA_TYPE_LOOK_UP[header[3]], DATA_TYPE_LOOK_UP[header[4]]] 92 | end 93 | 94 | def crc32_offset 95 | CRC32_SIZE 96 | end 97 | 98 | def header_offset 99 | HEADER_SIZE 100 | end 101 | 102 | def crc32_header_offset 103 | crc32_offset + header_offset 104 | end 105 | 106 | # Generates crc32 and seralizes to bytes 107 | # 108 | # @param [String] byte string 109 | # @return [String] crc32 bytes 110 | def crc32(data_bytes) 111 | [Zlib.crc32(data_bytes)].pack(CRC32_FORMAT) 112 | end 113 | 114 | # Derializes crc32 byte string 115 | # 116 | # @param [String] byte string 117 | # @return [Integer] crc32 118 | def desearlize_crc32(crc) 119 | crc.unpack1(CRC32_FORMAT) 120 | end 121 | 122 | def crc32_valid?(digest, data_bytes) 123 | digest == Zlib.crc32(data_bytes) 124 | end 125 | 126 | def pack(attribute, attribute_type) 127 | case attribute_type 128 | when :Integer, :Float 129 | [attribute].pack(directive(attribute_type)) 130 | when :String 131 | attribute.encode('utf-8') 132 | else 133 | raise StandardError, 'Invalid attribute_type for pack' 134 | end 135 | end 136 | 137 | def unpack(attribute, attribute_type) 138 | case attribute_type 139 | when :Integer, :Float 140 | attribute.unpack1(directive(attribute_type)) 141 | when :String 142 | attribute 143 | else 144 | raise StandardError, 'Invalid attribute_type for unpack' 145 | end 146 | end 147 | 148 | private 149 | 150 | def directive(attribute_type) 151 | DATA_TYPE_DIRECTIVE[DATA_TYPE[attribute_type]] 152 | end 153 | 154 | def type(attribute) 155 | attribute.class.to_s.to_sym 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /spec/bitcask/disk_store_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Bitcask::DiskStore do 6 | let(:test_db_file) { 'bitcask_test.db' } 7 | let(:test_db_fixture_file) { db_fixture_file_path } 8 | 9 | describe '#put' do 10 | subject { described_class.new(test_db_file) } 11 | 12 | after do 13 | File.delete(test_db_file) 14 | end 15 | 16 | it 'puts a kv pair on the disk' do 17 | expect(subject.put(Faker::Lorem.word, Faker::Lorem.sentence)).to be_nil 18 | expect(subject.put(Faker::Lorem.word, Faker::Lorem.sentence(word_count: 10))).to be_nil 19 | expect(subject.put(Faker::Lorem.word, Faker::Lorem.sentence(word_count: 100))).to be_nil 20 | expect(subject.put(Faker::Lorem.word, Faker::Lorem.sentence(word_count: 1000))).to be_nil 21 | expect(subject.put(rand(1..10_000), Faker::Lorem.sentence(word_count: 10_000))).to be_nil 22 | expect(subject.put(rand(10.2..100.234), rand(1..10_000))).to be_nil 23 | expect(subject.put(rand(1..10_000), rand(10.2..100.234))).to be_nil 24 | end 25 | end 26 | 27 | describe '#get' do 28 | context 'when the db file is present' do 29 | subject { described_class.new(test_db_fixture_file) } 30 | 31 | context 'when the key is not present' do 32 | it 'returns empty string' do 33 | expect(subject.get('Anime')).to eq('') 34 | end 35 | end 36 | 37 | context 'when the key is present' do 38 | it 'returns value' do 39 | expect(subject.get('ut')).to eq('Voluptatum esse non vero ut vitae harum blanditiis ducimus vel nam rerum quia ipsa necessitatibus quo eaque animi ab voluptatem sed sunt non ipsam aut velit rerum perspiciatis quasi doloribus omnis eum et reprehenderit qui minima aut illo veritatis atque sequi quas eius consequatur magni saepe numquam molestias odio beatae quo maiores dignissimos illum aut sint qui porro sed in enim enim asperiores et tenetur voluptas maxime possimus quidem accusantium laudantium aliquam ipsum voluptates consequuntur et tempora cumque voluptatem dolor tempore sint nemo ex omnis repudiandae aliquid pariatur neque nostrum debitis odit qui nihil voluptatem minus temporibus voluptatem ut sit.') 40 | expect(subject.get('saepe')).to eq('Non autem magni non quaerat non eos enim amet qui molestiae pariatur quam rerum facilis nulla tempora reprehenderit ipsa rerum sunt ducimus aspernatur magni blanditiis blanditiis eveniet sed nobis quisquam iusto quia corporis in deleniti repellendus iure similique facere maxime beatae aut quidem fuga labore laborum reprehenderit suscipit eveniet molestias aspernatur vel minus sunt quo reprehenderit sint deserunt corporis velit hic recusandae et voluptatem ipsa dolores eos facere eum qui ut nam et aperiam voluptatibus minima laborum doloremque officiis optio eaque voluptatibus et sint dicta esse ab ex ut cumque temporibus alias voluptatum qui iusto laboriosam nihil qui veritatis aut pariatur et sint rerum fugiat reiciendis non quia atque blanditiis et suscipit unde magni iste ea voluptates ex ad expedita eum ut quasi et adipisci reiciendis et a earum enim excepturi autem vel accusantium qui veniam est qui odio voluptatem aut deleniti omnis quia placeat ut modi nostrum tempora est labore ipsa accusamus et sit eum delectus nobis dolores laboriosam omnis est sed voluptatem neque ut dicta et est et dolor vero quis praesentium minima ut voluptas omnis ut velit sed eum harum veniam dignissimos tempora aliquid et consectetur in ducimus cum molestiae dolor hic quisquam sed tenetur non quam eaque fugiat ea qui esse recusandae nisi officia provident dolores facere voluptatem corrupti distinctio magnam perferendis sunt soluta ut perspiciatis laudantium officia veniam quia magnam sunt libero corporis ut nisi est reprehenderit saepe ut debitis et eum ipsum illo iste animi expedita rerum totam et animi temporibus voluptatum minus amet illum inventore vel aut dolorem est necessitatibus rem fugit est omnis laudantium dolor fugit sit distinctio voluptates voluptatem aut exercitationem placeat rerum molestias corrupti earum impedit numquam minima eum voluptate in quasi illo quo veritatis perferendis et est numquam sit quo dolores dolorem animi consectetur voluptatem est expedita commodi reprehenderit inventore accusamus nostrum sapiente adipisci molestiae nam et itaque odit assumenda cupiditate deserunt vel aut cupiditate quidem quo ut dolores aut ut ad nihil aut dolores sed eaque modi molestiae debitis qui omnis eius dicta dolorem odit ut et eveniet et repellat sit omnis porro nemo qui ullam culpa explicabo accusantium omnis aut ipsum sed dignissimos odit fugit cumque harum nihil ut repudiandae nisi impedit aut dolor velit nemo a nulla facilis eum optio quaerat repellendus expedita dolores sunt perferendis ullam optio ullam consequatur iste id saepe officiis aspernatur ea rerum et nesciunt sit velit et dolorem quo quam alias in omnis et quos consequatur consequatur natus ut ut mollitia ut quae sed hic et ex quae ut libero doloremque unde rem esse sint maiores cumque maiores non ratione vitae perferendis ratione qui sapiente doloribus consequuntur consequatur dicta laudantium eligendi qui voluptas voluptate amet vel quas sunt qui similique tenetur culpa alias accusamus repellat eos sint facere qui labore facilis vitae aliquam voluptates ipsum voluptatem voluptate est dolorum autem quia illum ea aliquid molestias voluptatem ipsam atque eaque mollitia minus excepturi aliquam est iure impedit deleniti occaecati et nemo dignissimos totam illo laudantium temporibus magni suscipit tempora veritatis aut autem ut quis necessitatibus natus maiores dignissimos nulla alias adipisci recusandae quis doloribus ipsam consequatur et quis eum inventore et architecto neque at sed aliquam asperiores voluptatem vel libero asperiores natus tempore sint distinctio voluptatem est commodi sequi saepe molestiae tempora maxime dolor facilis nesciunt repudiandae corporis consequatur similique itaque eum tempore et ex ipsa deserunt possimus ullam odio aut repellat molestiae earum ea et sit doloribus provident fugit eveniet sit excepturi adipisci nostrum ab corrupti quia harum omnis quia non deleniti libero et maiores consequatur eligendi accusantium quod nihil minus ut dicta voluptatem dolor distinctio sit harum dolorum est tenetur itaque quia vel est ratione atque ex voluptas est aut molestiae est sapiente commodi saepe atque possimus rerum nihil aut neque eum repellat porro hic est debitis sequi qui optio nam tempore aut nobis assumenda blanditiis commodi occaecati reiciendis et fuga hic rerum aut aut nam ut doloribus assumenda sit assumenda et dolorem nobis error debitis maxime et quod commodi asperiores voluptas velit earum et delectus enim qui quod sit corporis est totam blanditiis mollitia dolores voluptatem consequatur sunt ut cumque a et id consequuntur molestias eos occaecati mollitia at pariatur nesciunt reiciendis corrupti voluptatibus quia laboriosam excepturi qui cum odit neque dolore accusamus cupiditate et veniam animi necessitatibus nesciunt totam dolorem qui ea dolorum doloribus et odio sit fugit eos facere culpa perspiciatis ipsa consequuntur modi velit quidem aperiam labore consequatur animi ipsam exercitationem sed ipsam aperiam labore at cum tenetur dolor provident consequatur nobis quibusdam quaerat vel alias deserunt sed voluptas et et consectetur consequatur quos deleniti in dolorum ab voluptates quo id expedita quas eius esse quo explicabo fuga tempore ut quo velit enim voluptatibus voluptas placeat voluptas id porro molestiae autem accusantium sed voluptas voluptatem maxime enim illum sint omnis qui aperiam aliquam quis aut voluptatum in aut recusandae illum quia et quae quaerat asperiores explicabo autem sunt recusandae accusamus eius praesentium qui in quis voluptas nulla aut magnam quia sapiente ea eius est error soluta autem molestias enim rerum quod sed laborum rem delectus ea adipisci rerum cumque id harum enim omnis repellat veniam sed nostrum numquam quas incidunt quibusdam esse similique nemo qui autem perspiciatis non pariatur et beatae nisi repellendus itaque officiis consequatur qui cum voluptatem et placeat non sed aspernatur amet unde aut suscipit cupiditate doloremque soluta veritatis magni nihil error et totam sint porro et nostrum aut unde officia dolorem quia unde corrupti libero rem et et natus est vero consectetur modi quibusdam voluptatem fugiat dolore incidunt reiciendis asperiores nihil veritatis amet velit quo et eligendi numquam in ea id ut iure id sequi in consequatur id facilis dolore voluptas voluptatem quia qui quibusdam qui quos exercitationem ad omnis vitae sunt voluptate qui quo aut quod possimus est quos dolorem quasi aspernatur provident beatae iusto officia nihil voluptatum et et praesentium explicabo ut itaque id laboriosam aliquid autem ipsum quos vero id inventore quis officiis ad temporibus quisquam sit deserunt fugiat incidunt repellendus cum magnam quidem enim consectetur maxime laborum rerum.') 41 | expect(subject.get('suscipit')).to eq('Quia sapiente maiores sunt.') 42 | expect(subject.get(1)).to eq(10_000) 43 | expect(subject.get(1.000)).to eq(10_000.000) 44 | end 45 | end 46 | end 47 | 48 | context 'when the db file is not present' do 49 | subject { described_class.new(test_db_file) } 50 | 51 | after do 52 | File.delete(test_db_file) 53 | end 54 | 55 | context 'when the key is not present' do 56 | it 'returns empty string' do 57 | expect(subject.get(Faker::Lorem.word)).to eq('') 58 | end 59 | end 60 | 61 | context 'when the key is present' do 62 | let(:key_1) { Faker::Lorem.word } 63 | let(:key_2) { Faker::Lorem.word } 64 | let(:key_3) { rand(1..1000) } 65 | let(:value_1) { Faker::Lorem.sentence(word_count: 10_000) } 66 | let(:value_2) { Faker::Lorem.sentence(word_count: 100_000) } 67 | let(:value_3) { rand(11.2...76.9) } 68 | 69 | before do 70 | subject.put(key_1, value_1) 71 | subject.put(key_2, value_2) 72 | subject.put(key_3, value_3) 73 | end 74 | 75 | it 'returns value' do 76 | expect(subject.get(key_1)).to eq(value_1) 77 | expect(subject.get(key_2)).to eq(value_2) 78 | expect(subject.get(key_3)).to eq(value_3) 79 | end 80 | end 81 | end 82 | end 83 | 84 | describe '#keys' do 85 | context 'when the db file is present' do 86 | subject { described_class.new(test_db_fixture_file) } 87 | 88 | context 'when the db file has data' do 89 | it 'returns an array of keys' do 90 | expect(subject.keys.length).to eq(5) 91 | expect(subject.keys).to eq([1, 1.0, 'ut', 'saepe', 'suscipit']) 92 | end 93 | end 94 | end 95 | 96 | context 'when the db file is not present' do 97 | subject { described_class.new(test_db_file) } 98 | 99 | after do 100 | File.delete(test_db_file) 101 | end 102 | 103 | context 'when the db file is empty' do 104 | it 'returns an empty array' do 105 | expect(subject.keys.length).to eq(0) 106 | expect(subject.keys).to eq([]) 107 | end 108 | end 109 | 110 | context 'when the db file is not empty' do 111 | let(:key_1) { Faker::Lorem.word } 112 | let(:key_2) { Faker::Lorem.word } 113 | let(:value_1) { Faker::Lorem.sentence(word_count: 10_000) } 114 | let(:value_2) { Faker::Lorem.sentence(word_count: 100_000) } 115 | 116 | before do 117 | subject.put(key_1, value_1) 118 | subject.put(key_2, value_2) 119 | end 120 | 121 | it 'returns an empty array' do 122 | expect(subject.keys.length).to eq(2) 123 | expect(subject.keys).to eq([key_1, key_2]) 124 | end 125 | end 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /spec/bitcask/serializer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Bitcask::Serializer do 4 | let(:crc_size) { 4 } 5 | let(:header_size) { 16 } 6 | let(:crc_and_header_size) { header_size + crc_size } 7 | let(:subject) { Class.new { extend Bitcask::Serializer } } 8 | let(:now) { Time.now.to_i } 9 | 10 | describe '#serialize' do 11 | let(:key) { Faker::Lorem.word } 12 | let(:value) { Faker::Lorem.sentence(word_count: 10_000) } 13 | 14 | it 'returns serialized data and its size' do 15 | size, data = subject.serialize(epoc: now, key: key, value: value) 16 | 17 | expect(size).to eq(crc_and_header_size + key.length + value.length) 18 | expect(data).not_to be_empty 19 | end 20 | end 21 | 22 | describe '#serialize_header' do 23 | it 'returns serialized header' do 24 | data = subject.serialize_header(epoc: now, keysz: 10, valuesz: 20, key_type: :Integer, value_type: :Integer) 25 | 26 | expect(data.length).to eq(header_size) 27 | expect(data).not_to be_empty 28 | end 29 | end 30 | 31 | describe '#deserialize' do 32 | let(:serialized_data_1) do 33 | OpenStruct.new(raw: "<\v\x90\x8C\x01\xC1\xA9c\b\x00\x00\x00\b\x00\x00\x00\x01\x00\x02\x00\x01\x00\x00\x00\x00\x00\x00\x00333333\xF3?", 34 | epoc: 1_672_069_377, key: 1, value: 1.2) 35 | end 36 | let(:serialized_data_2) do 37 | OpenStruct.new(raw: "\xFE\xE9\xD2.\x01\xC1\xA9c\b\x00\x00\x00\b\x00\x00\x00\x02\x00\x01\x00333333\xF3?\x02\x00\x00\x00\x00\x00\x00\x00", 38 | epoc: 1_672_069_377, key: 1.2, value: 2) 39 | end 40 | let(:serialized_data_3) do 41 | OpenStruct.new(raw: "\xBA\xFD\x0EA\x01\xC1\xA9c\x05\x00\x00\x00\t\x00\x00\x00\x03\x00\x03\x00AnimeOne Piece", 42 | epoc: 1_672_069_377, key: 'Anime', value: 'One Piece') 43 | end 44 | 45 | context 'when a valid data is passed' do 46 | it 'returns epoc, key and value' do 47 | epoc, key, value = subject.deserialize(serialized_data_1.raw) 48 | 49 | expect(epoc).to eq(serialized_data_1.epoc) 50 | expect(key).to eq(serialized_data_1.key) 51 | expect(value).to eq(serialized_data_1.value) 52 | end 53 | 54 | it 'returns epoc, key and value' do 55 | epoc, key, value = subject.deserialize(serialized_data_2.raw) 56 | 57 | expect(epoc).to eq(serialized_data_2.epoc) 58 | expect(key).to eq(serialized_data_2.key) 59 | expect(value).to eq(serialized_data_2.value) 60 | end 61 | 62 | it 'returns epoc, key and value' do 63 | epoc, key, value = subject.deserialize(serialized_data_3.raw) 64 | 65 | expect(epoc).to eq(serialized_data_3.epoc) 66 | expect(key).to eq(serialized_data_3.key) 67 | expect(value).to eq(serialized_data_3.value) 68 | end 69 | end 70 | 71 | context 'when an empty string is passed' do 72 | it 'raises size as 0 and empty string' do 73 | epoc, key, value = subject.deserialize('') 74 | 75 | expect(epoc).to eq(0) 76 | expect(key).to eq('') 77 | expect(value).to eq('') 78 | end 79 | end 80 | 81 | context 'when crc is invalid' do 82 | it 'returns size as 0 and empty string' do 83 | epoc, key, value = subject.deserialize("\xCEi\x94\x03}\xA7\xA8c\x05\x00\x00\x00\t\x00\x00\x00animeTwo Piece") 84 | 85 | expect(epoc).to eq(0) 86 | expect(key).to eq('') 87 | expect(value).to eq('') 88 | end 89 | end 90 | end 91 | 92 | describe '#deserialize_header' do 93 | let(:key) { 'anime' } 94 | let(:value) { 'One Piece' } 95 | let(:serialized_header_data) do 96 | OpenStruct.new(raw: "\xF5\xC1\xA9c\n\x00\x00\x00\x14\x00\x00\x00\x01\x00\x01\x00", epoc: 1_672_069_621, keysz: 10, 97 | valuesz: 20, value_type: :Integer, key_type: :Integer) 98 | end 99 | 100 | it 'returns epoc, keysz and valuesz' do 101 | epoc, keysz, valuesz, key_type, value_type = subject.deserialize_header(serialized_header_data.raw) 102 | 103 | expect(epoc).to eq(serialized_header_data.epoc) 104 | expect(keysz).to eq(serialized_header_data.keysz) 105 | expect(valuesz).to eq(serialized_header_data.valuesz) 106 | expect(key_type).to eq(serialized_header_data.key_type) 107 | expect(value_type).to eq(serialized_header_data.value_type) 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /spec/fixtures/1672042848_bitcask.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dineshgowda24/bitcask-rb/63f1c3b701b68b679c5de8272c6dc039cb25d2f1/spec/fixtures/1672042848_bitcask.db -------------------------------------------------------------------------------- /spec/fixtures/1672070284_bitcask.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dineshgowda24/bitcask-rb/63f1c3b701b68b679c5de8272c6dc039cb25d2f1/spec/fixtures/1672070284_bitcask.db -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by the `rspec --init` command. Conventionally, all 4 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 5 | # The generated `.rspec` file contains `--require spec_helper` which will cause 6 | # this file to always be loaded, without a need to explicitly require it in any 7 | # files. 8 | # 9 | # Given that it is always loaded, you are encouraged to keep this file as 10 | # light-weight as possible. Requiring heavyweight dependencies from this file 11 | # will add to the boot time of your test suite on EVERY test run, even for an 12 | # individual file that may not need all of that loaded. Instead, consider making 13 | # a separate helper file that requires the additional dependencies and performs 14 | # the additional setup, and require it from the spec files that actually need 15 | # it. 16 | # 17 | # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 18 | require 'simplecov' 19 | require 'simplecov-cobertura' 20 | 21 | SimpleCov.start do 22 | formatter SimpleCov::Formatter::CoberturaFormatter if ENV['CI'] 23 | end 24 | 25 | require 'bitcask' 26 | require 'byebug' 27 | require 'ostruct' 28 | require 'faker' 29 | 30 | RSpec.configure do |config| 31 | # rspec-expectations config goes here. You can use an alternate 32 | # assertion/expectation library such as wrong or the stdlib/minitest 33 | # assertions if you prefer. 34 | config.expect_with :rspec do |expectations| 35 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 36 | end 37 | 38 | # rspec-mocks config goes here. You can use an alternate test double 39 | # library (such as bogus or mocha) by changing the `mock_with` option here. 40 | config.mock_with :rspec do |mocks| 41 | # Prevents you from mocking or stubbing a method that does not exist on 42 | # a real object. This is generally recommended, and will default to 43 | # `true` in RSpec 4. 44 | mocks.verify_partial_doubles = true 45 | end 46 | 47 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 48 | # have no way to turn it off -- the option exists only for backwards 49 | # compatibility in RSpec 3). It causes shared context metadata to be 50 | # inherited by the metadata hash of host groups and examples, rather than 51 | # triggering implicit auto-inclusion in groups with matching metadata. 52 | config.shared_context_metadata_behavior = :apply_to_host_groups 53 | end 54 | 55 | def db_fixture_file_path 56 | 'spec/fixtures/1672070284_bitcask.db' 57 | end 58 | --------------------------------------------------------------------------------