redcord 0.0.1.alpha
Sign up to get free protection for your applications and to get access to all the features.
- 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
|