Skip to content

Instantly share code, notes, and snippets.

@rmosolgo
Created October 13, 2021 18:22
Show Gist options
  • Save rmosolgo/84eeb2700e75aa21a1fea854c608c425 to your computer and use it in GitHub Desktop.
Save rmosolgo/84eeb2700e75aa21a1fea854c608c425 to your computer and use it in GitHub Desktop.
# frozen_string_literal: true
#
# LRU cache based on Ruby 1.9+ ordered hash inspired by Sam Saffron: https://stackoverflow.com/a/16161783
#
# @example Caching valid query documents in a Rails controller
#
# class GraphqlController < ApplicationController
# # TODO: not thread-safe. See https://github.com/samsaffron/lru_redux for a thread-safe LRU cache
# PARSED_QUERY_CACHE = GraphQLParseAndValidateCache.new(schema: MySchema, max_entries: 20)
#
# def execute
# query_string = params[:query_string]
# ast_or_errors = PARSED_QUERY_CACHE.fetch(query_string)
# if ast_or_errors.is_a?(Hash)
# # it's an error response
# render json: ast_or_errors
# else
# query_document = ast_or_errors
# context = { ... }
# # Pass `validate: false` since it was validated by the cache
# result = MySchema.execute(query_document, validate: false, context: context, ...)
# render json: result
# end
# end
# end
class GraphQLParseAndValidateCache
def initialize(schema:, max_entries:)
@schema = schema
@max_entries = max_entries
@entries = {}
end
# @param query_string [String]
# @return [GraphQL::Language::Document, Hash] Returns a hash containing an `"errors"` key if the query string failed parsing or static validation.
# Returns a valid AST if the query string passed parsing and validation (and may be returned from cache).
#
def fetch(query_string)
found = true
# Delete the value if found, so we can use the ordered nature of Ruby hashes to implement LRU
parsed_ast = @entries.delete(query_string) { found = false }
if found
# Re-assign the value to put it at the end of the hash
@entries[query_string] = parsed_ast
else
# The GraphQL-Ruby API here is not great:
# `GraphQL.parse` raises an error if `query_string` has syntax errors; rescue it below.
parsed_ast = GraphQL.parse(query_string)
# `Schema.validate` returns an array of validation errors if it's invalid.
# This implementation doesn't cache invalid responses, assuming that it's an infrequent path.
# But it could be updated if necessary, by storing the hash created in the `else` block below.
validation_errors = @schema.validate(parsed_ast)
if validation_errors.empty?
@entries[query_string] = parsed_ast
# If this entry exceeded the max size, remove the least-recently used entry:
if @entries.length > @max_entries
least_recently_used_key = @entries.first[0]
@entries.delete(least_recently_used_key)
end
parsed_ast
else
{ "errors" => validation_errors.map(&:to_h) }
end
end
rescue GraphQL::ParseError => err
{ "errors" => [err.to_h] }
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment