redis-memo 0.0.0.beta.4 → 0.1.2
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 +4 -4
- data/lib/redis_memo/memoizable/dependency.rb +18 -17
- data/lib/redis_memo/memoize_method.rb +77 -3
- data/lib/redis_memo/memoize_query/cached_select.rb +2 -12
- data/lib/redis_memo/memoize_query/invalidation.rb +133 -78
- data/lib/redis_memo/options.rb +0 -1
- data/lib/redis_memo/redis.rb +2 -1
- data/lib/redis_memo/testing.rb +55 -0
- metadata +4 -3
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 976988ff54686693396ffb07c47b9e4326646837b3be53264891c886f03771c6
         | 
| 4 | 
            +
              data.tar.gz: 9e4a3d4dca78cb9389af8e1728addb3aa8d04493365bcfda2ea236e20c657971
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 118d801be413e14616becfb3a9c4e4715a1e7b7cb6722ac48bd8b1c63e6b1c8f565c8a148909b4bd7b4b538f89d1d79145ad137403139eb7158cad61c9d7a847
         | 
| 7 | 
            +
              data.tar.gz: 52bdf762fc8bf22a6b65fc158512e001dd12490904db9766cd9f3ff1aee74e61f23f218bfa46cdf57fdb67f9f0d2508649fedc2379d6224d0d34416ffbafb902
         | 
| @@ -26,14 +26,18 @@ class RedisMemo::Memoizable::Dependency | |
| 26 26 | 
             
                    instance_exec(&memo.depends_on)
         | 
| 27 27 | 
             
                  end
         | 
| 28 28 | 
             
                when ActiveRecord::Relation
         | 
| 29 | 
            -
                  extracted =  | 
| 29 | 
            +
                  extracted = self.class.extract_from_relation(dependency)
         | 
| 30 30 | 
             
                  nodes.merge!(extracted.nodes)
         | 
| 31 | 
            -
                when  | 
| 32 | 
            -
                   | 
| 33 | 
            -
             | 
| 34 | 
            -
                     | 
| 35 | 
            -
                  ].each do |memo|
         | 
| 31 | 
            +
                when RedisMemo::MemoizeQuery::CachedSelect::BindParams
         | 
| 32 | 
            +
                  # A private API
         | 
| 33 | 
            +
                  dependency.params.each do |model, attrs_set|
         | 
| 34 | 
            +
                    memo = model.redis_memo_class_memoizable
         | 
| 36 35 | 
             
                    nodes[memo.cache_key] = memo
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                    attrs_set.each do |attrs|
         | 
| 38 | 
            +
                      memo = RedisMemo::MemoizeQuery.create_memo(model, **attrs)
         | 
| 39 | 
            +
                      nodes[memo.cache_key] = memo
         | 
| 40 | 
            +
                    end
         | 
| 37 41 | 
             
                  end
         | 
| 38 42 | 
             
                else
         | 
