rubcask 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.standard.yml +3 -0
  3. data/Gemfile +20 -0
  4. data/Gemfile.lock +74 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +111 -0
  7. data/Rakefile +14 -0
  8. data/benchmark/benchmark_io.rb +49 -0
  9. data/benchmark/benchmark_server.rb +10 -0
  10. data/benchmark/benchmark_server_pipeline.rb +24 -0
  11. data/benchmark/benchmark_worker.rb +46 -0
  12. data/benchmark/op_times.rb +32 -0
  13. data/benchmark/profile.rb +15 -0
  14. data/benchmark/server_benchmark_helper.rb +138 -0
  15. data/example/server_runner.rb +15 -0
  16. data/lib/rubcask/bytes.rb +11 -0
  17. data/lib/rubcask/concurrency/fake_atomic_fixnum.rb +34 -0
  18. data/lib/rubcask/concurrency/fake_lock.rb +41 -0
  19. data/lib/rubcask/concurrency/fake_monitor_mixin.rb +21 -0
  20. data/lib/rubcask/config.rb +55 -0
  21. data/lib/rubcask/data_entry.rb +9 -0
  22. data/lib/rubcask/data_file.rb +91 -0
  23. data/lib/rubcask/directory.rb +437 -0
  24. data/lib/rubcask/expirable_entry.rb +9 -0
  25. data/lib/rubcask/hint_entry.rb +9 -0
  26. data/lib/rubcask/hint_file.rb +56 -0
  27. data/lib/rubcask/hinted_file.rb +148 -0
  28. data/lib/rubcask/keydir_entry.rb +9 -0
  29. data/lib/rubcask/merge_directory.rb +75 -0
  30. data/lib/rubcask/protocol.rb +74 -0
  31. data/lib/rubcask/server/abstract_server.rb +113 -0
  32. data/lib/rubcask/server/async.rb +78 -0
  33. data/lib/rubcask/server/client.rb +131 -0
  34. data/lib/rubcask/server/config.rb +31 -0
  35. data/lib/rubcask/server/pipeline.rb +49 -0
  36. data/lib/rubcask/server/runner/config.rb +43 -0
  37. data/lib/rubcask/server/runner.rb +107 -0
  38. data/lib/rubcask/server/threaded.rb +171 -0
  39. data/lib/rubcask/task/clean_directory.rb +19 -0
  40. data/lib/rubcask/tombstone.rb +40 -0
  41. data/lib/rubcask/version.rb +5 -0
  42. data/lib/rubcask/worker/direct_worker.rb +23 -0
  43. data/lib/rubcask/worker/factory.rb +42 -0
  44. data/lib/rubcask/worker/ractor_worker.rb +40 -0
  45. data/lib/rubcask/worker/thread_worker.rb +40 -0
  46. data/lib/rubcask.rb +19 -0
  47. metadata +102 -0
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubcask
4
+ module Server
5
+ class Runner
6
+ # @!attribute merge_interval
7
+ # How frequent in seconds should merge operation by run.
8
+ #
9
+ # Default: 3600
10
+ # @return [Integer, null]
11
+ # @!attribute server_type
12
+ # Which type of server should be run.
13
+ #
14
+ # Use threaded if you are not on MRI. If you are on mri and can install `async-io` use :async.
15
+ #
16
+ # Default: :threaded
17
+ # @return [:threaded, :async]
18
+ # @!attribute directory_path
19
+ # Path of the directory in which the data is stored.
20
+ #
21
+ # Default: no default value, user has to set it manually.
22
+ # @return [String]
23
+ # Server runner config
24
+ Config = Struct.new(:merge_interval, :server_type, :directory_path) do
25
+ # Overide fields with the block
26
+ # @yieldparam [self] config
27
+ def initialize
28
+ self.server_type = :threaded
29
+ self.merge_interval = 3_600
30
+ self.directory_path = nil
31
+
32
+ yield(self) if block_given?
33
+ end
34
+
35
+ # Calls new and freezes the config
36
+ # @see .initialize
37
+ def self.configure(&block)
38
+ new(&block).freeze
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+ require_relative "./config"
5
+ require_relative "./runner/config"
6
+ require_relative "../config"
7
+
8
+ module Rubcask
9
+ module Server
10
+ # ServerRunner runs a server alongside merge worker
11
+ # It supports graceful shutdown with Ctrl-c
12
+ class Runner
13
+ def initialize(
14
+ server_config: Rubcask::Server::Config.new,
15
+ dir_config: Rubcask::Config::DEFAULT_SERVER_CONFIG,
16
+ runner_config: Rubcask::Server::Runner::Config.new
17
+ )
18
+ @dir = Rubcask::Directory.new(
19
+ runner_config.directory_path,
20
+ config: dir_config
21
+ )
22
+ @server = new_server(runner_config.server_type, server_config)
23
+ @merge_worker = if runner_config.merge_interval && runner_config.merge_interval > 0
24
+ Concurrent::TimerTask.new(
25
+ execution_interval: runner_config.merge_interval
26
+ ) do
27
+ merge_dir
28
+ end
29
+ end
30
+ end
31
+
32
+ # Starts the runner.
33
+ # @note It blocks the current thread
34
+ def start
35
+ install_trap!
36
+ @merge_worker.execute
37
+ @server.start
38
+ end
39
+
40
+ # Stops the server
41
+ def close
42
+ close_server
43
+ mutex_close
44
+ end
45
+
46
+ private
47
+
48
+ def close_server
49
+ puts "Shutting down server!"
50
+ begin
51
+ @server.shutdown
52
+ rescue
53
+ end
54
+ end
55
+
56
+ def mutex_close
57
+ if @merge_worker
58
+ puts "Stoping merge worker"
59
+ @merge_worker.shutdown
60
+ if @merge_worker.wait_for_termination(60)
61
+ puts "Closed merge worker"
62
+ else
63
+ puts "Failed to close worker"
64
+ end
65
+ end
66
+
67
+ puts "Closing Dir!"
68
+ begin
69
+ @dir.close
70
+ rescue
71
+ end
72
+ puts "Closed dir"
73
+ end
74
+
75
+ def install_trap!
76
+ Signal.trap("INT") do
77
+ puts ""
78
+ # Close server in the same thread
79
+ close_server
80
+
81
+ # Other things might needs mutex so a new thread is needed
82
+ Thread.new do
83
+ mutex_close
84
+ end.join
85
+ end
86
+ end
87
+
88
+ def new_server(type, config)
89
+ if type == :threaded
90
+ require_relative "threaded"
91
+ Rubcask::Server::Threaded.new(@dir, config: config)
92
+ elsif type == :async
93
+ require_relative "async"
94
+ Rubcask::Server::Async.new(@dir, config: config)
95
+ else
96
+ raise ArgumentError, "Unknown server typ #{type}"
97
+ end
98
+ end
99
+
100
+ def merge_dir
101
+ @server.dir.merge
102
+ rescue => e
103
+ puts e
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "socket"
5
+ require "io/wait"
6
+ require "stringio"
7
+
8
+ require_relative "../bytes"
9
+ require_relative "../protocol"
10
+ require_relative "config"
11
+ require_relative "abstract_server"
12
+
13
+ module Rubcask
14
+ module Server
15
+ # Thread-based server supporting Rubcask protocol
16
+ # If you are running on CRuby you should consider using Server::Async as it is generally more performant
17
+ class Threaded < AbstractServer
18
+ include Protocol
19
+
20
+ def initialize(dir, config: Server::Config.new)
21
+ @dir = dir
22
+ @config = config
23
+ @hostname = config.hostname
24
+ @port = config.port
25
+ @logger = Logger.new($stdout)
26
+ @logger.level = Logger::INFO
27
+ @threads = ThreadGroup.new
28
+ @connected = false
29
+ @status = :stopped
30
+ @listeners = []
31
+ end
32
+
33
+ # Creates sockets
34
+ # @return [self]
35
+ def connect
36
+ return if @connected
37
+ @connected = true
38
+ @listeners = Socket.tcp_server_sockets(@hostname, @port)
39
+ if @config.keepalive
40
+ @listeners.each do |s|
41
+ s.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
42
+ end
43
+ end
44
+ @listeners.each do |s|
45
+ address = s.connect_address
46
+ logger.info "Listening on #{address.ip_address}:#{address.ip_port}"
47
+ end
48
+ self
49
+ end
50
+
51
+ # Starts the server
52
+ # @note It blocks the current thread
53
+ def start
54
+ connect
55
+
56
+ setup_shutdown_pipe
57
+
58
+ @status = :running
59
+
60
+ Thread.handle_interrupt(Exception => :never) do
61
+ Thread.handle_interrupt(Exception => :immediate) do
62
+ accept_loop
63
+ end
64
+ ensure
65
+ cleanup_shutdown_pipe
66
+ @status = :shutdown
67
+ cleanup_listeners
68
+ @threads.list.each(&:kill)
69
+ @status = :stopped
70
+ @connected = false
71
+ logger.info "Closed server"
72
+ end
73
+ end
74
+
75
+ # Shuts down the server
76
+ # @note You probably want to use it in a signal trap
77
+ def shutdown
78
+ if @status == :running
79
+ @status = :shutdown
80
+ end
81
+ @shutdown_pipe[1].write_nonblock("\0")
82
+ @shutdown_pipe[1].close
83
+ end
84
+
85
+ # Prepares an IO pipe that is used in shutdown process
86
+ # Call if you need to shutdown the server from a different thread
87
+ # @return [self]
88
+ def setup_shutdown_pipe
89
+ @shutdown_pipe ||= IO.pipe
90
+ self
91
+ end
92
+
93
+ private
94
+
95
+ attr_reader :logger
96
+
97
+ def cleanup_listeners
98
+ @listeners.each do |listener|
99
+ listener.shutdown
100
+ rescue Errno::ENOTCONN
101
+ listener.close
102
+ else
103
+ listener.close
104
+ end
105
+ @listeners.clear
106
+ end
107
+
108
+ def cleanup_shutdown_pipe
109
+ pipe = @shutdown_pipe
110
+ pipe&.each(&:close)
111
+ @shutdown_pipe = nil
112
+ end
113
+
114
+ def accept_loop
115
+ shutdown_read = @shutdown_pipe[0]
116
+ while @status == :running
117
+ begin
118
+ fds = IO.select([shutdown_read, *@listeners])[0]
119
+ if fds.include?(shutdown_read)
120
+ consume_pipe(shutdown_read)
121
+ break
122
+ end
123
+ fds.each do |listener|
124
+ client = accept_client(listener)
125
+ next unless client
126
+ @threads.add(
127
+ Thread.start(client) { |conn| client_block(conn) }
128
+ )
129
+ end
130
+ rescue Errno::EBADF, Errno::ENOTSOCK, IOError
131
+ # Possible if socket was manually shut down
132
+ end
133
+ end
134
+ end
135
+
136
+ def accept_client(listener)
137
+ sock = listener.accept_nonblock(exception: false)
138
+ return nil if sock == :wait_readable
139
+ sock[0]
140
+ rescue
141
+ nil
142
+ end
143
+
144
+ def consume_pipe(pipe)
145
+ buf = +""
146
+ while String === pipe.read_nonblock([pipe.nread, 8].max, buf, exception: false)
147
+ end
148
+ end
149
+
150
+ def client_block(conn)
151
+ conn.binmode
152
+ with_interrupt_handle(conn) do |io|
153
+ client_loop(io)
154
+ end
155
+ end
156
+
157
+ def with_interrupt_handle(conn)
158
+ Thread.handle_interrupt(Exception => :never) do
159
+ Thread.handle_interrupt(Exception => :immediate) do
160
+ yield conn
161
+ end
162
+ ensure
163
+ begin
164
+ conn.close
165
+ rescue # It sometimes failes on jruby
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubcask
4
+ module Task
5
+ # Removes all files marked as executable in the directory
6
+ class CleanDirectory
7
+ def initialize(directory)
8
+ @directory = directory
9
+ end
10
+
11
+ def call
12
+ Dir.glob(["*.data", "*.hint"].map! { |ext| File.join(@directory, ext) }).each do |file|
13
+ next unless File.executable?(file)
14
+ File.delete(file)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubcask
4
+ # Tombstone represents deleted value
5
+
6
+ # The prev_file_id is
7
+ # stored to support merge of subset of directory files, that is currently not implemented
8
+ module Tombstone
9
+ extend self
10
+
11
+ PREFIX = "TOMBSTONE".b
12
+ PREFIX_SIZE = PREFIX.bytesize
13
+ FILE_ID_FORMAT = "N"
14
+ FULL_BYTE_SIZE = PREFIX_SIZE + 4
15
+
16
+ # @param [String] value value to check
17
+ # @return true if value is a tombstone
18
+ # @return false otherwise
19
+ def is_tombstone?(value)
20
+ value.bytesize <= FULL_BYTE_SIZE && value.start_with?(PREFIX)
21
+ end
22
+
23
+ # Creates a new tombstone value
24
+ # @param [Integer] current_file_id Id of the active file
25
+ # @param [Integer] prev_file_id Id of the file where the record is currently located
26
+ # @return [String]
27
+ def new_tombstone(current_file_id, prev_file_id)
28
+ return PREFIX if prev_file_id == current_file_id
29
+ PREFIX.b << [prev_file_id].pack(FILE_ID_FORMAT)
30
+ end
31
+
32
+ # Gets file id from tombstone value
33
+ # @param [String] value Tombstone value
34
+ # @return [Integer, nil]
35
+ def tombstone_file_id(value)
36
+ return nil if value.bytesize < FULL_BYTE_SIZE
37
+ value.byteslice(PREFIX_SIZE, 4).unpack1(FILE_ID_FORMAT)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubcask
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ module Rubcask
6
+ module Worker
7
+ # Worker implementation that executes the job in the current thread
8
+ class DirectWorker
9
+ def initialize
10
+ @logger = Logger.new($stdout)
11
+ end
12
+
13
+ def push(task)
14
+ task.call
15
+ rescue => e
16
+ @logger.warn("Error while executing task #{e}")
17
+ end
18
+
19
+ def close
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "direct_worker"
4
+ require_relative "ractor_worker"
5
+ require_relative "thread_worker"
6
+
7
+ module Rubcask
8
+ module Worker
9
+ module Factory
10
+ extend self
11
+
12
+ # Returns a new worker of provided type
13
+ # @param [:direct, :thread, :reactor] type Type of worker to create
14
+ # @return [Worker]
15
+ # @raise [ArgumentError] if unknown type
16
+ def new_worker(type)
17
+ case type
18
+ when :direct
19
+ DirectWorker.new
20
+ when :thread
21
+ ThreadWorker.new
22
+ when :ractor
23
+ RactorWorker.new
24
+ else
25
+ raise ArgumentError, "#{type} is not a known worker type"
26
+ end
27
+ end
28
+
29
+ # Class for documentation purposes
30
+ class Worker
31
+ # @param [#call] arg job to execute
32
+ # @return [void]
33
+ def push(arg)
34
+ end
35
+
36
+ # @return [void]
37
+ def close
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "forwardable"
5
+
6
+ module Rubcask
7
+ module Worker
8
+ # Worker implementation that delegates work to a dedicated ractor
9
+ class RactorWorker
10
+ extend Forwardable
11
+
12
+ def_delegator :@ractor, :send, :push
13
+
14
+ def initialize
15
+ @ractor = new_ractor
16
+ @logger = Logger.new($stdout)
17
+ end
18
+
19
+ def close
20
+ push(nil)
21
+ @ractor.take
22
+ end
23
+
24
+ private
25
+
26
+ def new_ractor
27
+ Ractor.new(@logger) do |logger|
28
+ while (value = Ractor.receive)
29
+ begin
30
+ value.call
31
+ rescue => e
32
+ logger.warn("Error while executing task " + e)
33
+ end
34
+ Ractor.yield(nil)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Rubcask
6
+ module Worker
7
+ # Worker implementation that delegates work to a dedicated thread
8
+ class ThreadWorker
9
+ extend Forwardable
10
+
11
+ def_delegator :@queue, :push
12
+
13
+ def initialize
14
+ @queue = Queue.new
15
+ @logger = Logger.new($stdout)
16
+ @thread = new_thread
17
+ end
18
+
19
+ def close
20
+ @queue.close
21
+ @thread.join
22
+ nil
23
+ end
24
+
25
+ private
26
+
27
+ def new_thread
28
+ Thread.new(@queue) do |queue|
29
+ while (el = queue.pop)
30
+ begin
31
+ el.call
32
+ rescue => e
33
+ @logger.warn(e)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
data/lib/rubcask.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rubcask/version"
4
+ require_relative "rubcask/directory"
5
+ require_relative "rubcask/bytes"
6
+
7
+ module Rubcask
8
+ class Error < StandardError; end
9
+
10
+ class LoadError < Error; end
11
+
12
+ class ChecksumError < LoadError; end
13
+
14
+ class MergeAlreadyInProgressError < Error; end
15
+
16
+ class ConfigValidationError < Error; end
17
+
18
+ NO_EXPIRE_TIMESTAMP = 0
19
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubcask
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Marcin Henryk Bartkowiak
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-01-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: concurrent-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
27
+ description: Bitcask-like Key/Value storege library with a TCP server included
28
+ email:
29
+ - mhbartkowiak@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".standard.yml"
35
+ - Gemfile
36
+ - Gemfile.lock
37
+ - LICENSE.txt
38
+ - README.md
39
+ - Rakefile
40
+ - benchmark/benchmark_io.rb
41
+ - benchmark/benchmark_server.rb
42
+ - benchmark/benchmark_server_pipeline.rb
43
+ - benchmark/benchmark_worker.rb
44
+ - benchmark/op_times.rb
45
+ - benchmark/profile.rb
46
+ - benchmark/server_benchmark_helper.rb
47
+ - example/server_runner.rb
48
+ - lib/rubcask.rb
49
+ - lib/rubcask/bytes.rb
50
+ - lib/rubcask/concurrency/fake_atomic_fixnum.rb
51
+ - lib/rubcask/concurrency/fake_lock.rb
52
+ - lib/rubcask/concurrency/fake_monitor_mixin.rb
53
+ - lib/rubcask/config.rb
54
+ - lib/rubcask/data_entry.rb
55
+ - lib/rubcask/data_file.rb
56
+ - lib/rubcask/directory.rb
57
+ - lib/rubcask/expirable_entry.rb
58
+ - lib/rubcask/hint_entry.rb
59
+ - lib/rubcask/hint_file.rb
60
+ - lib/rubcask/hinted_file.rb
61
+ - lib/rubcask/keydir_entry.rb
62
+ - lib/rubcask/merge_directory.rb
63
+ - lib/rubcask/protocol.rb
64
+ - lib/rubcask/server/abstract_server.rb
65
+ - lib/rubcask/server/async.rb
66
+ - lib/rubcask/server/client.rb
67
+ - lib/rubcask/server/config.rb
68
+ - lib/rubcask/server/pipeline.rb
69
+ - lib/rubcask/server/runner.rb
70
+ - lib/rubcask/server/runner/config.rb
71
+ - lib/rubcask/server/threaded.rb
72
+ - lib/rubcask/task/clean_directory.rb
73
+ - lib/rubcask/tombstone.rb
74
+ - lib/rubcask/version.rb
75
+ - lib/rubcask/worker/direct_worker.rb
76
+ - lib/rubcask/worker/factory.rb
77
+ - lib/rubcask/worker/ractor_worker.rb
78
+ - lib/rubcask/worker/thread_worker.rb
79
+ homepage: https://github.com/mhib/rubcask
80
+ licenses:
81
+ - MIT
82
+ metadata: {}
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: 2.7.0
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubygems_version: 3.4.1
99
+ signing_key:
100
+ specification_version: 4
101
+ summary: Key/Value storage library
102
+ test_files: []