Skip to content

Commit

Permalink
Merge branch 'master' into Add-thread-pool-and-concurrency_max_thread…
Browse files Browse the repository at this point in the history
…s-configuration-option
  • Loading branch information
jkeen authored Mar 28, 2024
2 parents d6fce34 + 764eb93 commit 2f2b941
Show file tree
Hide file tree
Showing 17 changed files with 400 additions and 17 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
graphiti changelog

# [1.7.0](https://github.com/graphiti-api/graphiti/compare/v1.6.4...v1.7.0) (2024-03-27)


### Features

* Add support for caching renders in Graphiti, and better support using etags and stale? in the controller ([#424](https://github.com/graphiti-api/graphiti/issues/424)) ([8bae50a](https://github.com/graphiti-api/graphiti/commit/8bae50ab82559e2644d506e16a4f715effd89317))

## [1.6.4](https://github.com/graphiti-api/graphiti/compare/v1.6.3...v1.6.4) (2024-03-27)

## [1.6.3](https://github.com/graphiti-api/graphiti/compare/v1.6.2...v1.6.3) (2024-03-26)


Expand Down
9 changes: 9 additions & 0 deletions lib/graphiti.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ def self.setup!
r.apply_sideloads_to_serializer
end
end

def self.cache=(val)
@cache = val
end

def self.cache
@cache
end
end

require "graphiti/version"
Expand Down Expand Up @@ -177,6 +185,7 @@ def self.setup!
require "graphiti/serializer"
require "graphiti/query"
require "graphiti/debugger"
require "graphiti/util/cache_debug"

if defined?(ActiveRecord)
require "graphiti/adapters/active_record"
Expand Down
12 changes: 12 additions & 0 deletions lib/graphiti/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class Configuration
attr_reader :debug, :debug_models

attr_writer :schema_path
attr_writer :cache_rendering

# Set defaults
# @api private
Expand All @@ -47,6 +48,7 @@ def initialize
@pagination_links = false
@typecast_reads = true
@raise_on_missing_sidepost = true
@cache_rendering = false
self.debug = ENV.fetch("GRAPHITI_DEBUG", true)
self.debug_models = ENV.fetch("GRAPHITI_DEBUG_MODELS", false)

Expand All @@ -67,6 +69,16 @@ def initialize
end
end

def cache_rendering?
use_caching = @cache_rendering && Graphiti.cache.respond_to?(:fetch)

use_caching.tap do |use|
if @cache_rendering && !Graphiti.cache&.respond_to?(:fetch)
raise "You must configure a cache store in order to use cache_rendering. Set Graphiti.cache = Rails.cache, for example."
end
end
end

def schema_path
@schema_path ||= raise("No schema_path defined! Set Graphiti.config.schema_path to save your schema.")
end
Expand Down
25 changes: 24 additions & 1 deletion lib/graphiti/debugger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,30 @@ def on_render(name, start, stop, id, payload)
took = ((stop - start) * 1000.0).round(2)
logs << [""]
logs << ["=== Graphiti Debug", :green, true]
logs << ["Rendering:", :green, true]
if payload[:proxy]&.cached? && Graphiti.config.cache_rendering?
logs << ["Rendering (cached):", :green, true]

Graphiti::Util::CacheDebug.new(payload[:proxy]).analyze do |cache_debug|
logs << ["Cache key for #{cache_debug.name}", :blue, true]
logs << if cache_debug.volatile?
[" \\_ volatile | Request count: #{cache_debug.request_count} | Hit count: #{cache_debug.hit_count}", :red, true]
else
[" \\_ stable | Request count: #{cache_debug.request_count} | Hit count: #{cache_debug.hit_count}", :blue, true]
end

if cache_debug.changed_key?
logs << [" [x] cache key changed #{cache_debug.last_version[:etag]} -> #{cache_debug.current_version[:etag]}", :red]
logs << [" removed: #{cache_debug.removed_segments}", :red]
logs << [" added: #{cache_debug.added_segments}", :red]
elsif cache_debug.new_key?
logs << [" [+] cache key added #{cache_debug.current_version[:etag]}", :red, true]
else
logs << [" [✓] #{cache_debug.current_version[:etag]}", :green, true]
end
end
else
logs << ["Rendering:", :green, true]
end
logs << ["Took: #{took}ms", :magenta, true]
end
end
Expand Down
16 changes: 16 additions & 0 deletions lib/graphiti/query.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require "digest"

module Graphiti
class Query
attr_reader :resource, :association_name, :params, :action
Expand Down Expand Up @@ -232,8 +234,22 @@ def paginate?
![false, "false"].include?(@params[:paginate])
end

def cache_key
"args-#{query_cache_key}"
end

private

def query_cache_key
attrs = {extra_fields: extra_fields,
fields: fields,
links: links?,
pagination_links: pagination_links?,
format: params[:format]}

Digest::SHA1.hexdigest(attrs.to_s)
end

def cast_page_param(name, value)
if [:before, :after].include?(name)
decode_cursor(value)
Expand Down
9 changes: 8 additions & 1 deletion lib/graphiti/renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,14 @@ def render(renderer)
options[:meta][:debug] = Debugger.to_a if debug_json?
options[:proxy] = proxy

renderer.render(records, options)
if proxy.cache? && Graphiti.config.cache_rendering?
Graphiti.cache.fetch("graphiti:render/#{proxy.cache_key}", version: proxy.updated_at, expires_in: proxy.cache_expires_in) do
options.delete(:cache) # ensure that we don't use JSONAPI-Resources's built-in caching logic
renderer.render(records, options)
end
else
renderer.render(records, options)
end
end
end

Expand Down
25 changes: 19 additions & 6 deletions lib/graphiti/resource/interface.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@ module Interface
extend ActiveSupport::Concern

class_methods do
def cache_resource(expires_in: false)
@cache_resource = true
@cache_expires_in = expires_in
end

def all(params = {}, base_scope = nil)
validate!(params)
validate_request!(params)
_all(params, {}, base_scope)
end

# @api private
def _all(params, opts, base_scope)
runner = Runner.new(self, params, opts.delete(:query), :all)
opts[:params] = params
runner.proxy(base_scope, opts)
runner.proxy(base_scope, opts.merge(caching_options))
end

def find(params = {}, base_scope = nil)
validate!(params)
validate_request!(params)
_find(params, base_scope)
end

Expand All @@ -31,21 +36,29 @@ def _find(params = {}, base_scope = nil)
params[:filter][:id] = id if id

runner = Runner.new(self, params, nil, :find)
runner.proxy base_scope,

find_options = {
single: true,
raise_on_missing: true,
bypass_required_filters: true
}.merge(caching_options)

runner.proxy base_scope, find_options
end

def build(params, base_scope = nil)
validate!(params)
validate_request!(params)
runner = Runner.new(self, params)
runner.proxy(base_scope, single: true, raise_on_missing: true)
end

private

def validate!(params)
def caching_options
{cache: @cache_resource, cache_expires_in: @cache_expires_in}
end

def validate_request!(params)
return if Graphiti.context[:graphql] || !validate_endpoints?

if context&.respond_to?(:request)
Expand Down
36 changes: 32 additions & 4 deletions lib/graphiti/resource_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,31 @@ module Graphiti
class ResourceProxy
include Enumerable

attr_reader :resource, :query, :scope, :payload
attr_reader :resource, :query, :scope, :payload, :cache_expires_in, :cache

def initialize(resource, scope, query,
payload: nil,
single: false,
raise_on_missing: false)
raise_on_missing: false,
cache: nil,
cache_expires_in: nil)

@resource = resource
@scope = scope
@query = query
@payload = payload
@single = single
@raise_on_missing = raise_on_missing
@cache = cache
@cache_expires_in = cache_expires_in
end

def cache?
!!@cache
end

alias_method :cached?, :cache?

def single?
!!@single
end
Expand Down Expand Up @@ -74,6 +85,7 @@ def data
end
end
alias_method :to_a, :data
alias_method :resolve_data, :data

def meta
@meta ||= data.respond_to?(:meta) ? data.meta : {}
Expand Down Expand Up @@ -136,7 +148,7 @@ def save(action: :create)
end

def destroy
data
resolve_data
transaction_response = @resource.transaction do
metadata = {method: :destroy}
model = @resource.destroy(@query.filters[:id], metadata)
Expand All @@ -154,7 +166,7 @@ def destroy
end

def update
data
resolve_data
save(action: :update)
end

Expand All @@ -179,6 +191,22 @@ def debug_requested?
query.debug_requested?
end

def updated_at
@scope.updated_at
end

def etag
"W/#{ActiveSupport::Digest.hexdigest(cache_key_with_version.to_s)}"
end

def cache_key
ActiveSupport::Cache.expand_cache_key([@scope.cache_key, @query.cache_key])
end

def cache_key_with_version
ActiveSupport::Cache.expand_cache_key([@scope.cache_key_with_version, @query.cache_key])
end

private

def persist
Expand Down
4 changes: 3 additions & 1 deletion lib/graphiti/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ def proxy(base = nil, opts = {})
query,
payload: deserialized_payload,
single: opts[:single],
raise_on_missing: opts[:raise_on_missing]
raise_on_missing: opts[:raise_on_missing],
cache: opts[:cache],
cache_expires_in: opts[:cache_expires_in]
end
end
end
56 changes: 56 additions & 0 deletions lib/graphiti/scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,64 @@ def resolve_sideloads(results)
end
end

def parent_resource
@resource
end

def cache_key
# This is the combined cache key for the base query and the query for all sideloads
# Changing the query will yield a different cache key

cache_keys = sideload_resource_proxies.map { |proxy| proxy.try(:cache_key) }

cache_keys << @object.try(:cache_key) # this is what calls into the ORM (ActiveRecord, most likely)
ActiveSupport::Cache.expand_cache_key(cache_keys.flatten.compact)
end

def cache_key_with_version
# This is the combined and versioned cache key for the base query and the query for all sideloads
# If any returned model's updated_at changes, this key will change

cache_keys = sideload_resource_proxies.map { |proxy| proxy.try(:cache_key_with_version) }

cache_keys << @object.try(:cache_key_with_version) # this is what calls into ORM (ActiveRecord, most likely)
ActiveSupport::Cache.expand_cache_key(cache_keys.flatten.compact)
end

def updated_at
updated_ats = sideload_resource_proxies.map(&:updated_at)

begin
updated_ats << @object.maximum(:updated_at)
rescue => e
Graphiti.log("error calculating last_modified_at for #{@resource.class}")
Graphiti.log(e)
end

updated_ats.compact.max
end
alias_method :last_modified_at, :updated_at

private

def sideload_resource_proxies
@sideload_resource_proxies ||= begin
@object = @resource.before_resolve(@object, @query)
results = @resource.resolve(@object)

[].tap do |proxies|
unless @query.sideloads.empty?
@query.sideloads.each_pair do |name, q|
sideload = @resource.class.sideload(name)
next if sideload.nil? || sideload.shared_remote?

proxies << sideload.build_resource_proxy(results, q, parent_resource)
end
end
end.flatten
end
end

def broadcast_data
opts = {
resource: @resource,
Expand Down
2 changes: 1 addition & 1 deletion lib/graphiti/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def strip_relationships!(hash)

def strip_relationships?
return false unless Graphiti.config.links_on_demand
params = Graphiti.context[:object].params || {}
params = Graphiti.context[:object]&.params || {}
[false, nil, "false"].include?(params[:links])
end
end
Expand Down
Loading

0 comments on commit 2f2b941

Please sign in to comment.