redcord 0.0.1.alpha

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