rattlecache 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .rvmrc
6
+ test.rb
7
+ .idea/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rattlecache.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2011 Marv Cool
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ Rattlecache is an abstraction layer between the battle.net API and Libraries (in particular the ruby gem "battlnet") that consume it.
2
+ It supports multiple backends (in which the json objects are stored) and takes care of expiring timers for the objects.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+ RSpec::Core::RakeTask.new(:spec) do |t|
6
+ t.rspec_opts = ['--color', '-f progress', '-r ./spec/spec_helper.rb']
7
+ t.pattern = 'spec/**/*_spec.rb'
8
+ t.fail_on_error = false
9
+ end
@@ -0,0 +1,49 @@
1
+ module Rattlecache
2
+ module Backend
3
+
4
+ class InvalidBackend < Exception; end
5
+
6
+ extend self
7
+
8
+ attr_reader :backends
9
+
10
+ @backends = {
11
+ :filesystem => "Filesystem"
12
+ #:redis => "Redis",
13
+ #:somOtherCoolBackend => "someOther"
14
+ }
15
+
16
+ def fetch(backend_name)
17
+ unless @backends.include?(backend_name)
18
+ raise InvalidBackend.new("#{backend_name.to_s} is not a valid backend!")
19
+ end
20
+
21
+ backend_class = @backends[backend_name]
22
+ return load_backend(backend_name, backend_class)
23
+ end
24
+
25
+ def register(identifier, klass)
26
+ @backends[identifier] = klass
27
+ end
28
+
29
+ private
30
+
31
+ def load_backend(backend_name, klass_name)
32
+ begin
33
+ klass = Rattlecache::Backend.const_get("#{klass_name}", false)
34
+ rescue NameError
35
+ begin
36
+ backend_file = "backends/#{backend_name.to_s}"
37
+ require backend_file
38
+ klass = Rattlecache::Backend.const_get("#{klass_name}", false)
39
+ rescue LoadError
40
+ raise InvalidBackend.new("backend #{klass_name} does not exist, and file #{backend_file} does not exist")
41
+ rescue NameError
42
+ raise InvalidBackend.new("expected #{backend_file} to define Rattlecache::Backend::#{klass_name}")
43
+ end
44
+ end
45
+
46
+ return klass.new
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,55 @@
1
+ require 'json'
2
+ require 'base64'
3
+
4
+ module Rattlecache
5
+ module Backend
6
+ class Filesystem < Cache
7
+
8
+ def initialize(prefix = "/tmp/rattlecache/")
9
+ @prefix = prefix
10
+ end
11
+
12
+ def get(objectKey)
13
+ #puts "Filesystem gets you: #{objectKey}"
14
+ # get the file from the filesystem
15
+ ret = {:status => 404, :lastModified => nil, :object => nil}
16
+ begin
17
+ f = open_file(objectKey)
18
+ firstline = f.first
19
+ object = f.read
20
+ mtime = f.mtime
21
+ f.close
22
+ ret = {:status => 200,
23
+ :object => object,
24
+ :lastModified => mtime,
25
+ :header => Base64.strict_decode64(firstline.chomp)
26
+ }
27
+ rescue Errno::ENOENT
28
+ end
29
+ return ret
30
+ end
31
+
32
+ def post(object)
33
+ # put the file to the filesysten
34
+ firstline = Base64.strict_encode64(object[:header].to_json)
35
+ #puts "Debug: putting headers as firstline: #{firstline}"
36
+ f = open_file(object[:key],"w")
37
+ f.puts(firstline)
38
+ f.puts(object[:data])
39
+ f.close
40
+ #puts "Debug: filesystem posted #{object[:key]}"
41
+ end
42
+
43
+ def open_file(objectKey,how="r")
44
+ begin
45
+ Dir.mkdir(@prefix) unless File.directory?(@prefix)
46
+ File.open(@prefix+objectKey,how)
47
+ rescue
48
+ # raise this to the caller
49
+ raise
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,79 @@
1
+ module Rattlecache
2
+ class Auctionscache < Cache
3
+
4
+
5
+ # @param backend [Rattlecache::Backend]
6
+ # @param adapter [Battlenet::Adapter::AbstractAdapter]
7
+ def initialize(backend, adapter)
8
+ @adapter = adapter
9
+ @backend = backend
10
+ end
11
+
12
+ # @param url [String]
13
+ # @return [String|nil]
14
+ def get(url,header)
15
+
16
+ cached_data = @backend.get(sanitize(url))
17
+
18
+ # Check if any fields are requested.
19
+ # If yes, request without fields and look into ['lastModified']
20
+ # if we need to fetch new or answer with our cached result.",
21
+ if cached_data[:status] == 200
22
+ puts "Debug: Auctionscache: cache HIT for #{url}"
23
+ unless is_lmt_and_url_request(url)
24
+ puts "Debug: Auctionscache: has fields. Timethings: #{cached_data[:lastModified]} < #{get_lmt(url,header)} #{cached_data[:lastModified] < get_lmt(url,header)}"
25
+ if cached_data[:lastModified] < get_lmt(url,header)
26
+ cached_data = {:status => 404}
27
+ end
28
+ else
29
+ # if a guild is requested without additional fields,
30
+ # simply check if this file is still valid
31
+ if generic_needs_request?(cached_data[:header],cached_data[:lastModified])
32
+ cached_data = {:status => 404}
33
+ end
34
+ end
35
+ else
36
+ puts "Debug: Auctionscache: cache MISS for #{url}"
37
+ end
38
+ cached_data
39
+ end
40
+
41
+ # check if the supplied url is not pointing to a .json auctions file,
42
+ # but to a generic url for lmt and the .json url
43
+ def is_lmt_and_url_request(url)
44
+ not url.include?(".json")
45
+ end
46
+
47
+ # method to get the lastModified info for a specified url
48
+ # @param url [String] the url for which the LMT is wanted
49
+ # @param header [Hash] the Hash of headers for a api request
50
+ def get_lmt(url,header)
51
+ cached_data = @backend.get(sanitize(generic_auctions_url(url)))
52
+ puts "Debug: get_lmt() cache result for generic url: #{cached_data[:status]}"
53
+ # if the object was not in the cache or it is expired
54
+ if cached_data[:status] != 200 or generic_needs_request?(cached_data[:header],cached_data[:lastModified])
55
+ # request it from the api
56
+ request_generic(url,header)
57
+ # and load the new object
58
+ cached_data = @backend.get(sanitize(generic_auctions_url(url)))
59
+ end
60
+ # this a JS milliseconds timestamp, we only do seconds!
61
+ Time.at(JSON.parse(cached_data[:object])["lastModified"]/1000)
62
+ end
63
+
64
+ def request_generic(url,header)
65
+ # need to request the generic guild response
66
+ url = generic_auctions_url(url)
67
+ # request it from the API:
68
+ got = request_raw(url,header)
69
+ @backend.post({:key => sanitize(url),:header => got.header.to_hash, :data => got.body}) # and put into cache
70
+ end
71
+
72
+
73
+ def generic_auctions_url(url)
74
+ u = URI.parse(url)
75
+ u.path=(u.path.gsub("-data","/data").gsub(/\/auctions.*\.json$/,""))
76
+ u.to_s
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,73 @@
1
+ module Rattlecache
2
+ class Fieldsrequestcache < Cache
3
+
4
+
5
+ # @param backend [Rattlecache::Backend]
6
+ # @param adapter [Battlenet::Adapter::AbstractAdapter]
7
+ def initialize(backend, adapter)
8
+ @adapter = adapter
9
+ @backend = backend
10
+ end
11
+
12
+ # @param url [String]
13
+ # @return [String|nil]
14
+ def get(url,header)
15
+
16
+ cached_data = @backend.get(sanitize(url))
17
+
18
+ # Check if any fields are requested.
19
+ # If yes, request without fields and look into ['lastModified']
20
+ # if we need to fetch new or answer with our cached result.",
21
+ if cached_data[:status] == 200
22
+ puts "Debug: guildcache: cache HIT for #{url}"
23
+ if has_fields?(url)
24
+ puts "Debug: guildcache: has fields. Timethings: #{cached_data[:lastModified]} < #{get_lmt(url,header)} #{cached_data[:lastModified] < get_lmt(url,header)}"
25
+ if cached_data[:lastModified] < get_lmt(url,header)
26
+ cached_data = {:status => 404}
27
+ end
28
+ else
29
+ # if a guild is requested without additional fields,
30
+ # simply check if this file is still valid
31
+ if generic_needs_request?(cached_data[:header],cached_data[:lastModified])
32
+ cached_data = {:status => 404}
33
+ end
34
+ end
35
+ else
36
+ puts "Debug: guildcache: cache MISS for #{url}"
37
+ end
38
+ cached_data
39
+ end
40
+
41
+ # method to get the lastModified info for a specified url
42
+ # @param url [String] the url for which the LMT is wanted
43
+ # @param header [Hash] the Hash of headers for a api request
44
+ def get_lmt(url,header)
45
+ cached_data = @backend.get(sanitize(url_without_fields(url)))
46
+ puts "Debug: get_lmt() cache result for generic url: #{cached_data[:status]}"
47
+ # if the object was not in the cache or it is expired
48
+ if cached_data[:status] != 200 or generic_needs_request?(cached_data[:header],cached_data[:lastModified])
49
+ # request it from the api
50
+ request_without_fields(url,header)
51
+ # and load the new object
52
+ cached_data = @backend.get(sanitize(url_without_fields(url)))
53
+ end
54
+ # this a JS milliseconds timestamp, we only do seconds!
55
+ Time.at(JSON.parse(cached_data[:object])["lastModified"]/1000)
56
+ end
57
+
58
+ def request_without_fields(url,header)
59
+ # need to request the generic guild response
60
+ url = url_without_fields(url)
61
+ # request it from the API:
62
+ got = request_raw(url,header)
63
+ @backend.post({:key => sanitize(url),:header => got.header.to_hash, :data => got.body}) # and put into cache
64
+ end
65
+
66
+ def url_without_fields(url)
67
+ url_obj = URI.parse(url)
68
+ url_obj.query=(url_obj.query.gsub(/fields=.+&|$/,"")) # strip the fields
69
+ url_obj.to_s
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,144 @@
1
+ require 'backend_manager'
2
+ require 'net/http'
3
+ require 'digest/sha2'
4
+
5
+ module Rattlecache
6
+
7
+ class Cache
8
+
9
+ # @param backend [Symbol]
10
+ # @param adapter [Battlenet::Adapter::AbstractAdapter]
11
+ def initialize(backend = :filesystem, adapter = nil)
12
+ puts "Debug: rattlecache adapter: #{adapter}"
13
+ @adapter = adapter
14
+ @backend = Rattlecache::Backend.fetch(backend)
15
+ end
16
+
17
+ @request_pragmas = {
18
+ "guild" => "Check if any fields are requested. If yes, request without fields and look into ['lastModified'] if we need to fetch new or answer with out cached result.",
19
+ "data" => "Everything data related should be considered stable. Cache it for one week.",
20
+ "auction" => "dont know yet...",
21
+ "else" => "cache it for as long as 'RETRY-AFTER'-responseheader told us. (600 sec)"
22
+ }
23
+
24
+ # @param url [String]
25
+ # @param header [Hash]
26
+ def get(url, header = nil)
27
+ @header = header
28
+ @header['User-Agent'] = @header['User-Agent'] << " (with rattlecache)"
29
+ #puts "Cache class gets you: #{objectKey}"
30
+
31
+ # what to do with this request?
32
+ case request_type(url)
33
+ when "guild"
34
+ puts "Debug: its a guild related request!"
35
+ require 'caches/Fieldsrequestcache'
36
+ Rattlecache::Fieldsrequestcache.new(@backend,@adapter).get(url,header)
37
+ when "character"
38
+ puts "Debug: its a character related request!"
39
+ require 'caches/Fieldsrequestcache'
40
+ Rattlecache::Fieldsrequestcache.new(@backend,@adapter).get(url,header)
41
+ when /auction.*/
42
+ puts "Debug: its a auchtion related request!"
43
+ require 'caches/Auctionscache'
44
+ Rattlecache::Auctionscache.new(@backend,@adapter).get(url,header)
45
+ when "item"
46
+ # for items it seems reasonable to cache them at least for a week
47
+ # a week in seconds: 60*60*24*7 = 604800
48
+ check_and_return(@backend.get(sanitize(url)),604800)
49
+ else
50
+ puts "Debug: its a boring request!"
51
+ check_and_return(@backend.get(sanitize(url)))
52
+ end
53
+ end
54
+
55
+ def check_and_return(backend_result,given_time = nil)
56
+ if given_time.nil?
57
+ if backend_result[:status] == 200 and generic_needs_request?(backend_result[:header],backend_result[:lastModified])
58
+ backend_result = {:status => 404}
59
+ end
60
+ else
61
+ if backend_result[:status] == 200 and needs_request_with_given_time?(given_time,backend_result[:lastModified])
62
+ backend_result = {:status => 404}
63
+ end
64
+ end
65
+ backend_result
66
+ end
67
+
68
+ # @param object [Hash]
69
+ def post(object)
70
+ #puts "Cache class puts: #{object[:key]}"
71
+ @backend.post({:key => sanitize(object[:key]), :header => object[:header], :data => object[:data]})
72
+ end
73
+
74
+ # @param headerline [Hash]
75
+ # @param mtime [Time]
76
+ # @return [TrueClass|FalseClass]
77
+ def generic_needs_request?(headerline,mtime)
78
+ header = JSON.parse(headerline)
79
+ #header["date"][0] is a String with CGI.rfc1123_date() encoded time,
80
+ # as there is no easy method to inverse this coding, I will keep using the files mtime
81
+ # to estimate when the data was last recieved.
82
+ unless header["cache-control"].nil?
83
+ mtime+header["cache-control"][0].split("=")[1].to_i < Time.now()
84
+ else
85
+ unless header["retry-after"].nil?
86
+ mtime+(header["retry-after"][0].to_i) < Time.now()
87
+ else
88
+ # if we dont find any hint, pull it again!
89
+ puts "Warning: Cache couldn't find any hint if this object is still valid!"
90
+ true
91
+ end
92
+ end
93
+ end
94
+
95
+ def needs_request_with_given_time?(given_time,mtime)
96
+ mtime+given_time < Time.now
97
+ end
98
+
99
+ # @param objectKey [String]
100
+ # @return [String]
101
+ def sanitize(objectKey)
102
+ # strip scheme, sort paramters and encode for safty
103
+ urlObj = URI.parse(objectKey)
104
+ key = urlObj.host
105
+ key << urlObj.path
106
+ key << sort_params(urlObj.query)
107
+ Digest::SHA256.hexdigest(key)
108
+ end
109
+
110
+ # @param query [String]
111
+ # @return [String]
112
+ def sort_params(query)
113
+ q = Hash.new
114
+ query.split("&").each do |parampair|
115
+ q[parampair.split("=")[0]] = parampair.split("=")[1]
116
+ end
117
+ s = Array.new
118
+ q.sort.each { |pair| s << pair.join("=")}
119
+ "?"+s.join("&")
120
+ end
121
+
122
+ # @param objectKey [String]
123
+ # @return [String]
124
+ def request_type(objectKey)
125
+ #[0] = "". [1]= "api". [2]="wow", [3]= what we want
126
+ URI.parse(objectKey).path.split("/")[3]
127
+ end
128
+
129
+ # @param query [String]
130
+ # @return [TrueClass|FalseClass]
131
+ def has_fields?(query)
132
+ not query.scan(/fields=/).empty?
133
+ end
134
+
135
+ # @param url [String]
136
+ # @param header [Hash]
137
+ # @return [Net::HTTPResponse]
138
+ def request_raw(url,header)
139
+ req = @adapter.get(url,header,true)
140
+ req.get(url,header)
141
+ end
142
+
143
+ end
144
+ end
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "rattlecache"
6
+ s.version = "0.1"
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ["Marv Cool"]
9
+ s.email = "marv@hostin.is"
10
+ s.homepage = "https://github.com/MrMarvin/rattlcache/wiki"
11
+ s.summary = %q{A smart caching system for battlenet API Requests.}
12
+
13
+ if s.respond_to?(:add_development_dependency)
14
+ s.add_development_dependency "rspec"
15
+ end
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rattlecache
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ version: "0.1"
9
+ platform: ruby
10
+ authors:
11
+ - Marv Cool
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+
16
+ date: 2011-08-23 00:00:00 +02:00
17
+ default_executable:
18
+ dependencies:
19
+ - !ruby/object:Gem::Dependency
20
+ name: rspec
21
+ prerelease: false
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
32
+ description:
33
+ email: marv@hostin.is
34
+ executables: []
35
+
36
+ extensions: []
37
+
38
+ extra_rdoc_files: []
39
+
40
+ files:
41
+ - .gitignore
42
+ - Gemfile
43
+ - LICENSE
44
+ - README.md
45
+ - Rakefile
46
+ - lib/backend_manager.rb
47
+ - lib/backends/filesystem.rb
48
+ - lib/caches/Auctionscache.rb
49
+ - lib/caches/Fieldsrequestcache.rb
50
+ - lib/rattlecache.rb
51
+ - rattlecache.gemspec
52
+ has_rdoc: true
53
+ homepage: https://github.com/MrMarvin/rattlcache/wiki
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options: []
58
+
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ requirements: []
78
+
79
+ rubyforge_project:
80
+ rubygems_version: 1.3.7
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: A smart caching system for battlenet API Requests.
84
+ test_files: []
85
+