| 39 43 | 
             
                  raise(
         | 
| @@ -43,24 +47,21 @@ class RedisMemo::Memoizable::Dependency | |
| 43 47 | 
             
                end
         | 
| 44 48 | 
             
              end
         | 
| 45 49 |  | 
| 46 | 
            -
               | 
| 50 | 
            +
              private
         | 
| 51 | 
            +
             | 
| 52 | 
            +
              def self.extract_from_relation(relation)
         | 
| 47 53 | 
             
                # Extract the dependent memos of an Arel without calling exec_query to actually execute the query
         | 
| 48 54 | 
             
                RedisMemo::MemoizeQuery::CachedSelect.with_new_query_context do
         | 
| 49 55 | 
             
                  connection = ActiveRecord::Base.connection
         | 
| 50 56 | 
             
                  query, binds, _ = connection.send(:to_sql_and_binds, relation.arel)
         | 
| 51 57 | 
             
                  RedisMemo::MemoizeQuery::CachedSelect.current_query = relation.arel
         | 
| 52 58 | 
             
                  is_query_cached = RedisMemo::MemoizeQuery::CachedSelect.extract_bind_params(query)
         | 
| 53 | 
            -
                    raise(
         | 
| 54 | 
            -
                      RedisMemo::WithoutMemoization,
         | 
| 55 | 
            -
                      "Arel query is not cached using RedisMemo."
         | 
| 56 | 
            -
                    ) unless is_query_cached
         | 
| 57 | 
            -
                    extracted_dependency = connection.dependency_of(:exec_query, query, nil, binds)
         | 
| 58 | 
            -
                end
         | 
| 59 | 
            -
              end
         | 
| 60 59 |  | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
                   | 
| 60 | 
            +
                  unless is_query_cached
         | 
| 61 | 
            +
                    raise RedisMemo::WithoutMemoization, 'Arel query is not cached using RedisMemo'
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  connection.dependency_of(:exec_query, query, nil, binds)
         | 
| 64 65 | 
             
                end
         | 
| 65 66 | 
             
              end
         | 
| 66 67 | 
             
            end
         | 
| @@ -81,14 +81,88 @@ module RedisMemo::MemoizeMethod | |
| 81 81 |  | 
| 82 82 | 
             
              def self.get_or_extract_dependencies(ref, *method_args, &depends_on)
         | 
| 83 83 | 
             
                if RedisMemo::Cache.local_dependency_cache
         | 
| 84 | 
            -
                  RedisMemo::Cache.local_dependency_cache[ref] ||= {}
         | 
| 85 | 
            -
                  RedisMemo::Cache.local_dependency_cache[ref][depends_on] ||= {}
         | 
| 86 | 
            -
                   | 
| 84 | 
            +
                  RedisMemo::Cache.local_dependency_cache[ref.class] ||= {}
         | 
| 85 | 
            +
                  RedisMemo::Cache.local_dependency_cache[ref.class][depends_on] ||= {}
         | 
| 86 | 
            +
                  named_args = exclude_anonymous_args(depends_on, ref, method_args)
         | 
| 87 | 
            +
                  RedisMemo::Cache.local_dependency_cache[ref.class][depends_on][named_args] ||= extract_dependencies(ref, *method_args, &depends_on)
         | 
| 87 88 | 
             
                else
         | 
| 88 89 | 
             
                  extract_dependencies(ref, *method_args, &depends_on)
         | 
| 89 90 | 
             
                end
         | 
| 90 91 | 
             
              end
         | 
| 91 92 |  | 
| 93 | 
            +
              # We only look at named method parameters in the dependency block in order to define its dependent
         | 
| 94 | 
            +
              # memos and ignore anonymous parameters, following the convention that nil or :_ is an anonymous parameter.
         | 
| 95 | 
            +
              # Example:
         | 
| 96 | 
            +
              # ```
         | 
| 97 | 
            +
              #    def method(param1, param2)
         | 
| 98 | 
            +
              #    end
         | 
| 99 | 
            +
              #
         | 
| 100 | 
            +
              #    memoize_method :method do |_, _, param2|`
         | 
| 101 | 
            +
              #      depends_on RedisMemo::Memoizable.new(param2: param2)
         | 
| 102 | 
            +
              #    end
         | 
| 103 | 
            +
              # ```
         | 
| 104 | 
            +
              #  `exclude_anonymous_args(depends_on, ref, [1, 2])` returns [2]
         | 
| 105 | 
            +
              def self.exclude_anonymous_args(depends_on, ref, args)
         | 
| 106 | 
            +
                return [] if depends_on.parameters.empty? or args.empty?
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                positional_args = []
         | 
| 109 | 
            +
                kwargs = {}
         | 
| 110 | 
            +
                depends_on_args = [ref] + args
         | 
| 111 | 
            +
                options = depends_on_args.extract_options!
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                # Keep track of the splat start index, and the number of positional args before and after the splat,
         | 
| 114 | 
            +
                # so we can map which args belong to positional args and which args belong to the splat.
         | 
| 115 | 
            +
                named_splat = false
         | 
| 116 | 
            +
                splat_index = nil
         | 
| 117 | 
            +
                num_positional_args_after_splat = 0
         | 
| 118 | 
            +
                num_positional_args_before_splat = 0
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                depends_on.parameters.each_with_index do |param, i|
         | 
| 121 | 
            +
                  # Defined by https://github.com/ruby/ruby/blob/22b8ddfd1049c3fd1e368684c4fd03bceb041b3a/proc.c#L3048-L3059
         | 
| 122 | 
            +
                  case param.first
         | 
| 123 | 
            +
                  when :opt, :req
         | 
| 124 | 
            +
                    if splat_index
         | 
| 125 | 
            +
                      num_positional_args_after_splat += 1
         | 
| 126 | 
            +
                    else
         | 
| 127 | 
            +
                      num_positional_args_before_splat += 1
         | 
| 128 | 
            +
                    end
         | 
| 129 | 
            +
                  when :rest
         | 
| 130 | 
            +
                    named_splat = is_named?(param)
         | 
| 131 | 
            +
                    splat_index = i
         | 
| 132 | 
            +
                  when :key, :keyreq
         | 
| 133 | 
            +
                    kwargs[param.last] = options[param.last] if is_named?(param)
         | 
| 134 | 
            +
                  when :keyrest
         | 
| 135 | 
            +
                    kwargs.merge!(options) if is_named?(param)
         | 
| 136 | 
            +
                  else
         | 
| 137 | 
            +
                    raise(RedisMemo::ArgumentError, "#{param.first} argument isn't supported in the dependency block")
         | 
| 138 | 
            +
                  end
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                # Determine the named positional and splat arguments after we know the # of pos. arguments before and after splat
         | 
| 142 | 
            +
                after_splat_index = depends_on_args.size - num_positional_args_after_splat
         | 
| 143 | 
            +
                depends_on_args.each_with_index do |arg, i|
         | 
| 144 | 
            +
                  # if the index is within the splat
         | 
| 145 | 
            +
                  if i >= num_positional_args_before_splat && i < after_splat_index
         | 
| 146 | 
            +
                    positional_args << arg if named_splat
         | 
| 147 | 
            +
                  else
         | 
| 148 | 
            +
                    j = i < num_positional_args_before_splat ? i : i - (after_splat_index - splat_index) - 1
         | 
| 149 | 
            +
                    positional_args << arg if is_named?(depends_on.parameters[j])
         | 
| 150 | 
            +
                  end
         | 
| 151 | 
            +
                end
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                if !kwargs.empty?
         | 
| 154 | 
            +
                  positional_args + [kwargs]
         | 
| 155 | 
            +
                elsif named_splat && !options.empty?
         | 
| 156 | 
            +
                  positional_args + [options]
         | 
| 157 | 
            +
                else
         | 
| 158 | 
            +
                  positional_args
         | 
| 159 | 
            +
                end
         | 
| 160 | 
            +
              end
         | 
| 161 | 
            +
            private
         | 
| 162 | 
            +
              def self.is_named?(param)
         | 
| 163 | 
            +
                param.size == 2 && param.last != :_
         | 
| 164 | 
            +
              end
         | 
| 165 | 
            +
             | 
| 92 166 | 
             
              def self.method_cache_keys(future_contexts)
         | 
| 93 167 | 
             
                memos = Array.new(future_contexts.size)
         | 
| 94 168 | 
             
                future_contexts.each_with_index do |(_, _, dependent_memos), i|
         | 
| @@ -111,15 +111,8 @@ class RedisMemo::MemoizeQuery::CachedSelect | |
| 111 111 | 
             
                      sql.gsub(/(\$\d+)/, '?')      # $1 -> ?
         | 
| 112 112 | 
             
                         .gsub(/((, *)*\?)+/, '?')  # (?, ?, ? ...) -> (?)
         | 
| 113 113 | 
             
                    end,
         | 
| 114 | 
            -
                  ) do |_, sql,  | 
