rubcask 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []