redcord 0.0.1.alpha
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.
- checksums.yaml +7 -0
- data/lib/redcord.rb +11 -0
- data/lib/redcord.rbi +94 -0
- data/lib/redcord/actions.rb +135 -0
- data/lib/redcord/attribute.rb +75 -0
- data/lib/redcord/base.rb +59 -0
- data/lib/redcord/configurations.rb +72 -0
- data/lib/redcord/logger.rb +69 -0
- data/lib/redcord/lua_script_reader.rb +16 -0
- data/lib/redcord/migration.rb +27 -0
- data/lib/redcord/migration/migrator.rb +74 -0
- data/lib/redcord/migration/ttl.rb +11 -0
- data/lib/redcord/migration/version.rb +40 -0
- data/lib/redcord/prepared_redis.rb +18 -0
- data/lib/redcord/railtie.rb +16 -0
- data/lib/redcord/range_interval.rb +9 -0
- data/lib/redcord/redis_connection.rb +103 -0
- data/lib/redcord/relation.rb +95 -0
- data/lib/redcord/serializer.rb +129 -0
- data/lib/redcord/server_scripts.rb +78 -0
- data/lib/redcord/server_scripts/create_hash_returning_id.erb.lua +68 -0
- data/lib/redcord/server_scripts/delete_hash.erb.lua +48 -0
- data/lib/redcord/server_scripts/find_by_attr.erb.lua +67 -0
- data/lib/redcord/server_scripts/find_by_attr_count.erb.lua +52 -0
- data/lib/redcord/server_scripts/shared/index_helper_methods.erb.lua +61 -0
- data/lib/redcord/server_scripts/shared/lua_helper_methods.erb.lua +33 -0
- data/lib/redcord/server_scripts/shared/query_helper_methods.erb.lua +105 -0
- data/lib/redcord/server_scripts/update_hash.erb.lua +91 -0
- data/lib/redcord/tasks/redis.rake +34 -0
- metadata +210 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require 'rails'
|
3
|
+
module Redcord::Logger
|
4
|
+
extend T::Sig
|
5
|
+
extend T::Helpers
|
6
|
+
|
7
|
+
sig { params(klass: Module).void }
|
8
|
+
def self.included(klass)
|
9
|
+
klass.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
@@logger = T.let(Rails.logger, T.untyped)
|
16
|
+
|
17
|
+
sig { returns(T.untyped) }
|
18
|
+
def logger
|
19
|
+
@@logger
|
20
|
+
end
|
21
|
+
|
22
|
+
sig { params(logger: T.untyped).void }
|
23
|
+
def logger=(logger)
|
24
|
+
@@logger = logger
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module LoggerMethods
|
29
|
+
extend T::Sig
|
30
|
+
|
31
|
+
#
|
32
|
+
# Forward all the logger methods call to a module -- almost all logger
|
33
|
+
# methods are missing and handled by method_missing. We use this trick to
|
34
|
+
# dynamically swap loggers without reconfiguring the Redis clients.
|
35
|
+
#
|
36
|
+
sig do
|
37
|
+
params(
|
38
|
+
method: Symbol,
|
39
|
+
args: T.untyped,
|
40
|
+
blk: T.nilable(T.proc.returns(T.untyped))
|
41
|
+
).returns(T.untyped)
|
42
|
+
end
|
43
|
+
def self.method_missing(method, *args, &blk)
|
44
|
+
logger = Redcord::Base.logger
|
45
|
+
return if logger.nil?
|
46
|
+
logger.send(method, *args)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# If we set the logger to nil, but we're not rebuilding the connection(s) at
|
51
|
+
# all.
|
52
|
+
# Example:
|
53
|
+
#
|
54
|
+
# 2.5.5 :001 > Redcord::Base.redis.ping
|
55
|
+
# [Redis] command=PING args=
|
56
|
+
# [Redis] call_time=0.80 ms
|
57
|
+
# => "PONG"
|
58
|
+
# 2.5.5 :002 > Redcord::Base.logger = nil
|
59
|
+
# => nil
|
60
|
+
# 2.5.5 :003 > Redcord::Base.redis.ping # show no logs
|
61
|
+
# => "PONG"
|
62
|
+
#
|
63
|
+
sig { returns(T.untyped) }
|
64
|
+
def self.proxy
|
65
|
+
Redcord::Logger::LoggerMethods
|
66
|
+
end
|
67
|
+
|
68
|
+
mixes_in_class_methods(ClassMethods)
|
69
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# typed: strict
|
2
|
+
module Redcord::LuaScriptReader
|
3
|
+
extend T::Sig
|
4
|
+
|
5
|
+
sig {params(script_name: String).returns(String) }
|
6
|
+
def self.read_lua_script(script_name)
|
7
|
+
path = File.join(File.dirname(__FILE__), "server_scripts/#{script_name}.erb.lua")
|
8
|
+
ERB.new(File.read(path)).result(binding)
|
9
|
+
end
|
10
|
+
|
11
|
+
sig {params(relative_path: String).returns(String) }
|
12
|
+
def self.include_lua(relative_path)
|
13
|
+
path = File.join(File.dirname(__FILE__), "server_scripts/#{relative_path}.erb.lua")
|
14
|
+
File.read(path)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# typed: strict
|
2
|
+
class Redcord::Migration
|
3
|
+
end
|
4
|
+
|
5
|
+
require 'redcord/migration/ttl'
|
6
|
+
|
7
|
+
class Redcord::Migration
|
8
|
+
extend T::Sig
|
9
|
+
extend T::Helpers
|
10
|
+
include Redcord::Migration::TTL
|
11
|
+
|
12
|
+
abstract!
|
13
|
+
|
14
|
+
sig { returns(Redis) }
|
15
|
+
attr_reader :redis
|
16
|
+
|
17
|
+
sig { params(redis: Redis).void }
|
18
|
+
def initialize(redis)
|
19
|
+
@redis = redis
|
20
|
+
end
|
21
|
+
|
22
|
+
sig { abstract.void }
|
23
|
+
def up; end
|
24
|
+
|
25
|
+
sig { abstract.void }
|
26
|
+
def down; end
|
27
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require 'redcord/migration'
|
3
|
+
class Redcord::Migration::Migrator
|
4
|
+
extend T::Sig
|
5
|
+
|
6
|
+
sig { params(redis: Redis).returns(T::Boolean) }
|
7
|
+
def self.need_to_migrate?(redis)
|
8
|
+
local_version = Redcord::Migration::Version.new
|
9
|
+
remote_version = Redcord::Migration::Version.new(redis: redis)
|
10
|
+
!(local_version.all - remote_version.all).empty?
|
11
|
+
end
|
12
|
+
|
13
|
+
sig { params(redis: Redis, version: String, direction: Symbol).void }
|
14
|
+
def self.migrate(redis:, version:, direction:)
|
15
|
+
migration = load_version(version)
|
16
|
+
print [
|
17
|
+
T.must("#{redis.inspect.match('(redis://.*)>')[1]}"[0...30]),
|
18
|
+
direction.to_s.upcase,
|
19
|
+
version,
|
20
|
+
T.must(migration.name).underscore.humanize,
|
21
|
+
].map { |str| str.ljust(30) }.join("\t")
|
22
|
+
|
23
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
24
|
+
migration.new(redis).send(direction)
|
25
|
+
if direction == :up
|
26
|
+
redis.sadd(
|
27
|
+
Redcord::Migration::Version::MIGRATION_VERSIONS_REDIS_KEY,
|
28
|
+
version,
|
29
|
+
)
|
30
|
+
else
|
31
|
+
redis.srem(
|
32
|
+
Redcord::Migration::Version::MIGRATION_VERSIONS_REDIS_KEY,
|
33
|
+
version,
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
37
|
+
puts "\t#{(end_time - start_time) * 1000.0.round(3)} ms"
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
sig { params(version: String).returns(T.class_of(Redcord::Migration)) }
|
43
|
+
def self.load_version(version)
|
44
|
+
file = T.must(migration_files.select { |f| f.match(version) }.first)
|
45
|
+
require(File.expand_path(file))
|
46
|
+
underscore_const_name = parse_migration_filename(file)[1]
|
47
|
+
Object.const_get(underscore_const_name.camelize)
|
48
|
+
end
|
49
|
+
|
50
|
+
MIGRATION_FILENAME_REGEX = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/
|
51
|
+
|
52
|
+
@@migrations_paths = T.let(
|
53
|
+
['db/redisrecord/migrate'],
|
54
|
+
T::Array[String],
|
55
|
+
)
|
56
|
+
|
57
|
+
sig { returns(T::Array[String]) }
|
58
|
+
def self.migrations_paths
|
59
|
+
@@migrations_paths
|
60
|
+
end
|
61
|
+
|
62
|
+
sig { returns(T::Array[String]) }
|
63
|
+
def self.migration_files
|
64
|
+
paths = migrations_paths
|
65
|
+
# Use T.unsafe to workaround sorbet: splat the paths
|
66
|
+
T.unsafe(Dir)[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
sig { params(filename: String).returns([String, String, String]) }
|
71
|
+
def self.parse_migration_filename(filename)
|
72
|
+
T.unsafe(File.basename(filename).scan(MIGRATION_FILENAME_REGEX).first)
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# typed: strict
|
2
|
+
module Redcord::Migration::TTL
|
3
|
+
extend T::Sig
|
4
|
+
|
5
|
+
# This won't change ttl until we call update on a record
|
6
|
+
sig { params(model: T.class_of(Redcord::Base)).void }
|
7
|
+
def change_ttl_passive(model)
|
8
|
+
ttl = model.class_variable_get(:@@ttl)
|
9
|
+
model.redis.set("#{model.model_key}:ttl", ttl ? ttl : -1)
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# typed: strict
|
2
|
+
class Redcord::Migration::Version
|
3
|
+
extend T::Sig
|
4
|
+
|
5
|
+
MIGRATION_VERSIONS_REDIS_KEY = 'Redcord:__migration_versions__'
|
6
|
+
|
7
|
+
sig { params(redis: T.nilable(Redis)).void }
|
8
|
+
def initialize(redis: nil)
|
9
|
+
@redis = T.let(redis, T.nilable(Redis))
|
10
|
+
end
|
11
|
+
|
12
|
+
sig { returns(T.nilable(String)) }
|
13
|
+
def current
|
14
|
+
all.sort.last
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { returns(T::Array[String]) }
|
18
|
+
def all
|
19
|
+
if @redis
|
20
|
+
remote_versions
|
21
|
+
else
|
22
|
+
local_versions
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
sig { returns(T::Array[String]) }
|
29
|
+
def local_versions
|
30
|
+
Redcord::Migration::Migrator.migration_files.map do |filename|
|
31
|
+
fields = Redcord::Migration::Migrator.parse_migration_filename(filename)
|
32
|
+
fields[0]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
sig { returns(T::Array[String]) }
|
37
|
+
def remote_versions
|
38
|
+
T.must(@redis).smembers(MIGRATION_VERSIONS_REDIS_KEY)
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require 'redis'
|
3
|
+
require 'redcord/server_scripts'
|
4
|
+
|
5
|
+
class Redcord::PreparedRedis < Redis
|
6
|
+
extend T::Sig
|
7
|
+
include Redcord::ServerScripts
|
8
|
+
|
9
|
+
sig { returns(T::Hash[Symbol, String]) }
|
10
|
+
def redcord_server_script_shas
|
11
|
+
instance_variable_get(:@_redcord_server_script_shas)
|
12
|
+
end
|
13
|
+
|
14
|
+
sig { params(shas: T::Hash[Symbol, String]).void }
|
15
|
+
def redcord_server_script_shas=(shas)
|
16
|
+
instance_variable_set(:@_redcord_server_script_shas, shas)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require 'rails'
|
3
|
+
|
4
|
+
class Redcord::Railtie < Rails::Railtie
|
5
|
+
railtie_name 'redcord'
|
6
|
+
|
7
|
+
rake_tasks do
|
8
|
+
path = File.expand_path(T.must(__dir__))
|
9
|
+
Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
|
10
|
+
end
|
11
|
+
|
12
|
+
# Load necessary dependency to configure redcord
|
13
|
+
config.before_configuration do
|
14
|
+
require 'redcord/base'
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require 'redcord/attribute'
|
3
|
+
|
4
|
+
class Redcord::RangeInterval < T::Struct
|
5
|
+
prop :min, T.nilable(Redcord::Attribute::RangeIndexType), default: nil
|
6
|
+
prop :min_exclusive, T::Boolean, default: false
|
7
|
+
prop :max, T.nilable(Redcord::Attribute::RangeIndexType), default: nil
|
8
|
+
prop :max_exclusive, T::Boolean, default: false
|
9
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require 'rails'
|
3
|
+
require 'redcord/prepared_redis'
|
4
|
+
require 'redcord/lua_script_reader'
|
5
|
+
|
6
|
+
module Redcord::RedisConnection
|
7
|
+
extend T::Sig
|
8
|
+
extend T::Helpers
|
9
|
+
|
10
|
+
@connections = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
|
11
|
+
@procs_to_prepare = T.let([], T::Array[Proc])
|
12
|
+
|
13
|
+
sig { params(klass: T.any(Module, T.class_of(T::Struct))).void }
|
14
|
+
def self.included(klass)
|
15
|
+
klass.extend(ClassMethods)
|
16
|
+
klass.include(InstanceMethods)
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
extend T::Sig
|
21
|
+
|
22
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
23
|
+
def connection_config
|
24
|
+
env_config = Redcord::Base.configurations[Rails.env]
|
25
|
+
(env_config[name.underscore] || env_config['default']).symbolize_keys
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { returns(Redcord::PreparedRedis) }
|
29
|
+
def redis
|
30
|
+
Redcord::RedisConnection.connections[name.underscore] ||= prepare_redis!
|
31
|
+
end
|
32
|
+
|
33
|
+
sig { returns(Redcord::PreparedRedis) }
|
34
|
+
def establish_connection
|
35
|
+
Redcord::RedisConnection.connections[name.underscore] = prepare_redis!
|
36
|
+
end
|
37
|
+
|
38
|
+
sig { params(redis: Redis).returns(Redcord::PreparedRedis) }
|
39
|
+
def redis=(redis)
|
40
|
+
Redcord::RedisConnection.connections[name.underscore] = prepare_redis!(redis)
|
41
|
+
end
|
42
|
+
|
43
|
+
# We prepare the model definition such as TTL, index, and uniq when we
|
44
|
+
# establish a Redis connection (once per connection) instead of sending the
|
45
|
+
# definitions in each Redis query.
|
46
|
+
#
|
47
|
+
# TODO: Replace this with Redcord migrations
|
48
|
+
sig { params(client: T.nilable(Redis)).returns(Redcord::PreparedRedis) }
|
49
|
+
def prepare_redis!(client=nil)
|
50
|
+
return client if client.is_a?(Redcord::PreparedRedis)
|
51
|
+
|
52
|
+
client = Redcord::PreparedRedis.new(
|
53
|
+
**(client.nil? ? connection_config : client.instance_variable_get(:@options)),
|
54
|
+
logger: Redcord::Logger.proxy,
|
55
|
+
)
|
56
|
+
|
57
|
+
client.pipelined do
|
58
|
+
Redcord::RedisConnection.procs_to_prepare.each do |proc_to_prepare|
|
59
|
+
proc_to_prepare.call(client)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
script_names = Redcord::ServerScripts.instance_methods
|
64
|
+
res = client.pipelined do
|
65
|
+
script_names.each do |script_name|
|
66
|
+
client.script(:load, Redcord::LuaScriptReader.read_lua_script(script_name.to_s))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
client.redcord_server_script_shas = script_names.zip(res).to_h
|
71
|
+
client
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
module InstanceMethods
|
76
|
+
extend T::Sig
|
77
|
+
|
78
|
+
sig { returns(Redcord::PreparedRedis) }
|
79
|
+
def redis
|
80
|
+
self.class.redis
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
sig { params(config: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
|
85
|
+
def self.merge_and_resolve_default(config)
|
86
|
+
env = Rails.env
|
87
|
+
config[env] = {} if !config.include?(env)
|
88
|
+
config[env]['default'] = {} if !config[env].include?('default')
|
89
|
+
config
|
90
|
+
end
|
91
|
+
|
92
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
93
|
+
def self.connections
|
94
|
+
@connections ||= {}
|
95
|
+
end
|
96
|
+
|
97
|
+
sig { returns(T::Array[Proc]) }
|
98
|
+
def self.procs_to_prepare
|
99
|
+
@procs_to_prepare
|
100
|
+
end
|
101
|
+
|
102
|
+
mixes_in_class_methods(ClassMethods)
|
103
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require 'active_support/core_ext/module'
|
3
|
+
|
4
|
+
class Redcord::Relation
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
sig { returns(T.class_of(Redcord::Base)) }
|
8
|
+
attr_reader :model
|
9
|
+
|
10
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
11
|
+
attr_reader :query_conditions
|
12
|
+
|
13
|
+
sig { returns(T::Set[Symbol]) }
|
14
|
+
attr_reader :select_attrs
|
15
|
+
|
16
|
+
# TODO: Add sig for []
|
17
|
+
delegate :[], to: :to_a
|
18
|
+
|
19
|
+
sig do
|
20
|
+
type_parameters(:U).params(
|
21
|
+
blk: T.proc.params(arg0: Redcord::Base).returns(T.type_parameter(:U)),
|
22
|
+
).returns(T::Array[T.type_parameter(:U)])
|
23
|
+
end
|
24
|
+
def map(&blk)
|
25
|
+
to_a.map(&blk)
|
26
|
+
end
|
27
|
+
|
28
|
+
sig do
|
29
|
+
params(
|
30
|
+
model: T.class_of(Redcord::Base),
|
31
|
+
query_conditions: T::Hash[Symbol, T.untyped],
|
32
|
+
select_attrs: T::Set[Symbol]
|
33
|
+
).void
|
34
|
+
end
|
35
|
+
def initialize(model, query_conditions={}, select_attrs=Set.new)
|
36
|
+
@model = model
|
37
|
+
@query_conditions = query_conditions
|
38
|
+
@select_attrs = select_attrs
|
39
|
+
end
|
40
|
+
|
41
|
+
sig { params(args: T::Hash[Symbol, T.untyped]).returns(Redcord::Relation) }
|
42
|
+
def where(args)
|
43
|
+
encoded_args = args.map do |attr_key, attr_val|
|
44
|
+
encoded_val = model.validate_and_encode_query(attr_key, attr_val)
|
45
|
+
[attr_key, encoded_val]
|
46
|
+
end
|
47
|
+
query_conditions.merge!(encoded_args.to_h)
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
sig do
|
52
|
+
params(
|
53
|
+
args: Symbol,
|
54
|
+
blk: T.nilable(T.proc.params(arg0: T.untyped).void),
|
55
|
+
).returns(T.any(Redcord::Relation, T::Array[T.untyped]))
|
56
|
+
end
|
57
|
+
def select(*args, &blk)
|
58
|
+
if block_given?
|
59
|
+
return execute_query.select { |*item| blk.call(*item) }
|
60
|
+
end
|
61
|
+
select_attrs.merge(args)
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
sig { returns(Integer) }
|
66
|
+
def count
|
67
|
+
redis.find_by_attr_count(model.model_key, query_conditions)
|
68
|
+
end
|
69
|
+
|
70
|
+
sig { returns(T::Array[T.untyped]) }
|
71
|
+
def to_a
|
72
|
+
execute_query
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
sig { returns(T::Array[T.untyped]) }
|
77
|
+
def execute_query
|
78
|
+
if !select_attrs.empty?
|
79
|
+
res_hash = redis.find_by_attr(model.model_key, query_conditions, select_attrs)
|
80
|
+
return res_hash.map do |id, args|
|
81
|
+
args = model.from_redis_hash(args)
|
82
|
+
args = args.map { |k, v| [k.to_sym, TypeCoerce[model.get_attr_type(k.to_sym)].new.from(v)] }.to_h
|
83
|
+
args.merge!(:id => id)
|
84
|
+
end
|
85
|
+
else
|
86
|
+
res_hash = redis.find_by_attr(model.model_key, query_conditions)
|
87
|
+
return res_hash.map { |id, args| model.coerce_and_set_id(args, id) }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
sig { returns(Redcord::PreparedRedis) }
|
92
|
+
def redis
|
93
|
+
model.redis
|
94
|
+
end
|
95
|
+
end
|