| 115 | 
            -
                    RedisMemo::MemoizeQuery::CachedSelect
         | 
| 116 | 
            -
                      .current_query_bind_params
         | 
| 117 | 
            -
                      .params
         | 
| 118 | 
            -
                      .each do |model, attrs_set|
         | 
| 119 | 
            -
                        attrs_set.each do |attrs|
         | 
| 120 | 
            -
                          depends_on model, **attrs
         | 
| 121 | 
            -
                        end
         | 
| 122 | 
            -
                      end
         | 
| 114 | 
            +
                  ) do |_, sql, _, binds, **|
         | 
| 115 | 
            +
                    depends_on RedisMemo::MemoizeQuery::CachedSelect.current_query_bind_params
         | 
| 123 116 |  | 
| 124 117 | 
             
                    depends_on RedisMemo::Memoizable.new(
         | 
| 125 118 | 
             
                      __redis_memo_memoize_query_memoize_query_sql__: sql,
         | 
| @@ -274,9 +267,6 @@ class RedisMemo::MemoizeQuery::CachedSelect | |
| 274 267 |  | 
| 275 268 | 
             
                  bind_params
         | 
| 276 269 | 
             
                when Arel::Nodes::SelectStatement
         | 
| 277 | 
            -
                  # No OREDER BY
         | 
| 278 | 
            -
                  return unless node.orders.empty?
         | 
| 279 | 
            -
             | 
| 280 270 | 
             
                  node.cores.each do |core|
         | 
| 281 271 | 
             
                    # We don't support JOINs
         | 
| 282 272 | 
             
                    return unless core.source.right.empty?
         | 
| @@ -36,10 +36,8 @@ class RedisMemo::MemoizeQuery::Invalidation | |
| 36 36 | 
             
                  decrement_counter
         | 
| 37 37 | 
             
                  delete_all delete_by
         | 
| 38 38 | 
             
                  increment_counter
         | 
| 39 | 
            -
                  insert insert! insert_all insert_all!
         | 
| 40 39 | 
             
                  touch_all
         | 
| 41 40 | 
             
                  update_column update_columns update_all update_counters
         | 
| 42 | 
            -
                  upsert upsert_all
         | 
| 43 41 | 
             
                ).each do |method_name|
         | 
| 44 42 | 
             
                  # Example: Model.update_all
         | 
| 45 43 | 
             
                  rewrite_default_method(
         | 
| @@ -58,6 +56,24 @@ class RedisMemo::MemoizeQuery::Invalidation | |
| 58 56 | 
             
                  )
         | 
| 59 57 | 
             
                end
         | 
| 60 58 |  | 
| 59 | 
            +
                %i(
         | 
| 60 | 
            +
                  insert insert! insert_all insert_all!
         | 
| 61 | 
            +
                ).each do |method_name|
         | 
| 62 | 
            +
                  rewrite_insert_method(
         | 
| 63 | 
            +
                    model_class,
         | 
| 64 | 
            +
                    method_name,
         | 
| 65 | 
            +
                  )
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                %i(
         | 
| 69 | 
            +
                  upsert upsert_all
         | 
| 70 | 
            +
                ).each do |method_name|
         | 
| 71 | 
            +
                  rewrite_upsert_method(
         | 
| 72 | 
            +
                    model_class,
         | 
| 73 | 
            +
                    method_name,
         | 
| 74 | 
            +
                  )
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 61 77 | 
             
                %i(
         | 
| 62 78 | 
             
                  import import!
         | 
| 63 79 | 
             
                ).each do |method_name|
         | 
| @@ -70,6 +86,36 @@ class RedisMemo::MemoizeQuery::Invalidation | |
| 70 86 | 
             
                model_class.class_variable_set(var_name, true)
         | 
| 71 87 | 
             
              end
         | 
| 72 88 |  | 
| 89 | 
            +
              def self.invalidate_new_records(model_class, &blk)
         | 
| 90 | 
            +
                current_id = model_class.maximum(model_class.primary_key)
         | 
| 91 | 
            +
                result = blk.call
         | 
| 92 | 
            +
                records = select_by_new_ids(model_class, current_id)
         | 
| 93 | 
            +
                RedisMemo::MemoizeQuery.invalidate(*records) unless records.empty?
         | 
| 94 | 
            +
                result
         | 
| 95 | 
            +
              end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
              def self.invalidate_records_by_conflict_target(model_class, records:, conflict_target: nil, &blk)
         | 
| 98 | 
            +
                if conflict_target.nil?
         | 
| 99 | 
            +
                  # When the conflict_target is not set, we are basically inserting new
         | 
| 100 | 
            +
                  # records since duplicate rows are simply skipped
         | 
| 101 | 
            +
                  return invalidate_new_records(model_class, &blk)
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                relation = build_relation_by_conflict_target(model_class, records, conflict_target)
         | 
| 105 | 
            +
                # Invalidate records before updating
         | 
| 106 | 
            +
                records = select_by_conflict_target_relation(model_class, relation)
         | 
| 107 | 
            +
                RedisMemo::MemoizeQuery.invalidate(*records) unless records.empty?
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                # Perform updating
         | 
| 110 | 
            +
                result = blk.call
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                # Invalidate records after updating
         | 
| 113 | 
            +
                records = select_by_conflict_target_relation(model_class, relation)
         | 
| 114 | 
            +
                RedisMemo::MemoizeQuery.invalidate(*records) unless records.empty?
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                result
         | 
| 117 | 
            +
              end
         | 
| 118 | 
            +
             | 
| 73 119 | 
             
              private
         | 
| 74 120 |  | 
| 75 121 | 
             
              #
         | 
| @@ -95,12 +141,48 @@ class RedisMemo::MemoizeQuery::Invalidation | |
| 95 141 | 
             
                end
         | 
| 96 142 | 
             
              end
         | 
| 97 143 |  | 
| 98 | 
            -
              def self. | 
| 99 | 
            -
                 | 
| 100 | 
            -
             | 
| 101 | 
            -
             | 
| 102 | 
            -
                   | 
| 144 | 
            +
              def self.rewrite_insert_method(model_class, method_name)
         | 
| 145 | 
            +
                return unless model_class.respond_to?(method_name)
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                model_class.singleton_class.class_eval do
         | 
| 148 | 
            +
                  alias_method :"#{method_name}_without_redis_memo_invalidation", method_name
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                  define_method method_name do |*args, &blk|
         | 
| 151 | 
            +
                    RedisMemo::MemoizeQuery::Invalidation.invalidate_new_records(model_class) do
         | 
| 152 | 
            +
                      send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
         | 
| 153 | 
            +
                    end
         | 
| 154 | 
            +
                  end
         | 
| 103 155 | 
             
                end
         | 
| 156 | 
            +
              end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
              def self.rewrite_upsert_method(model_class, method_name)
         | 
| 159 | 
            +
                return unless model_class.respond_to?(method_name)
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                model_class.singleton_class.class_eval do
         | 
| 162 | 
            +
                  alias_method :"#{method_name}_without_redis_memo_invalidation", method_name
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                  define_method method_name do |attributes, unique_by: nil, **kwargs, &blk|
         | 
| 165 | 
            +
                    RedisMemo::MemoizeQuery::Invalidation.invalidate_records_by_conflict_target(
         | 
| 166 | 
            +
                      model_class,
         | 
| 167 | 
            +
                      records: nil, # not used
         | 
| 168 | 
            +
                      # upsert does not support on_duplicate_key_update yet at activerecord
         | 
| 169 | 
            +
                      # HEAD (6.1.3)
         | 
| 170 | 
            +
                      conflict_target: nil,
         | 
| 171 | 
            +
                    ) do
         | 
| 172 | 
            +
                      send(
         | 
| 173 | 
            +
                        :"#{method_name}_without_redis_memo_invalidation",
         | 
| 174 | 
            +
                        attributes,
         | 
| 175 | 
            +
                        unique_by: unique_by,
         | 
| 176 | 
            +
                        **kwargs,
         | 
| 177 | 
            +
                        &blk
         | 
| 178 | 
            +
                      )
         | 
| 179 | 
            +
                    end
         | 
| 180 | 
            +
                  end
         | 
| 181 | 
            +
                end
         | 
| 182 | 
            +
              end
         | 
| 183 | 
            +
             | 
| 184 | 
            +
              def self.rewrite_import_method(model_class, method_name)
         | 
| 185 | 
            +
                return unless model_class.respond_to?(method_name)
         | 
| 104 186 |  | 
| 105 187 | 
             
                model_class.singleton_class.class_eval do
         | 
| 106 188 | 
             
                  alias_method :"#{method_name}_without_redis_memo_invalidation", method_name
         | 
| @@ -110,102 +192,75 @@ class RedisMemo::MemoizeQuery::Invalidation | |
| 110 192 | 
             
                  define_method method_name do |*args, &blk|
         | 
| 111 193 | 
             
                    options = args.last.is_a?(Hash) ? args.last : {}
         | 
| 112 194 | 
             
                    records = args[args.last.is_a?(Hash) ? -2 : -1]
         | 
| 113 | 
            -
                     | 
| 114 | 
            -
                     | 
