From 169afb7c1002f9db2b3276af36d552575cc80ba2 Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Wed, 18 Oct 2023 04:24:05 +0200 Subject: [PATCH 01/18] use $DEBUG_API_CLIENT instead of $DEBUG for debugging request --- lib/ontologies_api_client/http.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ontologies_api_client/http.rb b/lib/ontologies_api_client/http.rb index 8b736eb..ecb809c 100644 --- a/lib/ontologies_api_client/http.rb +++ b/lib/ontologies_api_client/http.rb @@ -61,7 +61,7 @@ def self.get(path, params = {}, options = {}) invalidate_cache = params.delete(:invalidate_cache) || false begin - puts "Getting: #{path} with #{params}" if $DEBUG + puts "Getting: #{path} with #{params}" if $DEBUG_API_CLIENT begin response = conn.get do |req| req.url path @@ -87,7 +87,7 @@ def self.get(path, params = {}, options = {}) obj = recursive_struct(load_json(response.body)) end rescue StandardError => e - puts "Problem getting #{path}" if $DEBUG + puts "Problem getting #{path}" if $DEBUG_API_CLIENT raise e end obj @@ -143,7 +143,7 @@ def self.patch(path, obj) end def self.delete(id) - puts "Deleting #{id}" if $DEBUG + puts "Deleting #{id}" if $DEBUG_API_CLIENT response = conn.delete id raise StandardError, response.body if response.status >= 500 From 38e4299d8cad25aef71779704d9eff1aabbca1ce Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Sun, 17 Dec 2023 00:31:29 +0100 Subject: [PATCH 02/18] add $API_CLIENT_INVALIDATE_CACHE option to disable cache --- lib/ontologies_api_client/http.rb | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/ontologies_api_client/http.rb b/lib/ontologies_api_client/http.rb index ecb809c..d3df241 100644 --- a/lib/ontologies_api_client/http.rb +++ b/lib/ontologies_api_client/http.rb @@ -2,7 +2,7 @@ require 'multi_json' require 'digest' require 'ostruct' - +require 'benchmark' ## # This monkeypatch makes OpenStruct act like Struct objects class OpenStruct @@ -58,18 +58,20 @@ def self.get(path, params = {}, options = {}) raw = options[:raw] || false # return the unparsed body of the request params = params.delete_if { |k, v| v == nil || v.to_s.empty? } params[:ncbo_cache_buster] = Time.now.to_f if raw # raw requests don't get cached to ensure body is available - invalidate_cache = params.delete(:invalidate_cache) || false - + invalidate_cache = params.delete(:invalidate_cache) || $API_CLIENT_INVALIDATE_CACHE || false begin - puts "Getting: #{path} with #{params}" if $DEBUG_API_CLIENT begin - response = conn.get do |req| - req.url path - req.params = params.dup - req.options[:timeout] = 60 - req.headers.merge(headers) - req.headers[:invalidate_cache] = invalidate_cache + response = nil + time = Benchmark.realtime do + response = conn.get do |req| + req.url path + req.params = params.dup + req.options[:timeout] = 60 + req.headers.merge(headers) + req.headers[:invalidate_cache] = invalidate_cache + end end + puts "Getting: #{path} with #{params} (#{time}s)" if $DEBUG_API_CLIENT rescue Exception => e params = Faraday::Utils.build_query(params) path << "?" unless params.empty? || path.include?("?") From fe41668785affb77cc6d8a133da800819529447f Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Mon, 18 Dec 2023 06:33:58 +0100 Subject: [PATCH 03/18] save and re-use the top_level_links http response as the result do not change --- Gemfile.lock | 3 ++- lib/ontologies_api_client/collection.rb | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2d0a48c..77dace9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,6 +54,7 @@ GEM PLATFORMS x86_64-darwin-21 + x86_64-darwin-23 x86_64-linux DEPENDENCIES @@ -63,4 +64,4 @@ DEPENDENCIES test-unit BUNDLED WITH - 2.3.23 + 2.4.21 diff --git a/lib/ontologies_api_client/collection.rb b/lib/ontologies_api_client/collection.rb index 97ccc0f..7e6b476 100644 --- a/lib/ontologies_api_client/collection.rb +++ b/lib/ontologies_api_client/collection.rb @@ -25,7 +25,7 @@ def method_missing(meth, *args, &block) ## # Get all top-level links for the API def top_level_links - HTTP.get(LinkedData::Client.settings.rest_url) + @top_level_links||= HTTP.get(LinkedData::Client.settings.rest_url) end ## From ab9048ec0dd2b4b5ef1aef9d40fa0df61282b5ef Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Mon, 18 Dec 2023 06:35:33 +0100 Subject: [PATCH 04/18] deprecate the find method in favor of get method for better performance --- lib/ontologies_api_client/collection.rb | 8 ++++---- lib/ontologies_api_client/models/ontology.rb | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/ontologies_api_client/collection.rb b/lib/ontologies_api_client/collection.rb index 7e6b476..6e0e36f 100644 --- a/lib/ontologies_api_client/collection.rb +++ b/lib/ontologies_api_client/collection.rb @@ -75,16 +75,16 @@ def where(params = {}, &block) ## # Find a resource by id + # @deprecated replace with "get" def find(id, params = {}) - found = where do |obj| - obj.id.eql?(id) - end - found.first + [get(id, params)] end ## # Get a resource by id (this will retrieve it from the REST service) def get(id, params = {}) + path = collection_path + id = "#{path}/#{id}" unless id.include?(path) HTTP.get(id, params) end diff --git a/lib/ontologies_api_client/models/ontology.rb b/lib/ontologies_api_client/models/ontology.rb index 5db6ed4..0aeb365 100644 --- a/lib/ontologies_api_client/models/ontology.rb +++ b/lib/ontologies_api_client/models/ontology.rb @@ -108,7 +108,7 @@ def self.find_by(attrs, *args) # Override to search for views as well by default # Views get hidden on the REST service unless the `include_views` # parameter is set to `true` - def find(id, params = {}) + def self.find(id, params = {}) params[:include_views] = params[:include_views] || true super(id, params) end @@ -119,6 +119,9 @@ def self.include_params "acronym,administeredBy,group,hasDomain,name,notes,projects,reviews,summaryOnly,viewingRestriction" end + def self.find_by_acronym(acronym, params = {}) + [find(acronym, params)] + end end end end From 674193220f93d99587e6d3a969adcb384caf0a61 Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Mon, 18 Dec 2023 06:36:32 +0100 Subject: [PATCH 05/18] disable the refresh_cache on each update call --- lib/ontologies_api_client/collection.rb | 2 +- lib/ontologies_api_client/models/ontology.rb | 4 ---- lib/ontologies_api_client/read_write.rb | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/ontologies_api_client/collection.rb b/lib/ontologies_api_client/collection.rb index 6e0e36f..77572bf 100644 --- a/lib/ontologies_api_client/collection.rb +++ b/lib/ontologies_api_client/collection.rb @@ -77,7 +77,7 @@ def where(params = {}, &block) # Find a resource by id # @deprecated replace with "get" def find(id, params = {}) - [get(id, params)] + get(id, params) end ## diff --git a/lib/ontologies_api_client/models/ontology.rb b/lib/ontologies_api_client/models/ontology.rb index 0aeb365..5a62a01 100644 --- a/lib/ontologies_api_client/models/ontology.rb +++ b/lib/ontologies_api_client/models/ontology.rb @@ -52,10 +52,6 @@ def admin?(user) return administeredBy.any? {|u| u == user.id} end - def invalidate_cache(cache_refresh_all = true) - self.class.all(invalidate_cache: true, include_views: true) - super(cache_refresh_all) - end # ACL with administrators def full_acl diff --git a/lib/ontologies_api_client/read_write.rb b/lib/ontologies_api_client/read_write.rb index 721c669..3549bb0 100644 --- a/lib/ontologies_api_client/read_write.rb +++ b/lib/ontologies_api_client/read_write.rb @@ -90,7 +90,7 @@ def invalidate_cache(cache_refresh_all = true) HTTP.get(self.id, invalidate_cache: true) if self.id session = Thread.current[:session] session[:last_updated] = Time.now.to_f if session - refresh_cache + # refresh_cache end def refresh_cache From 4eb424539f114a96c4af7cee2800ca25af88afd7 Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Sun, 14 Apr 2024 15:18:14 +0200 Subject: [PATCH 06/18] Refactor: update cache code and add tests (#15) * add tests for the api cache code * refactor the cache middleware code * remove unused function in the cache middleware --- Gemfile | 3 +- Gemfile.lock | 14 ++ config/config.test.rb | 3 +- .../middleware/faraday-object-cache.rb | 213 ++++++++---------- test/middleware/test_cache.rb | 141 ++++++++++++ 5 files changed, 259 insertions(+), 115 deletions(-) create mode 100644 test/middleware/test_cache.rb diff --git a/Gemfile b/Gemfile index acdf917..b7f6653 100644 --- a/Gemfile +++ b/Gemfile @@ -4,4 +4,5 @@ gemspec gem 'rake' gem 'pry' -gem 'test-unit' \ No newline at end of file +gem 'test-unit' +gem 'webmock', require: false \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 77dace9..413fd8c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,8 +20,14 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + bigdecimal (3.1.7) coderay (1.1.3) concurrent-ruby (1.1.10) + crack (1.0.0) + bigdecimal + rexml excon (0.95.0) faraday (2.0.1) faraday-net_http (~> 2.0) @@ -32,6 +38,7 @@ GEM faraday-multipart (1.0.4) multipart-post (~> 2) faraday-net_http (2.1.0) + hashdiff (1.1.0) i18n (1.12.0) concurrent-ruby (~> 1.0) lz4-ruby (0.3.3) @@ -44,13 +51,19 @@ GEM pry (0.14.1) coderay (~> 1.1) method_source (~> 1.0) + public_suffix (5.0.5) rake (13.0.6) + rexml (3.2.6) ruby2_keywords (0.0.5) spawnling (2.1.5) test-unit (3.5.7) power_assert tzinfo (2.0.5) concurrent-ruby (~> 1.0) + webmock (3.23.0) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) PLATFORMS x86_64-darwin-21 @@ -62,6 +75,7 @@ DEPENDENCIES pry rake test-unit + webmock BUNDLED WITH 2.4.21 diff --git a/config/config.test.rb b/config/config.test.rb index af6837b..a5d6166 100644 --- a/config/config.test.rb +++ b/config/config.test.rb @@ -3,7 +3,8 @@ # be set via ENV variable UT_APIKEY abort('UT_APIKEY env variable is not set. Canceling tests') unless ENV.include?('UT_APIKEY') abort('UT_APIKEY env variable is set to an empty value. Canceling tests') unless ENV['UT_APIKEY'].size > 5 - +$API_CLIENT_INVALIDATE_CACHE = false +$DEBUG_API_CLIENT = false LinkedData::Client.config do |config| config.rest_url = 'https://data.bioontology.org' config.apikey = ENV['UT_APIKEY'] diff --git a/lib/ontologies_api_client/middleware/faraday-object-cache.rb b/lib/ontologies_api_client/middleware/faraday-object-cache.rb index f5e06d3..4e9eb65 100644 --- a/lib/ontologies_api_client/middleware/faraday-object-cache.rb +++ b/lib/ontologies_api_client/middleware/faraday-object-cache.rb @@ -8,107 +8,47 @@ class ObjectCacheResponse < Faraday::Response attr_accessor :parsed_body end - ## - # This middleware causes Faraday to return an actual object instead of a response - # This is done so that the object is cached instead of the unparsed json body. - # Otherwise, we have to re-parse the json on every cache hit, which is extrememly costly - # when compared to unmarshaling an object. class ObjectCache < Faraday::Middleware def initialize(app, *arguments) super(app) + options = arguments.last.is_a?(Hash) ? arguments.pop : {} + @logger = options.delete(:logger) + @store = options[:store] || ActiveSupport::Cache.lookup_store(nil, options) + end - if arguments.last.is_a? Hash - options = arguments.pop - @logger = options.delete(:logger) - else - options = arguments - end - @store = options[:store] || ActiveSupport::Cache.lookup_store(store, options) + def last_modified_key_id(request_key) + "LM::#{request_key}" + end + + def last_retrieved_key_id(request_key) + "LR::#{request_key}" end def call(env) invalidate_cache = env[:request_headers].delete(:invalidate_cache) - # Add if newer than last modified statement to headers request_key = cache_key_for(create_request(env)) - last_modified_key = "LM::#{request_key}" - last_retrieved_key = "LR::#{request_key}" + last_modified_key = last_modified_key_id(request_key) + last_retrieved_key = last_retrieved_key_id(request_key) - # If we invalidate the cache, then it forces a clean request if invalidate_cache - cache_delete(request_key) - cache_delete(last_modified_key) - cache_delete(last_retrieved_key) + delete_cache_entries(request_key, last_modified_key, last_retrieved_key) env[:request_headers]["Cache-Control"] = "no-cache" puts "Invalidated key #{request_key}" if enable_debug(request_key) end - # If we made the last request within the expiry if cache_exist?(last_retrieved_key) && cache_exist?(request_key) puts "Not expired: #{env[:url].to_s}, key #{request_key}" if enable_debug(request_key) - cached_item = cache_read(request_key) - ld_obj = cached_item.is_a?(Hash) && cached_item.key?(:ld_obj) ? cached_item[:ld_obj] : cached_item - env[:status] = 304 - cached_response = ObjectCacheResponse.new(env) - cached_response.parsed_body = ld_obj - return cached_response + return retrieve_cached_response(request_key) end - last_modified = cache_read(last_modified_key) headers = env[:request_headers] - puts "last modified: " + last_modified.to_s if last_modified && enable_debug(request_key) - headers['If-Modified-Since'] = last_modified if last_modified + headers['If-Modified-Since'] = cache_read(last_modified_key) if cache_read(last_modified_key) @app.call(env).on_complete do |response_env| - # Only cache get and head requests if [:get, :head].include?(response_env[:method]) - puts "Response status for key #{request_key}: " + response_env[:status].to_s if enable_debug(request_key) - last_modified = response_env[:response_headers]["Last-Modified"] - # Generate key using header hash - key = request_key - - # If the last retrieve time is less than expiry - if response_env[:status] == 304 && cache_exist?(key) - stored_obj = cache_read(key) - - # Update if last modified is different - if stored_obj[:last_modified] != last_modified - puts "Updating cache #{response_env[:url].to_s}, key #{request_key}" if enable_debug(request_key) - stored_obj[:last_modified] = last_modified - cache_write(last_modified_key, last_modified) - cache_write(key, stored_obj) - end - - ld_obj = stored_obj[:ld_obj] - else - if response_env[:body].nil? || response_env[:body].empty? - # We got here with an empty body, meaning the object wasn't - # in the cache (weird). So re-do the request. - puts "REDOING REQUEST, NO CACHE ENTRY for #{response_env[:url].to_s}, key #{request_key}" if enable_debug(request_key) - env[:request_headers].delete("If-Modified-Since") - response_env = @app.call(env).env - puts "REDOING REQUEST expiry: #{response_env[:response_headers]["Cache-Control"]}, last_modified: #{last_modified} for key #{request_key}" if enable_debug(request_key) - end - - ld_obj = LinkedData::Client::HTTP.object_from_json(response_env[:body]) - # This stmt was missing in the old code, resulting in repeated calls to REST because object failed to cache - last_modified = response_env[:response_headers]["Last-Modified"] - expiry = response_env[:response_headers]["Cache-Control"].to_s.split("max-age=").last.to_i - puts "Before storing object: expiry: #{expiry}, last_modified: #{last_modified} for key #{request_key}" if enable_debug(request_key) - - if expiry > 0 && last_modified - # This request is cacheable, store it - puts "Storing object: #{response_env[:url].to_s}, key #{request_key}" if enable_debug(request_key) - stored_obj = {last_modified: last_modified, ld_obj: ld_obj} - cache_write(last_modified_key, last_modified) - cache_write(last_retrieved_key, true, expires_in: expiry) - cache_write(key, stored_obj) - end - end - - response = ObjectCacheResponse.new(response_env) - response.parsed_body = ld_obj + response = process_response(response_env, request_key) return response end end @@ -117,8 +57,83 @@ def call(env) private def enable_debug(key) - return true if LinkedData::Client.settings.debug_client && (LinkedData::Client.settings.debug_client_keys.empty? || LinkedData::Client.settings.debug_client_keys.include?(key)) - false + LinkedData::Client.settings.debug_client && (LinkedData::Client.settings.debug_client_keys.empty? || LinkedData::Client.settings.debug_client_keys.include?(key)) + end + + def delete_cache_entries(*keys) + keys.each { |key| cache_delete(key) } + end + + def retrieve_cached_response(request_key) + cached_item = cache_read(request_key) + ld_obj = cached_item.is_a?(Hash) && cached_item.key?(:ld_obj) ? cached_item[:ld_obj] : cached_item + env = { status: 304 } + cached_response = ObjectCacheResponse.new(env) + cached_response.parsed_body = ld_obj + cached_response.env.response_headers = { "x-rack-cache" => 'hit' } + cached_response + end + + def process_response(response_env, request_key) + last_modified = response_env[:response_headers]["Last-Modified"] + key = request_key + cache_state = "miss" + + if response_env[:status] == 304 && cache_exist?(key) + cache_state = "fresh" + ld_obj = update_cache(request_key, last_modified) + else + ld_obj = cache_response(response_env, request_key) + end + + response = ObjectCacheResponse.new(response_env) + response.parsed_body = ld_obj + response.env.response_headers["x-rack-cache"] = cache_state + response + end + + def update_cache(request_key, last_modified) + stored_obj = cache_read(request_key) + if stored_obj[:last_modified] != last_modified + stored_obj[:last_modified] = last_modified + cache_write(last_modified_key_id(request_key), last_modified) + cache_write(request_key, stored_obj) + end + stored_obj + end + + def cache_response(response_env, request_key) + last_modified = response_env[:response_headers]["Last-Modified"] + + if response_env[:body].nil? || response_env[:body].empty? + # We got here with an empty body, meaning the object wasn't + # in the cache (weird). So re-do the request. + puts "REDOING REQUEST, NO CACHE ENTRY for #{response_env[:url].to_s}, key #{request_key}" if enable_debug(request_key) + response_env[:request_headers].delete("If-Modified-Since") + + response_env = @app.call(response_env).env + puts "REDOING REQUEST expiry: #{response_env[:response_headers]["Cache-Control"]}, last_modified: #{last_modified} for key #{request_key}" if enable_debug(request_key) + end + + + return nil if response_env[:body].nil? || response_env[:body].empty? + + ld_obj = LinkedData::Client::HTTP.object_from_json(response_env[:body]) + + expiry = response_env[:response_headers]["Cache-Control"].to_s.split("max-age=").last.to_i + + if expiry > 0 && last_modified + store_cache(request_key, ld_obj, last_modified, expiry) + end + + ld_obj + end + + def store_cache(request_key, ld_obj, last_modified, expiry) + stored_obj = { last_modified: last_modified, ld_obj: ld_obj } + cache_write(last_modified_key_id(request_key), last_modified) + cache_write(last_retrieved_key_id(request_key), true, expires_in: expiry) + cache_write(request_key, stored_obj) end def cache_write(key, obj, *args) @@ -132,12 +147,6 @@ def cache_write(key, obj, *args) if result return result else - # This should still get stored in memcache - # keep it in memory, though, because - # marshal/unmarshal is too slow. - # This way memcache will act as a backup - # and you load from there if it isn't - # in memory yet. @large_object_cache ||= {} @large_object_cache[key] = obj cache_write_compressed(key, obj, *args) @@ -147,11 +156,10 @@ def cache_write(key, obj, *args) def cache_read(key) obj = @store.read(key) - return if obj.nil? + return unless obj + if obj.is_a?(CompressedMemcache) - # Try to get from the large object cache large_obj = @large_object_cache[key] if @large_object_cache - # Fallback to the memcache version large_obj ||= cache_read_compressed(key) obj = large_obj end @@ -162,13 +170,14 @@ def cache_exist?(key) @store.exist?(key) end - class CompressedMemcache; attr_accessor :key; end + class CompressedMemcache + attr_accessor :key + end - ## - # Compress cache entry def cache_write_compressed(key, obj, *args) compressed = LZ4::compress(Marshal.dump(obj)) - return if compressed.nil? + return unless compressed + placeholder = CompressedMemcache.new placeholder.key = "#{key}::#{(Time.now.to_f * 1000).to_i}::LZ4" begin @@ -181,8 +190,6 @@ def cache_write_compressed(key, obj, *args) end end - ## - # Read compressed cache entry def cache_read_compressed(key) obj = @store.read(key) if obj.is_a?(CompressedMemcache) @@ -190,7 +197,6 @@ def cache_read_compressed(key) uncompressed = LZ4::uncompress(@store.read(obj.key)) obj = Marshal.load(uncompressed) rescue StandardError => e - # There is a problem with the stored value, let's remove it so we don't get the error again @store.delete(key) @store.delete(obj.key) raise e @@ -203,35 +209,16 @@ def cache_delete(key) @store.delete(key) end - # Internal: Generates a String key for a given request object. - # The request object is folded into a sorted Array (since we can't count - # on hashes order on Ruby 1.8), encoded as JSON and digested as a `SHA1` - # string. - # - # Returns the encoded String. def cache_key_for(request) array = request.stringify_keys.to_a.sort Digest::SHA1.hexdigest(Marshal.dump(array)) end - # Internal: Creates a new 'Hash' containing the request information. - # - # env - the environment 'Hash' from the Faraday stack. - # - # Returns a 'Hash' containing the ':method', ':url' and 'request_headers' - # entries. def create_request(env) request = env.to_hash.slice(:method, :url, :request_headers) request[:request_headers] = request[:request_headers].dup request end - - def clean_request_headers(request) - request[:request_headers].delete("If-Modified-Since") - request[:request_headers].delete("Expect") - request - end - end end diff --git a/test/middleware/test_cache.rb b/test/middleware/test_cache.rb new file mode 100644 index 0000000..ddea7a9 --- /dev/null +++ b/test/middleware/test_cache.rb @@ -0,0 +1,141 @@ +require_relative '../test_case' +require 'faraday' +require 'active_support' +require 'active_support/cache' +require_relative '../../lib/ontologies_api_client/middleware/faraday-object-cache' +require 'pry' +require 'benchmark' +require 'webmock' + +class FaradayObjectCacheTest < LinkedData::Client::TestCase + def setup + WebMock.disable! + apikey = LinkedData::Client.settings.apikey + @url = "#{LinkedData::Client.settings.rest_url}/ontologies/SNOMEDCT?apikey=#{apikey}" + + @cache_store = ActiveSupport::Cache::MemoryStore.new + @app = Faraday.new(url: @url) do |faraday| + faraday.use Faraday::ObjectCache, store: @cache_store + faraday.adapter :excon + end + end + + def test_cache_hit_for_get_request + body1, body2 = nil + # First request should not hit the cache + time1 = Benchmark.realtime do + response1 = @app.get + assert_equal 200, response1.status + assert uncached?(response1) + + body1 = JSON.parse(response1.body) + end + + time2 = Benchmark.realtime do + # Second request should hit the cache + response2 = @app.get + assert_equal 304, response2.status + assert cached?(response2) + body2 = response2.parsed_body.to_hash.stringify_keys + end + + assert time2 < time1 + + body2.each do |k,v| + k = "@id" if k.eql?('id') + k = "@type" if k.eql?('type') + + next if k.eql?('context') || k.eql?('links') + + assert_equal v, body1[k] + end + end + + + def test_cache_invalidation + # Make a request and cache the response + response1 = @app.get + assert_equal 200, response1.status + + response1 = @app.get + assert_equal 304, response1.status + assert cached?(response1) + + # Invalidate the cache + response2 = @app.get do |req| + req.headers['invalidate_cache'] = true + end + assert_equal 200, response2.status + assert uncached?(response2) + end + + def test_cache_expiration + WebMock.enable! + WebMock.stub_request(:get, @url) + .to_return(headers: { 'Cache-Control': "max-age=1" , 'Last-Modified': Time.now.httpdate}, body: {result: 'hello'}.to_json) + + + # Make a request and cache the response with a short expiry time + response1 = @app.get + assert_equal 200, response1.status + + # Wait for the cache to expire + sleep 2 + + response2 = @app.get + + assert_equal 200, response2.status + assert uncached?(response2) + + sleep 2 + + WebMock.stub_request(:get, @url) + .to_return(headers: { 'Cache-Control': "max-age=100" , 'Last-Modified': Time.now.httpdate}, body: {result: 'hello'}.to_json) + @app.get + + + # Wait for the cache to expire + sleep 2 + + response2 = @app.get + + assert cached?(response2) + + WebMock.disable! + end + + def test_cache_last_modified + WebMock.enable! + # Make a request with Last-Modified header + WebMock.stub_request(:get, @url) + .to_return(headers: { 'Cache-Control': "max-age=1", 'Last-Modified': 3.days.ago.to_time.httpdate}, body: {result: 'hello'}.to_json, status: 304) + + @app.get + + response2 = @app.get + assert cached?(response2) + + sleep 1 + + WebMock.stub_request(:get, @url) + .to_return(headers: { 'Cache-Control': "max-age=10", 'Last-Modified': 1.days.ago.to_time.httpdate}, body: {result: 'hello'}.to_json, status: 304) + + response2 = @app.get + assert refreshed?(response2) + WebMock.disable! + end + + + private + def cached?(response) + response.env.response_headers['x-rack-cache'].eql?('hit') + end + + def uncached?(response) + response.env.response_headers['x-rack-cache'].eql?('miss') + end + + def refreshed?(response) + response.env.response_headers['x-rack-cache'].eql?('fresh') + end +end From abea6c217c0ee93bcfd80c47c2af52747365d491 Mon Sep 17 00:00:00 2001 From: Syphax Bouazzouni Date: Sat, 25 May 2024 08:29:55 +0200 Subject: [PATCH 07/18] fix refactored cache code not getting the ld_obj when updating the cache --- config/config.test.rb | 6 +++--- .../middleware/faraday-object-cache.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.test.rb b/config/config.test.rb index a5d6166..9185c5d 100644 --- a/config/config.test.rb +++ b/config/config.test.rb @@ -7,8 +7,8 @@ $DEBUG_API_CLIENT = false LinkedData::Client.config do |config| config.rest_url = 'https://data.bioontology.org' - config.apikey = ENV['UT_APIKEY'] -# config.apikey = 'xxxxx-xxxxx-xxxxxxxxxx' + config.apikey = '8b5b7825-538d-40e0-9e9e-5ab9274a9aeb' config.links_attr = 'links' - config.cache = false + config.cache = true + config.debug_client = false end diff --git a/lib/ontologies_api_client/middleware/faraday-object-cache.rb b/lib/ontologies_api_client/middleware/faraday-object-cache.rb index 4e9eb65..080c416 100644 --- a/lib/ontologies_api_client/middleware/faraday-object-cache.rb +++ b/lib/ontologies_api_client/middleware/faraday-object-cache.rb @@ -99,7 +99,7 @@ def update_cache(request_key, last_modified) cache_write(last_modified_key_id(request_key), last_modified) cache_write(request_key, stored_obj) end - stored_obj + stored_obj.is_a?(Hash) && stored_obj.key?(:ld_obj) ? stored_obj[:ld_obj] : stored_obj end def cache_response(response_env, request_key) From db35d02e3cc63099ac0e92d83193884db8e987f6 Mon Sep 17 00:00:00 2001 From: Syphax Bouazzouni Date: Sun, 1 Sep 2024 23:56:56 +0200 Subject: [PATCH 08/18] add unit tests of the expected behavior of the federation --- config/config.test.rb | 31 +++++++-- test/models/test_federation.rb | 122 +++++++++++++++++++++++++++++++++ test/test_case.rb | 2 + 3 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 test/models/test_federation.rb diff --git a/config/config.test.rb b/config/config.test.rb index 9185c5d..1200719 100644 --- a/config/config.test.rb +++ b/config/config.test.rb @@ -1,14 +1,33 @@ -# config.rb is required for testing -# unit test makes calls to bioportal api so it needs a valid API key which can -# be set via ENV variable UT_APIKEY -abort('UT_APIKEY env variable is not set. Canceling tests') unless ENV.include?('UT_APIKEY') -abort('UT_APIKEY env variable is set to an empty value. Canceling tests') unless ENV['UT_APIKEY'].size > 5 $API_CLIENT_INVALIDATE_CACHE = false $DEBUG_API_CLIENT = false + LinkedData::Client.config do |config| - config.rest_url = 'https://data.bioontology.org' + config.rest_url = 'https://data.bioontology.org/' config.apikey = '8b5b7825-538d-40e0-9e9e-5ab9274a9aeb' config.links_attr = 'links' config.cache = true config.debug_client = false + config.debug_client_keys = [] + config.federated_portals = { + bioportal: { + api: 'https://data.agroportal.lirmm.fr/', + apikey: '1de0a270-29c5-4dda-b043-7c3580628cd5', + color: '#234979', + }, + ecoportal: { + api: 'https://data.ecoportal.lifewatch.eu/', + apikey: "43a437ba-a437-4bf0-affd-ab520e584719", + color: '#0f4e8a', + }, + # earthportal: { + # api: 'https://earthportal.eu:8443/', + # apikey: "c9147279-954f-41bd-b068-da9b0c441288", + # color: '#1e2251', + # }, + biodivportal: { + api: 'https://data.biodivportal.gfbio.org/', + apikey: "47a57aa3-7b54-4f34-b695-dbb5f5b7363e", + color: '#1e2251', + } + } end diff --git a/test/models/test_federation.rb b/test/models/test_federation.rb new file mode 100644 index 0000000..4890af5 --- /dev/null +++ b/test/models/test_federation.rb @@ -0,0 +1,122 @@ +require_relative '../test_case' +require 'pry' +require 'benchmark' +require 'webmock' +require 'request_store' + +class FederationTest < LinkedData::Client::TestCase + + def test_federated_ontologies_all + ontologies = [] + time1 = Benchmark.realtime do + ontologies = LinkedData::Client::Models::Ontology.all(display_links: false, display_context: false) + end + + ontologies_federate_all = [] + time2 = Benchmark.realtime do + ontologies_federate_all = LinkedData::Client::Models::Ontology.all(federate: true, display_links: false, display_context: false) + end + + puts "" + puts "AgroPortal ontologies: #{ontologies.length} in #{time1}s" + puts "Federated ontologies: #{ontologies_federate_all.length} in #{time2}s" + + refute_equal ontologies.length, ontologies_federate_all.length + + ontologies_federate_all.group_by{|x| x.id.split('/')[0..-2].join('/')}.each do |portal, onts| + puts "#{portal} ontologies: #{onts.length}" + end + + ontologies_federate_all_cache = [] + time2 = Benchmark.realtime do + ontologies_federate_all_cache = LinkedData::Client::Models::Ontology.all(federate: true, display_links: false, display_context: false) + end + + + puts "Federated ontologies with cache: #{ontologies_federate_all_cache.length} in #{time2}s" + + assert_equal ontologies_federate_all_cache.size, ontologies_federate_all.size + + ontologies_federate_two = [] + time2 = Benchmark.realtime do + ontologies_federate_two = LinkedData::Client::Models::Ontology.all(federate: [:ecoportal, :biodivportal], display_links: false, display_context: false) + end + + puts "Federated ontologies with two portal only with cache: #{ontologies_federate_two.length} in #{time2}s" + + refute_equal ontologies_federate_two.size, ontologies_federate_all.size + + federated_portals = ontologies_federate_two.map{|x| x.id.split('/')[0..-2].join('/')}.uniq + assert_equal 3, federated_portals.size + assert %w[bioontology ecoportal biodivportal].all? { |p| federated_portals.any?{|id| id[p]} } + end + + def test_federated_submissions_all + onts = [] + time1 = Benchmark.realtime do + onts = LinkedData::Client::Models::OntologySubmission.all + end + + onts_federate = [] + time2 = Benchmark.realtime do + onts_federate = LinkedData::Client::Models::OntologySubmission.all(federate: true) + end + + puts "" + puts "AgroPortal submissions: #{onts.length} in #{time1}s" + puts "Federated submissions: #{onts_federate.length} in #{time2}s" + + refute_equal onts.length, onts_federate.length + + onts_federate.group_by{|x| x.id.split('/')[0..-4].join('/')}.each do |portal, onts| + puts "#{portal} submissions: #{onts.length}" + end + + onts_federate = [] + time2 = Benchmark.realtime do + onts_federate = LinkedData::Client::Models::OntologySubmission.all(federate: true) + end + puts "Federated submissions with cache: #{onts_federate.length} in #{time2}s" + + end + + def test_federation_middleware + ontologies_federate_one = LinkedData::Client::Models::Ontology.all(federate: [:ecoportal, :biodivportal], display_links: false, display_context: false) + + RequestStore.store[:federated_portals] = [:ecoportal, :biodivportal] #saved globally + + ontologies_federate_two = LinkedData::Client::Models::Ontology.all(display_links: false, display_context: false) + assert_equal ontologies_federate_one.size, ontologies_federate_two.size + end + + + def test_federation_error + WebMock.enable! + LinkedData::Client::Models::Ontology.all(invalidate_cache: true) + WebMock.stub_request(:get, "#{LinkedData::Client.settings.rest_url.chomp('/')}/ontologies?include=all&display_links=false&display_context=false") + .to_return(body: "Internal server error", status: 500) + + ontologies_federate_one = LinkedData::Client::Models::Ontology.all(federate: [:ecoportal, :biodivportal], display_links: false, display_context: false, invalidate_cache: true) + + assert_equal "Problem retrieving #{LinkedData::Client.settings.rest_url}/ontologies", ontologies_federate_one.first.errors + + WebMock.disable! + end + + def test_federated_analytics + RequestStore.store[:federated_portals] = [:ecoportal,:biodivportal] + analytics = LinkedData::Client::Analytics.last_month + refute_empty analytics.onts + end + + + def test_federation_ssl_error + WebMock.enable! + WebMock.stub_request(:get, "#{LinkedData::Client.settings.rest_url.chomp('/')}") + .to_raise(Faraday::SSLError) + ontologies_federate_one = LinkedData::Client::Models::Ontology.all(display_links: false, display_context: false, invalidate_cache: true) + + refute_nil ontologies_federate_one.first.errors + WebMock.disable! + end +end \ No newline at end of file diff --git a/test/test_case.rb b/test/test_case.rb index cd9fe2e..0f2f907 100644 --- a/test/test_case.rb +++ b/test/test_case.rb @@ -1,7 +1,9 @@ require 'test-unit' require_relative '../lib/ontologies_api_client' require_relative '../config/config' +require 'webmock' +WebMock.allow_net_connect! module LinkedData module Client class TestCase < Test::Unit::TestCase From 9d5bb868fa1eef9fe3336a1354f42a0555725f14 Mon Sep 17 00:00:00 2001 From: Syphax Bouazzouni Date: Sun, 1 Sep 2024 23:58:16 +0200 Subject: [PATCH 09/18] add caching debuging message option to see the cached and missed calls --- lib/ontologies_api_client/http.rb | 12 +++++++++--- .../middleware/faraday-object-cache.rb | 5 +++-- test/middleware/test_cache.rb | 10 +++++++--- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/ontologies_api_client/http.rb b/lib/ontologies_api_client/http.rb index d3df241..ebd8750 100644 --- a/lib/ontologies_api_client/http.rb +++ b/lib/ontologies_api_client/http.rb @@ -3,6 +3,7 @@ require 'digest' require 'ostruct' require 'benchmark' +require 'active_support/cache' ## # This monkeypatch makes OpenStruct act like Struct objects class OpenStruct @@ -48,22 +49,27 @@ def self.conn rails = Kernel.const_get("Rails") store = rails.cache if rails.cache end - LinkedData::Client.config_connection(cache_store: store) + LinkedData::Client.config_connection(cache_store: store || ActiveSupport::Cache::MemoryStore.new) end LinkedData::Client.settings.conn end + def self.federated_conn + LinkedData::Client.settings.federated_conn + end + def self.get(path, params = {}, options = {}) headers = options[:headers] || {} raw = options[:raw] || false # return the unparsed body of the request params = params.delete_if { |k, v| v == nil || v.to_s.empty? } params[:ncbo_cache_buster] = Time.now.to_f if raw # raw requests don't get cached to ensure body is available invalidate_cache = params.delete(:invalidate_cache) || $API_CLIENT_INVALIDATE_CACHE || false + connection = options[:connection] || conn begin begin response = nil time = Benchmark.realtime do - response = conn.get do |req| + response = connection.get do |req| req.url path req.params = params.dup req.options[:timeout] = 60 @@ -71,7 +77,7 @@ def self.get(path, params = {}, options = {}) req.headers[:invalidate_cache] = invalidate_cache end end - puts "Getting: #{path} with #{params} (#{time}s)" if $DEBUG_API_CLIENT + puts "Getting: #{path} with #{params} (t: #{time}s - cache: #{response.headers["X-Rack-Cache"]})" if $DEBUG_API_CLIENT rescue Exception => e params = Faraday::Utils.build_query(params) path << "?" unless params.empty? || path.include?("?") diff --git a/lib/ontologies_api_client/middleware/faraday-object-cache.rb b/lib/ontologies_api_client/middleware/faraday-object-cache.rb index 080c416..f8ea13f 100644 --- a/lib/ontologies_api_client/middleware/faraday-object-cache.rb +++ b/lib/ontologies_api_client/middleware/faraday-object-cache.rb @@ -1,4 +1,5 @@ require 'digest/sha1' +require 'active_support' require 'active_support/cache' require 'lz4-ruby' require_relative '../http' @@ -70,7 +71,7 @@ def retrieve_cached_response(request_key) env = { status: 304 } cached_response = ObjectCacheResponse.new(env) cached_response.parsed_body = ld_obj - cached_response.env.response_headers = { "x-rack-cache" => 'hit' } + cached_response.env.response_headers = { "X-Rack-Cache" => 'hit' } cached_response end @@ -88,7 +89,7 @@ def process_response(response_env, request_key) response = ObjectCacheResponse.new(response_env) response.parsed_body = ld_obj - response.env.response_headers["x-rack-cache"] = cache_state + response.env.response_headers["X-Rack-Cache"] = cache_state response end diff --git a/test/middleware/test_cache.rb b/test/middleware/test_cache.rb index ddea7a9..943b55c 100644 --- a/test/middleware/test_cache.rb +++ b/test/middleware/test_cache.rb @@ -20,6 +20,10 @@ def setup end end + def teardown + WebMock.disable! + end + def test_cache_hit_for_get_request body1, body2 = nil # First request should not hit the cache @@ -128,14 +132,14 @@ def test_cache_last_modified private def cached?(response) - response.env.response_headers['x-rack-cache'].eql?('hit') + response.env.response_headers['X-Rack-Cache'].eql?('hit') end def uncached?(response) - response.env.response_headers['x-rack-cache'].eql?('miss') + response.env.response_headers['X-Rack-Cache'].eql?('miss') end def refreshed?(response) - response.env.response_headers['x-rack-cache'].eql?('fresh') + response.env.response_headers['X-Rack-Cache'].eql?('fresh') end end From 1daf74f9be56cb273f8830d3d58d69cf776686d7 Mon Sep 17 00:00:00 2001 From: Syphax Bouazzouni Date: Sun, 1 Sep 2024 23:59:35 +0200 Subject: [PATCH 10/18] update the initialization of the http connection to have multiple by API --- lib/ontologies_api_client/config.rb | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/ontologies_api_client/config.rb b/lib/ontologies_api_client/config.rb index 25805a8..fbf6cfb 100644 --- a/lib/ontologies_api_client/config.rb +++ b/lib/ontologies_api_client/config.rb @@ -37,8 +37,22 @@ def config(&block) def config_connection(options = {}) return if @settings_run_connection - store = options[:cache_store] - @settings.conn = Faraday.new(@settings.rest_url) do |faraday| + store = options[:cache_store] || ActiveSupport::Cache::MemoryStore.new + @settings.conn = faraday_connection(@settings.rest_url, @settings.apikey, store) + @settings.federated_conn = @settings.federated_portals.map do |portal_name, portal_info| + [portal_name, faraday_connection(portal_info[:api], portal_info[:apikey], store)] + end.to_h + + @settings_run_connection = true + end + + def connection_configured? + @settings_run_connection + end + + private + def faraday_connection(url, apikey, store) + Faraday.new(url.to_s.chomp('/')) do |faraday| if @settings.enable_long_request_log require_relative 'middleware/faraday-long-requests' faraday.use :long_requests @@ -69,15 +83,10 @@ def config_connection(options = {}) faraday.adapter :excon faraday.headers = { "Accept" => "application/json", - "Authorization" => "apikey token=#{@settings.apikey}", + "Authorization" => "apikey token=#{apikey}", "User-Agent" => "NCBO API Ruby Client v0.1.0" } end - @settings_run_connection = true - end - - def connection_configured? - @settings_run_connection end end end \ No newline at end of file From 63d00f0f38372087afa06cbc98b2c5ef0f8f6785 Mon Sep 17 00:00:00 2001 From: Syphax Bouazzouni Date: Mon, 2 Sep 2024 00:00:48 +0200 Subject: [PATCH 11/18] create the RequestFederation to implement federated_get function --- lib/ontologies_api_client.rb | 1 + .../request_federation.rb | 45 +++++++++++++++++++ ontologies_api_client.gemspec | 4 +- 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 lib/ontologies_api_client/request_federation.rb diff --git a/lib/ontologies_api_client.rb b/lib/ontologies_api_client.rb index 739639a..43c4f6d 100644 --- a/lib/ontologies_api_client.rb +++ b/lib/ontologies_api_client.rb @@ -1,6 +1,7 @@ require 'oj' require 'multi_json' require 'spawnling' +require 'request_store' require_relative 'ontologies_api_client/config' require_relative 'ontologies_api_client/http' diff --git a/lib/ontologies_api_client/request_federation.rb b/lib/ontologies_api_client/request_federation.rb new file mode 100644 index 0000000..458af90 --- /dev/null +++ b/lib/ontologies_api_client/request_federation.rb @@ -0,0 +1,45 @@ +require 'active_support/core_ext/hash' + +module LinkedData + module Client + module RequestFederation + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def federated_get(params = {}, &link) + portals = request_portals(params) + + connections = Parallel.map(portals, in_threads: portals.size) do |conn| + begin + HTTP.get(link.call(conn.url_prefix.to_s.chomp('/')), params, connection: conn) + rescue StandardError => e + [OpenStruct.new(errors: "Problem retrieving #{link.call(conn.url_prefix.to_s.chomp('/')) || conn.url_prefix}")] + end + end + + connections.flatten + end + + def request_portals(params = {}) + federate = params.delete(:federate) || ::RequestStore.store[:federated_portals] + + portals = [LinkedData::Client::HTTP.conn] + + if federate.is_a?(Array) + portals += LinkedData::Client::HTTP.federated_conn + .select { |portal_name, _| federate.include?(portal_name) || federate.include?(portal_name.to_s) } + .values + elsif !federate.blank? # all + portals += LinkedData::Client::HTTP.federated_conn.values + end + + portals + end + end + + end + end +end diff --git a/ontologies_api_client.gemspec b/ontologies_api_client.gemspec index fad13e4..b2ae8c2 100644 --- a/ontologies_api_client.gemspec +++ b/ontologies_api_client.gemspec @@ -12,7 +12,7 @@ Gem::Specification.new do |gem| gem.require_paths = ["lib"] gem.version = "2.2.0" - gem.add_dependency('activesupport') + gem.add_dependency('activesupport', '~> 7.0.4') gem.add_dependency('excon') gem.add_dependency('faraday') gem.add_dependency('faraday-excon', '~> 2.0.0') @@ -20,5 +20,7 @@ Gem::Specification.new do |gem| gem.add_dependency('lz4-ruby') gem.add_dependency('multi_json') gem.add_dependency('oj') + gem.add_dependency('parallel') + gem.add_dependency('request_store') gem.add_dependency('spawnling', '2.1.5') end From 1161610ca3ed7463bb34ea107a8ab063a98bb749 Mon Sep 17 00:00:00 2001 From: Syphax Bouazzouni Date: Mon, 2 Sep 2024 00:02:24 +0200 Subject: [PATCH 12/18] update collections calls to use federated_get() instead of get() --- Gemfile.lock | 35 +++++++++++++++---------- lib/ontologies_api_client/base.rb | 13 ++++++--- lib/ontologies_api_client/collection.rb | 15 ++++++++--- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 413fd8c..dd5fb94 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,7 @@ PATH remote: . specs: ontologies_api_client (2.2.0) - activesupport + activesupport (~> 7.0.4) excon faraday faraday-excon (~> 2.0.0) @@ -10,12 +10,14 @@ PATH lz4-ruby multi_json oj + parallel + request_store spawnling (= 2.1.5) GEM remote: https://rubygems.org/ specs: - activesupport (7.0.4) + activesupport (7.0.8.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -24,11 +26,11 @@ GEM public_suffix (>= 2.0.2, < 6.0) bigdecimal (3.1.7) coderay (1.1.3) - concurrent-ruby (1.1.10) + concurrent-ruby (1.2.3) crack (1.0.0) bigdecimal rexml - excon (0.95.0) + excon (0.110.0) faraday (2.0.1) faraday-net_http (~> 2.0) ruby2_keywords (>= 0.0.4) @@ -39,26 +41,31 @@ GEM multipart-post (~> 2) faraday-net_http (2.1.0) hashdiff (1.1.0) - i18n (1.12.0) + i18n (1.14.4) concurrent-ruby (~> 1.0) lz4-ruby (0.3.3) - method_source (1.0.0) - minitest (5.16.3) + method_source (1.1.0) + minitest (5.22.3) multi_json (1.15.0) - multipart-post (2.2.3) - oj (3.13.23) - power_assert (2.0.2) - pry (0.14.1) + multipart-post (2.4.0) + oj (3.16.3) + bigdecimal (>= 3.0) + parallel (1.24.0) + power_assert (2.0.3) + pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) public_suffix (5.0.5) - rake (13.0.6) + rack (3.0.10) + rake (13.2.1) + request_store (1.7.0) + rack (>= 1.4) rexml (3.2.6) ruby2_keywords (0.0.5) spawnling (2.1.5) - test-unit (3.5.7) + test-unit (3.6.2) power_assert - tzinfo (2.0.5) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) webmock (3.23.0) addressable (>= 2.8.0) diff --git a/lib/ontologies_api_client/base.rb b/lib/ontologies_api_client/base.rb index ff4aba6..a4404df 100644 --- a/lib/ontologies_api_client/base.rb +++ b/lib/ontologies_api_client/base.rb @@ -132,11 +132,16 @@ def create_attributes(attributes) attr_exists = self.public_methods(false).include?(attr) unless attr_exists self.class.class_eval do - define_method attr.to_sym do - instance_variable_get("@#{attr}") + unless method_defined?(attr.to_sym) + define_method attr.to_sym do + instance_variable_get("@#{attr}") + end end - define_method "#{attr}=" do |val| - instance_variable_set("@#{attr}", val) + + unless method_defined?("#{attr}=".to_sym) + define_method "#{attr}=" do |val| + instance_variable_set("@#{attr}", val) + end end end end diff --git a/lib/ontologies_api_client/collection.rb b/lib/ontologies_api_client/collection.rb index 77572bf..a4d34c2 100644 --- a/lib/ontologies_api_client/collection.rb +++ b/lib/ontologies_api_client/collection.rb @@ -1,11 +1,15 @@ require_relative 'config' require_relative 'http' +require_relative 'request_federation' +require 'parallel' module LinkedData module Client module Collection + def self.included(base) + base.include LinkedData::Client::RequestFederation base.extend(ClassMethods) end @@ -24,8 +28,8 @@ def method_missing(meth, *args, &block) ## # Get all top-level links for the API - def top_level_links - @top_level_links||= HTTP.get(LinkedData::Client.settings.rest_url) + def top_level_links(link = LinkedData::Client.settings.rest_url) + HTTP.get(link) end ## @@ -36,11 +40,14 @@ def uri_from_context(object, media_type) end end + ## # Get the first collection of resources for a given type def entry_point(media_type, params = {}) - params = {include: @include_attrs}.merge(params) - HTTP.get(uri_from_context(top_level_links, media_type), params) + params = { include: @include_attrs, display_links: false, display_context: false}.merge(params) + federated_get(params) do |url| + uri_from_context(top_level_links(url), media_type) rescue nil + end end ## From f651f9f920a55492629ad08a83dde4ed65278def Mon Sep 17 00:00:00 2001 From: Syphax Bouazzouni Date: Mon, 2 Sep 2024 00:02:51 +0200 Subject: [PATCH 13/18] update the special case of analytics to handle federated calls --- lib/ontologies_api_client/analytics.rb | 35 +++++++++++++++++++------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/lib/ontologies_api_client/analytics.rb b/lib/ontologies_api_client/analytics.rb index cbb85ac..b6a78cc 100644 --- a/lib/ontologies_api_client/analytics.rb +++ b/lib/ontologies_api_client/analytics.rb @@ -1,6 +1,9 @@ +require_relative 'request_federation' + module LinkedData::Client class Analytics HTTP = LinkedData::Client::HTTP + include LinkedData::Client::RequestFederation attr_accessor :onts, :date @@ -10,18 +13,32 @@ def self.all(params = {}) def self.last_month data = self.new - data.date = last_month = DateTime.now - 1.month + last_month = DateTime.now.prev_month year_num = last_month.year month_num = last_month.month - analytics = get(:analytics, {year: year_num, month: month_num}).to_h - analytics.delete(:links) - analytics.delete(:context) + params = { year: year_num, month: month_num } + + responses = federated_get(params) do |url| + "#{url}/analytics" + end + + portals = request_portals onts = [] - analytics.keys.each do |ont| - views = analytics[ont][:"#{year_num}"][:"#{month_num}"] - onts << {ont: ont, views: views} + responses.each_with_index do |portal_views, index| + next nil if portal_views&.errors + + portal_views = portal_views.to_h + + url = portals[index].url_prefix.to_s.chomp('/') + portal_views.delete(:links) + portal_views.delete(:context) + portal_views.keys.map do |ont| + views = portal_views[ont][:"#{year_num}"][:"#{month_num}"] + onts << { ont: "#{url}/ontologies/#{ont}", views: views } + end end - data.onts = onts + + data.onts = onts.flatten.compact data end @@ -29,7 +46,7 @@ def self.last_month def self.get(path, params = {}) path = path.to_s - path = "/"+path unless path.start_with?("/") + path = "/" + path unless path.start_with?("/") HTTP.get(path, params) end From ccf38c2054689d3f99d86cc1827126c380c9d90a Mon Sep 17 00:00:00 2001 From: Syphax Date: Mon, 9 Sep 2024 16:42:26 +0200 Subject: [PATCH 14/18] in parallel calls save main thread states a cross threads --- lib/ontologies_api_client/config.rb | 19 +++++++++++-------- .../request_federation.rb | 4 +++- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/ontologies_api_client/config.rb b/lib/ontologies_api_client/config.rb index fbf6cfb..e2d063e 100644 --- a/lib/ontologies_api_client/config.rb +++ b/lib/ontologies_api_client/config.rb @@ -38,7 +38,7 @@ def config(&block) def config_connection(options = {}) return if @settings_run_connection store = options[:cache_store] || ActiveSupport::Cache::MemoryStore.new - @settings.conn = faraday_connection(@settings.rest_url, @settings.apikey, store) + @settings.conn = faraday_connection(@settings.rest_url, @settings.apikey, store, current_portal: true) @settings.federated_conn = @settings.federated_portals.map do |portal_name, portal_info| [portal_name, faraday_connection(portal_info[:api], portal_info[:apikey], store)] end.to_h @@ -51,21 +51,24 @@ def connection_configured? end private - def faraday_connection(url, apikey, store) + def faraday_connection(url, apikey, store, current_portal: false) Faraday.new(url.to_s.chomp('/')) do |faraday| + if @settings.enable_long_request_log require_relative 'middleware/faraday-long-requests' faraday.use :long_requests end - require_relative 'middleware/faraday-user-apikey' - faraday.use :user_apikey + if current_portal + require_relative 'middleware/faraday-user-apikey' + faraday.use :user_apikey - require_relative 'middleware/faraday-slices' - faraday.use :ncbo_slices + require_relative 'middleware/faraday-slices' + faraday.use :ncbo_slices - require_relative 'middleware/faraday-last-updated' - faraday.use :last_updated + require_relative 'middleware/faraday-last-updated' + faraday.use :last_updated + end if @settings.cache begin diff --git a/lib/ontologies_api_client/request_federation.rb b/lib/ontologies_api_client/request_federation.rb index 458af90..ff1c9e3 100644 --- a/lib/ontologies_api_client/request_federation.rb +++ b/lib/ontologies_api_client/request_federation.rb @@ -11,11 +11,13 @@ def self.included(base) module ClassMethods def federated_get(params = {}, &link) portals = request_portals(params) + main_thread_locals = Thread.current.keys.map { |key| [key, Thread.current[key]] }.to_h connections = Parallel.map(portals, in_threads: portals.size) do |conn| + main_thread_locals.each { |key, value| Thread.current[key] = value } begin HTTP.get(link.call(conn.url_prefix.to_s.chomp('/')), params, connection: conn) - rescue StandardError => e + rescue Exception => e [OpenStruct.new(errors: "Problem retrieving #{link.call(conn.url_prefix.to_s.chomp('/')) || conn.url_prefix}")] end end From 0598e0068e545a5be9bbb501b0155c8bb2d9afb5 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Mon, 14 Oct 2024 10:57:42 +0200 Subject: [PATCH 15/18] Feature: Aggretate federated search results (#18) * handle ssl error exception when federated call to a collection * federate search endpoint and merge results * fix activesupport gem version issue * fix search params to keep federated and non federated calls work * put collections mergin only for class search * pass federation errors in federated search results * remove unused parallel dependency in class.rb * clean class.rb federation code * update the activesupport gem version * remove uncompleted federated search test * use the key "collection" instead of "results" in federated search result hash to maintain the endpoint expected schema * add test for federated search --- Gemfile.lock | 1 + lib/ontologies_api_client/models/class.rb | 22 ++++++++++++++++++- .../request_federation.rb | 2 ++ test/models/test_federation.rb | 19 +++++++++++++++- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index dd5fb94..b147d35 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,6 +73,7 @@ GEM hashdiff (>= 0.4.0, < 2.0.0) PLATFORMS + ruby x86_64-darwin-21 x86_64-darwin-23 x86_64-linux diff --git a/lib/ontologies_api_client/models/class.rb b/lib/ontologies_api_client/models/class.rb index e249887..6362c98 100644 --- a/lib/ontologies_api_client/models/class.rb +++ b/lib/ontologies_api_client/models/class.rb @@ -1,11 +1,14 @@ require "cgi" require_relative "../base" +require_relative "../request_federation" module LinkedData module Client module Models + class Class < LinkedData::Client::Base HTTP = LinkedData::Client::HTTP + include LinkedData::Client::RequestFederation @media_type = %w[http://www.w3.org/2002/07/owl#Class http://www.w3.org/2004/02/skos/core#Concept] @include_attrs = "prefLabel,definition,synonym,obsolete,hasChildren,inScheme,memberOf" @include_attrs_full = "prefLabel,definition,synonym,obsolete,properties,hasChildren,childre,inScheme,memberOf" @@ -61,10 +64,27 @@ def self.find(id, ontology, params = {}) def self.search(*args) query = args.shift + params = args.shift || {} + params[:q] = query + raise ArgumentError, "You must provide a search query: Class.search(query: 'melanoma')" if query.nil? || !query.is_a?(String) - HTTP.post("/search", params) + + + search_result = federated_get(params) do |url| + "#{url}/search" + end + merged_collections = {collection: [], errors: []} + search_result.each do |result| + if result.collection + merged_collections[:collection].concat(result.collection) + elsif result.errors + merged_collections[:errors] << result.errors + end + end + merged_collections + end def expanded? diff --git a/lib/ontologies_api_client/request_federation.rb b/lib/ontologies_api_client/request_federation.rb index ff1c9e3..b57d3db 100644 --- a/lib/ontologies_api_client/request_federation.rb +++ b/lib/ontologies_api_client/request_federation.rb @@ -25,6 +25,8 @@ def federated_get(params = {}, &link) connections.flatten end + + def request_portals(params = {}) federate = params.delete(:federate) || ::RequestStore.store[:federated_portals] diff --git a/test/models/test_federation.rb b/test/models/test_federation.rb index 4890af5..600f2bf 100644 --- a/test/models/test_federation.rb +++ b/test/models/test_federation.rb @@ -119,4 +119,21 @@ def test_federation_ssl_error refute_nil ontologies_federate_one.first.errors WebMock.disable! end -end \ No newline at end of file + + def test_federated_search + query = 'test' + + time1 = Benchmark.realtime do + @search_results = LinkedData::Client::Models::Class.search(query)[:collection] + end + + time2 = Benchmark.realtime do + @federated_search_results = LinkedData::Client::Models::Class.search(query, {federate: 'true'})[:collection] + end + + puts "Search results: #{@search_results .length} in #{time1}s" + puts "Federated search results: #{@federated_search_results.length} in #{time2}s" + + refute_equal @search_results.length, @federated_search_results.length + end +end From 8788515be8be4101fba62dc4561b3d14b12d40a0 Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Wed, 16 Oct 2024 23:47:24 +0200 Subject: [PATCH 16/18] Feature: Add rails performance gem and save API calls as custom events (#20) * add rails performance gem * save as a custom event the API calls * disable by default the rail performance monitoring * use directly RailsPerformance::Models::CustomRecord instead of measure --- Gemfile.lock | 60 +++++++++++++++++++++++++++++-- lib/ontologies_api_client/http.rb | 14 ++++++++ ontologies_api_client.gemspec | 1 + 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b147d35..0e45725 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,13 +11,27 @@ PATH multi_json oj parallel + rails_performance request_store spawnling (= 2.1.5) GEM remote: https://rubygems.org/ specs: - activesupport (7.0.8.1) + actionpack (7.0.4) + actionview (= 7.0.4) + activesupport (= 7.0.4) + rack (~> 2.0, >= 2.2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actionview (7.0.4) + activesupport (= 7.0.4) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activesupport (7.0.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -25,11 +39,16 @@ GEM addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) bigdecimal (3.1.7) + browser (5.3.1) + builder (3.3.0) coderay (1.1.3) concurrent-ruby (1.2.3) + connection_pool (2.4.1) crack (1.0.0) bigdecimal rexml + crass (1.0.6) + erubi (1.13.0) excon (0.110.0) faraday (2.0.1) faraday-net_http (~> 2.0) @@ -43,11 +62,16 @@ GEM hashdiff (1.1.0) i18n (1.14.4) concurrent-ruby (~> 1.0) + loofah (2.22.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) lz4-ruby (0.3.3) method_source (1.1.0) minitest (5.22.3) multi_json (1.15.0) multipart-post (2.4.0) + nokogiri (1.15.6-x86_64-linux) + racc (~> 1.4) oj (3.16.3) bigdecimal (>= 3.0) parallel (1.24.0) @@ -56,8 +80,36 @@ GEM coderay (~> 1.1) method_source (~> 1.0) public_suffix (5.0.5) - rack (3.0.10) + racc (1.8.1) + rack (2.2.9) + rack-test (2.1.0) + rack (>= 1.3) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + rails_performance (1.2.2) + browser + railties + redis + redis-namespace + railties (7.0.4) + actionpack (= 7.0.4) + activesupport (= 7.0.4) + method_source + rake (>= 12.2) + thor (~> 1.0) + zeitwerk (~> 2.5) rake (13.2.1) + redis (5.3.0) + redis-client (>= 0.22.0) + redis-client (0.22.2) + connection_pool + redis-namespace (1.11.0) + redis (>= 4) request_store (1.7.0) rack (>= 1.4) rexml (3.2.6) @@ -65,12 +117,14 @@ GEM spawnling (2.1.5) test-unit (3.6.2) power_assert + thor (1.3.2) tzinfo (2.0.6) concurrent-ruby (~> 1.0) webmock (3.23.0) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) + zeitwerk (2.6.18) PLATFORMS ruby @@ -86,4 +140,4 @@ DEPENDENCIES webmock BUNDLED WITH - 2.4.21 + 2.4.22 diff --git a/lib/ontologies_api_client/http.rb b/lib/ontologies_api_client/http.rb index ebd8750..36dc67a 100644 --- a/lib/ontologies_api_client/http.rb +++ b/lib/ontologies_api_client/http.rb @@ -4,6 +4,7 @@ require 'ostruct' require 'benchmark' require 'active_support/cache' +require 'rails_performance' if defined?(Rails) ## # This monkeypatch makes OpenStruct act like Struct objects class OpenStruct @@ -77,6 +78,8 @@ def self.get(path, params = {}, options = {}) req.headers[:invalidate_cache] = invalidate_cache end end + + monitor_request(params, path, response, time) puts "Getting: #{path} with #{params} (t: #{time}s - cache: #{response.headers["X-Rack-Cache"]})" if $DEBUG_API_CLIENT rescue Exception => e params = Faraday::Utils.build_query(params) @@ -164,6 +167,17 @@ def self.object_from_json(json) private + def self.monitor_request(params, path, response, time) + RailsPerformance::Models::CustomRecord.new( + tag_name: "Getting: #{path} with #{params} - cache: (#{response.headers["X-Rack-Cache"]})", + namespace_name: "API call #{path}", + status: response.status, + duration: time, + datetime: Time.current.strftime(RailsPerformance::FORMAT), + datetimei: Time.current.to_i).save if defined?(Rails) && RailsPerformance.enabled + end + + def self.custom_req(obj, file, file_attribute, req) req.headers['Content-Type'] = 'application/json' diff --git a/ontologies_api_client.gemspec b/ontologies_api_client.gemspec index b2ae8c2..63cc247 100644 --- a/ontologies_api_client.gemspec +++ b/ontologies_api_client.gemspec @@ -23,4 +23,5 @@ Gem::Specification.new do |gem| gem.add_dependency('parallel') gem.add_dependency('request_store') gem.add_dependency('spawnling', '2.1.5') + gem.add_dependency('rails_performance') end From 65ac0d61fa363144906b761426e90c6b4a9b2c35 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Sat, 26 Oct 2024 09:29:36 +0200 Subject: [PATCH 17/18] Fix: Return search results as an object instead of a hash (#21) * return search results as an object instad of a hash * update test federated search to use search results as an object instead of a hash * fix tests timeout by using the dev server of biodivportal --------- Co-authored-by: Syphax --- config/config.test.rb | 2 +- lib/ontologies_api_client/models/class.rb | 2 +- test/models/test_federation.rb | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/config.test.rb b/config/config.test.rb index 1200719..f259b60 100644 --- a/config/config.test.rb +++ b/config/config.test.rb @@ -25,7 +25,7 @@ # color: '#1e2251', # }, biodivportal: { - api: 'https://data.biodivportal.gfbio.org/', + api: 'https://data.biodivportal.gfbio.dev/', apikey: "47a57aa3-7b54-4f34-b695-dbb5f5b7363e", color: '#1e2251', } diff --git a/lib/ontologies_api_client/models/class.rb b/lib/ontologies_api_client/models/class.rb index 6362c98..41895d5 100644 --- a/lib/ontologies_api_client/models/class.rb +++ b/lib/ontologies_api_client/models/class.rb @@ -83,7 +83,7 @@ def self.search(*args) merged_collections[:errors] << result.errors end end - merged_collections + OpenStruct.new(merged_collections) end diff --git a/test/models/test_federation.rb b/test/models/test_federation.rb index 600f2bf..5a9a147 100644 --- a/test/models/test_federation.rb +++ b/test/models/test_federation.rb @@ -124,14 +124,14 @@ def test_federated_search query = 'test' time1 = Benchmark.realtime do - @search_results = LinkedData::Client::Models::Class.search(query)[:collection] + @search_results = LinkedData::Client::Models::Class.search(query).collection end time2 = Benchmark.realtime do - @federated_search_results = LinkedData::Client::Models::Class.search(query, {federate: 'true'})[:collection] + @federated_search_results = LinkedData::Client::Models::Class.search(query, {federate: 'true'}).collection end - puts "Search results: #{@search_results .length} in #{time1}s" + puts "Search results: #{@search_results.length} in #{time1}s" puts "Federated search results: #{@federated_search_results.length} in #{time2}s" refute_equal @search_results.length, @federated_search_results.length From b161ff6a54314b5abc9abc507433c12015b5e8f7 Mon Sep 17 00:00:00 2001 From: Syphax Date: Mon, 28 Oct 2024 11:39:21 +0100 Subject: [PATCH 18/18] Revert "Feature: Add rails performance gem and save API calls as custom events (#20)" This reverts commit 8788515be8be4101fba62dc4561b3d14b12d40a0. --- Gemfile.lock | 60 ++----------------------------- lib/ontologies_api_client/http.rb | 14 -------- ontologies_api_client.gemspec | 1 - 3 files changed, 3 insertions(+), 72 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0e45725..b147d35 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,27 +11,13 @@ PATH multi_json oj parallel - rails_performance request_store spawnling (= 2.1.5) GEM remote: https://rubygems.org/ specs: - actionpack (7.0.4) - actionview (= 7.0.4) - activesupport (= 7.0.4) - rack (~> 2.0, >= 2.2.0) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actionview (7.0.4) - activesupport (= 7.0.4) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activesupport (7.0.4) + activesupport (7.0.8.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -39,16 +25,11 @@ GEM addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) bigdecimal (3.1.7) - browser (5.3.1) - builder (3.3.0) coderay (1.1.3) concurrent-ruby (1.2.3) - connection_pool (2.4.1) crack (1.0.0) bigdecimal rexml - crass (1.0.6) - erubi (1.13.0) excon (0.110.0) faraday (2.0.1) faraday-net_http (~> 2.0) @@ -62,16 +43,11 @@ GEM hashdiff (1.1.0) i18n (1.14.4) concurrent-ruby (~> 1.0) - loofah (2.22.0) - crass (~> 1.0.2) - nokogiri (>= 1.12.0) lz4-ruby (0.3.3) method_source (1.1.0) minitest (5.22.3) multi_json (1.15.0) multipart-post (2.4.0) - nokogiri (1.15.6-x86_64-linux) - racc (~> 1.4) oj (3.16.3) bigdecimal (>= 3.0) parallel (1.24.0) @@ -80,36 +56,8 @@ GEM coderay (~> 1.1) method_source (~> 1.0) public_suffix (5.0.5) - racc (1.8.1) - rack (2.2.9) - rack-test (2.1.0) - rack (>= 1.3) - rails-dom-testing (2.2.0) - activesupport (>= 5.0.0) - minitest - nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) - loofah (~> 2.21) - nokogiri (~> 1.14) - rails_performance (1.2.2) - browser - railties - redis - redis-namespace - railties (7.0.4) - actionpack (= 7.0.4) - activesupport (= 7.0.4) - method_source - rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) + rack (3.0.10) rake (13.2.1) - redis (5.3.0) - redis-client (>= 0.22.0) - redis-client (0.22.2) - connection_pool - redis-namespace (1.11.0) - redis (>= 4) request_store (1.7.0) rack (>= 1.4) rexml (3.2.6) @@ -117,14 +65,12 @@ GEM spawnling (2.1.5) test-unit (3.6.2) power_assert - thor (1.3.2) tzinfo (2.0.6) concurrent-ruby (~> 1.0) webmock (3.23.0) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - zeitwerk (2.6.18) PLATFORMS ruby @@ -140,4 +86,4 @@ DEPENDENCIES webmock BUNDLED WITH - 2.4.22 + 2.4.21 diff --git a/lib/ontologies_api_client/http.rb b/lib/ontologies_api_client/http.rb index 36dc67a..ebd8750 100644 --- a/lib/ontologies_api_client/http.rb +++ b/lib/ontologies_api_client/http.rb @@ -4,7 +4,6 @@ require 'ostruct' require 'benchmark' require 'active_support/cache' -require 'rails_performance' if defined?(Rails) ## # This monkeypatch makes OpenStruct act like Struct objects class OpenStruct @@ -78,8 +77,6 @@ def self.get(path, params = {}, options = {}) req.headers[:invalidate_cache] = invalidate_cache end end - - monitor_request(params, path, response, time) puts "Getting: #{path} with #{params} (t: #{time}s - cache: #{response.headers["X-Rack-Cache"]})" if $DEBUG_API_CLIENT rescue Exception => e params = Faraday::Utils.build_query(params) @@ -167,17 +164,6 @@ def self.object_from_json(json) private - def self.monitor_request(params, path, response, time) - RailsPerformance::Models::CustomRecord.new( - tag_name: "Getting: #{path} with #{params} - cache: (#{response.headers["X-Rack-Cache"]})", - namespace_name: "API call #{path}", - status: response.status, - duration: time, - datetime: Time.current.strftime(RailsPerformance::FORMAT), - datetimei: Time.current.to_i).save if defined?(Rails) && RailsPerformance.enabled - end - - def self.custom_req(obj, file, file_attribute, req) req.headers['Content-Type'] = 'application/json' diff --git a/ontologies_api_client.gemspec b/ontologies_api_client.gemspec index 63cc247..b2ae8c2 100644 --- a/ontologies_api_client.gemspec +++ b/ontologies_api_client.gemspec @@ -23,5 +23,4 @@ Gem::Specification.new do |gem| gem.add_dependency('parallel') gem.add_dependency('request_store') gem.add_dependency('spawnling', '2.1.5') - gem.add_dependency('rails_performance') end