| 115 | 
            -
                       | 
| 116 | 
            -
             | 
| 117 | 
            -
             | 
| 118 | 
            -
             | 
| 119 | 
            -
             | 
| 120 | 
            -
             | 
| 121 | 
            -
             | 
| 122 | 
            -
             | 
| 123 | 
            -
                    # - default values filled by the database
         | 
| 124 | 
            -
                    # - updates on conflict conditions
         | 
| 125 | 
            -
                    records_to_invalidate =
         | 
| 126 | 
            -
                      if columns_to_update
         | 
| 127 | 
            -
                        RedisMemo::MemoizeQuery::Invalidation.send(
         | 
| 128 | 
            -
                          :select_by_columns,
         | 
| 129 | 
            -
                          records,
         | 
| 130 | 
            -
                          columns_to_update,
         | 
| 131 | 
            -
                        )
         | 
| 195 | 
            +
                    on_duplicate_key_update = options[:on_duplicate_key_update]
         | 
| 196 | 
            +
                    conflict_target =
         | 
| 197 | 
            +
                      case on_duplicate_key_update
         | 
| 198 | 
            +
                      when Hash
         | 
| 199 | 
            +
                        # The conflict_target option is only supported in PostgreSQL. In
         | 
| 200 | 
            +
                        # MySQL, the primary_key is used as the conflict_target
         | 
| 201 | 
            +
                        on_duplicate_key_update[:conflict_target] || [model_class.primary_key.to_sym]
         | 
| 202 | 
            +
                      when Array
         | 
| 203 | 
            +
                        # The default conflict_target is just the primary_key
         | 
| 204 | 
            +
                        [model_class.primary_key.to_sym]
         | 
| 132 205 | 
             
                      else
         | 
| 133 | 
            -
                         | 
| 206 | 
            +
                        # Ignore duplicate rows
         | 
| 207 | 
            +
                        nil
         | 
| 134 208 | 
             
                      end
         | 
| 135 209 |  | 
| 136 | 
            -
                     | 
| 137 | 
            -
             | 
| 138 | 
            -
                    # Offload the records to invalidate while selecting the next set of
         | 
| 139 | 
            -
                    # records to invalidate
         | 
| 140 | 
            -
                    case records_to_invalidate
         | 
| 141 | 
            -
                    when Array
         | 
| 142 | 
            -
                      RedisMemo::MemoizeQuery.invalidate(*records_to_invalidate) unless records_to_invalidate.empty?
         | 
| 143 | 
            -
             | 
| 144 | 
            -
                      RedisMemo::MemoizeQuery.invalidate(*RedisMemo::MemoizeQuery::Invalidation.send(
         | 
| 145 | 
            -
                        :select_by_id,
         | 
| 146 | 
            -
                        model_class,
         | 
| 147 | 
            -
                        # Not all databases support "RETURNING", which is useful when
         | 
| 148 | 
            -
                        # invaldating records after bulk creation
         | 
| 149 | 
            -
                        result.ids,
         | 
| 150 | 
            -
                      ))
         | 
| 151 | 
            -
                    else
         | 
| 152 | 
            -
                      RedisMemo::MemoizeQuery.invalidate_all(model_class)
         | 
| 210 | 
            +
                    if conflict_target && records.last.is_a?(Hash)
         | 
| 211 | 
            +
                      records.map! { |hash| model_class.new(hash) }
         | 
| 153 212 | 
             
                    end
         | 
| 154 213 |  | 
| 155 | 
            -
                     | 
| 214 | 
            +
                    RedisMemo::MemoizeQuery::Invalidation.invalidate_records_by_conflict_target(
         | 
| 215 | 
            +
                      model_class,
         | 
| 216 | 
            +
                      records: records,
         | 
| 217 | 
            +
                      conflict_target: conflict_target,
         | 
| 218 | 
            +
                    ) do
         | 
| 219 | 
            +
                      send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
         | 
| 220 | 
            +
                    end
         | 
| 156 221 | 
             
                  end
         | 
| 157 222 | 
             
                end
         | 
| 158 223 | 
             
              end
         | 
| 159 224 |  | 
| 160 | 
            -
              def self. | 
| 161 | 
            -
                model_class = records.first.class
         | 
| 225 | 
            +
              def self.build_relation_by_conflict_target(model_class, records, conflict_target)
         | 
| 162 226 | 
             
                or_chain = nil
         | 
| 163 | 
            -
                columns_to_select = columns_to_update & RedisMemo::MemoizeQuery
         | 
| 164 | 
            -
                  .memoized_columns(model_class)
         | 
| 165 | 
            -
                  .to_a.flatten.uniq
         | 
| 166 | 
            -
             | 
| 167 | 
            -
                # Nothing to invalidate here
         | 
| 168 | 
            -
                return [] if columns_to_select.empty?
         | 
| 169 227 |  | 
| 170 | 
            -
                 | 
| 171 | 
            -
                   | 
| 172 | 
            -
                   | 
| 173 | 
            -
             | 
| 174 | 
            -
                  records.each do |record|
         | 
| 175 | 
            -
                    conditions = {}
         | 
| 176 | 
            -
                    columns_to_select.each do |column|
         | 
| 177 | 
            -
                      conditions[column] = record.send(column)
         | 
| 178 | 
            -
                    end
         | 
| 179 | 
            -
                    if or_chain
         | 
| 180 | 
            -
                      or_chain = or_chain.or(model_class.where(conditions))
         | 
| 181 | 
            -
                    else
         | 
| 182 | 
            -
                      or_chain = model_class.where(conditions)
         | 
| 183 | 
            -
                    end
         | 
| 228 | 
            +
                records.each do |record|
         | 
| 229 | 
            +
                  conditions = {}
         | 
| 230 | 
            +
                  conflict_target.each do |column|
         | 
| 231 | 
            +
                    conditions[column] = record.send(column)
         | 
| 184 232 | 
             
                  end
         | 
| 185 | 
            -
             | 
| 186 | 
            -
             | 
| 187 | 
            -
                  if record_count > bulk_operations_invalidation_limit
         | 
| 188 | 
            -
                    nil
         | 
| 233 | 
            +
                  if or_chain
         | 
| 234 | 
            +
                    or_chain = or_chain.or(model_class.where(conditions))
         | 
| 189 235 | 
             
                  else
         | 
| 190 | 
            -
                     | 
| 236 | 
            +
                    or_chain = model_class.where(conditions)
         | 
| 191 237 | 
             
                  end
         | 
| 192 238 | 
             
                end
         | 
| 239 | 
            +
             | 
| 240 | 
            +
                or_chain
         | 
| 193 241 | 
             
              end
         | 
| 194 242 |  | 
| 195 | 
            -
              def self. | 
| 243 | 
            +
              def self.select_by_new_ids(model_class, target_id)
         | 
| 196 244 | 
             
                RedisMemo::Tracer.trace(
         | 
| 197 245 | 
             
                  'redis_memo.memoize_query.invalidation',
         | 
| 198 246 | 
             
                  "#{__method__}##{model_class.name}",
         | 
| 199 247 | 
             
                ) do
         | 
| 200 248 | 
             
                  RedisMemo.without_memo do
         | 
| 201 | 
            -
                    model_class.where( | 
| 249 | 
            +
                    model_class.where(
         | 
| 250 | 
            +
                      model_class.arel_table[model_class.primary_key].gt(target_id),
         | 
| 251 | 
            +
                    ).to_a
         | 
| 202 252 | 
             
                  end
         | 
| 203 253 | 
             
                end
         | 
| 204 254 | 
             
              end
         | 
| 205 255 |  | 
| 206 | 
            -
              def self. | 
| 207 | 
            -
                 | 
| 208 | 
            -
             | 
| 209 | 
            -
             | 
| 256 | 
            +
              def self.select_by_conflict_target_relation(model_class, relation)
         | 
| 257 | 
            +
                return [] unless relation
         | 
| 258 | 
            +
             | 
| 259 | 
            +
                RedisMemo::Tracer.trace(
         | 
| 260 | 
            +
                  'redis_memo.memoize_query.invalidation',
         | 
| 261 | 
            +
                  "#{__method__}##{model_class.name}",
         | 
| 262 | 
            +
                ) do
         | 
| 263 | 
            +
                  RedisMemo.without_memo { relation.reload }
         | 
| 264 | 
            +
                end
         | 
| 210 265 | 
             
              end
         | 
| 211 266 | 
             
            end
         | 
    
        data/lib/redis_memo/options.rb
    CHANGED
    
    
    
        data/lib/redis_memo/redis.rb
    CHANGED
    
    | @@ -31,7 +31,8 @@ class RedisMemo::Redis < Redis::Distributed | |
| 31 31 | 
             
              end
         | 
| 32 32 |  | 
| 33 33 | 
             
              class WithReplicas < ::Redis
         | 
| 34 | 
            -
                def initialize( | 
| 34 | 
            +
                def initialize(orig_options)
         | 
| 35 | 
            +
                  options = orig_options.dup
         | 
| 35 36 | 
             
                  primary_option = options.shift
         | 
| 36 37 | 
             
                  @replicas = options.map do |option|
         | 
| 37 38 | 
             
                    option[:logger] ||= RedisMemo::DefaultOptions.logger
         | 
| @@ -0,0 +1,55 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Redis memo can be flaky due to transient network errors (e.g. Redis connection errors), or when
         | 
| 4 | 
            +
            # used with async handlers. This class allows users to override the default redis-memo behavior
         | 
| 5 | 
            +
            # to be more robust when testing their code that uses redis-memo.
         | 
| 6 | 
            +
            module RedisMemo
         | 
| 7 | 
            +
              class Testing
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def self.__test_mode
         | 
| 10 | 
            +
                  @__test_mode
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def self.__test_mode=(mode)
         | 
| 14 | 
            +
                  @__test_mode = mode
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def self.enable_test_mode(&blk)
         | 
| 18 | 
            +
                  __set_test_mode(true, &blk)
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def self.disable_test_mode(&blk)
         | 
| 22 | 
            +
                  __set_test_mode(false, &blk)
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def self.enabled?
         | 
| 26 | 
            +
                  __test_mode
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                private
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def self.__set_test_mode(mode, &blk)
         | 
| 32 | 
            +
                  if blk.nil?
         | 
| 33 | 
            +
                    __test_mode = mode
         | 
| 34 | 
            +
                  else
         | 
| 35 | 
            +
                    prev_mode = __test_mode
         | 
| 36 | 
            +
                    begin
         | 
| 37 | 
            +
                      __test_mode = mode
         | 
| 38 | 
            +
                      yield
         | 
| 39 | 
            +
                    ensure
         | 
| 40 | 
            +
                      __test_mode = prev_mode
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
              module TestOverrides
         | 
| 47 | 
            +
                def without_memo?
         | 
| 48 | 
            +
                  if RedisMemo::Testing.enabled? && !RedisMemo::Memoizable::Invalidation.class_variable_get(:@@invalidation_queue).empty?
         | 
| 49 | 
            +
                    return true
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
                  super
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
              singleton_class.prepend(TestOverrides)
         | 
| 55 | 
            +
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: redis-memo
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.1.2
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Chan Zuckerberg Initiative
         | 
| @@ -192,6 +192,7 @@ files: | |
| 192 192 | 
             
            - lib/redis_memo/middleware.rb
         | 
| 193 193 | 
             
            - lib/redis_memo/options.rb
         | 
| 194 194 | 
             
            - lib/redis_memo/redis.rb
         | 
| 195 | 
            +
            - lib/redis_memo/testing.rb
         | 
| 195 196 | 
             
            - lib/redis_memo/tracer.rb
         | 
| 196 197 | 
             
            homepage: https://github.com/chanzuckerberg/redis-memo
         | 
| 197 198 | 
             
            licenses:
         | 
| @@ -208,9 +209,9 @@ required_ruby_version: !ruby/object:Gem::Requirement | |
| 208 209 | 
             
                  version: 2.5.0
         | 
| 209 210 | 
             
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 210 211 | 
             
              requirements:
         | 
| 211 | 
            -
              - - " | 
| 212 | 
            +
              - - ">="
         | 
| 212 213 | 
             
                - !ruby/object:Gem::Version
         | 
| 213 | 
            -
                  version:  | 
| 214 | 
            +
                  version: '0'
         | 
| 214 215 | 
             
            requirements: []
         | 
| 215 216 | 
             
            rubygems_version: 3.0.8
         | 
| 216 217 | 
             
            signing_key: 
         |