Class: A4Tools::CachingClient

Inherits:
AcresClient show all
Defined in:
lib/clients/caching_client.rb

Class Attribute Summary collapse

Instance Attribute Summary collapse

Attributes inherited from AcresClient

#connect_time, #connected, #history, #password, #ready, #server_info, #start_time, #token, #uri, #username, #version

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from AcresClient

#add_message_to_history, #app_info, #attempt_snoop, #authenticate, #authenticate_if_needed, #connect, #connect_if_needed, #device_info, #disconnect, #empty_query, #inject_token, #jsonrpc_message, #make_password, #next_msg_id, #response_body, #send_message, #snoop_token, #transport_connect, #wrapped_message

Methods included from EventGenerator

#on, #passthrough, #signal

Constructor Details

#initialize(destination, username = nil, password = nil, timeout = 300) ⇒ CachingClient

Returns a new instance of CachingClient.



30
31
32
33
34
35
36
37
# File 'lib/clients/caching_client.rb', line 30

def initialize(destination, username=nil, password=nil, timeout=300)
  super(destination, username, password)
  @timeout = timeout
  @query_cache = {}
  @cache = {}
  @update_thread = nil
  @query_methods = self.class.query_methods.clone rescue {}
end

Class Attribute Details

.cache_methodsObject

Returns the value of attribute cache_methods.



12
13
14
# File 'lib/clients/caching_client.rb', line 12

def cache_methods
  @cache_methods
end

.query_methodsObject

Returns the value of attribute query_methods.



12
13
14
# File 'lib/clients/caching_client.rb', line 12

def query_methods
  @query_methods
end

Instance Attribute Details

#timeoutObject

Returns the value of attribute timeout.



9
10
11
# File 'lib/clients/caching_client.rb', line 9

def timeout
  @timeout
end

Class Method Details

.cache(name, params = {}, &blk) ⇒ Object



13
14
15
16
17
18
19
# File 'lib/clients/caching_client.rb', line 13

def cache(name, params={}, &blk)
  @cache_methods ||= {}
  @cache_methods[name] = {
    params:params,
    block:blk
  }
end

.query(name, params = {}, &blk) ⇒ Object



21
22
23
24
25
26
27
# File 'lib/clients/caching_client.rb', line 21

def query(name, params={}, &blk)
  @query_methods ||= {}
  @query_methods[name] = {
    params:params,
    block:blk
  }
end

Instance Method Details

#[](key) ⇒ Object



74
75
76
# File 'lib/clients/caching_client.rb', line 74

def [](key)
  cache(key)
end

#cache(key, sync = false) ⇒ Object



78
79
80
81
82
83
84
85
86
# File 'lib/clients/caching_client.rb', line 78

def cache(key, sync=false)
  if sync then
    refresh unless fresh? # force caller to wait until we've refreshed
  else
    refresh_async unless fresh? # kick off an async update if we're stale, but still return what we have
  end
  
  @cache[key]
end

#dirtyObject

Force a cache update next time someone makes a request



109
110
111
112
# File 'lib/clients/caching_client.rb', line 109

def dirty
  @cache_update_ts = nil
  @query_methods.each { |method, defn| defn[:update] = nil }
end

#ensure_freshObject



114
115
116
# File 'lib/clients/caching_client.rb', line 114

def ensure_fresh
  refresh unless fresh?
end

#fresh?Boolean

Returns:

  • (Boolean)


118
119
120
# File 'lib/clients/caching_client.rb', line 118

def fresh?
  not timestamp_expired?(@cache_update_ts)
end

#query(method, *args) ⇒ Object

Raises:



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/clients/caching_client.rb', line 88

def query(method, *args)
  defn = @query_methods[method]
  raise UpdateError, "#{method}: Unsupported method" if defn.nil?

  if defn[:params][:authenticate] then
    raise(AuthenticationError, "Unable to authenticate as #{username}") unless authenticate_if_needed
  end

  @query_cache[method] ||= {}
  item = @query_cache[method] || {}
  return item[:cache] unless timestamp_expired?(item[:update])

  request = defn[:block].nil? ? nil : defn[:block].call(*args)
  result = update_from_method(method, request)
  item[:update] = Time.now
  item[:cache] = result
  @query_cache[method] = item
  result
end

#refreshObject



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/clients/caching_client.rb', line 138

def refresh
  unless @update_thread.nil? then
    # We already have an update thread in flight; block on that
    @update_thread.join unless @update_thread == Thread.current
    return @cache
  end

  @update_thread = Thread.current
  @cache = send_update_request
  @cache_update_ts = Time.now
  @update_thread = nil
end

#refresh_asyncObject



126
127
128
129
130
131
132
133
134
135
136
# File 'lib/clients/caching_client.rb', line 126

def refresh_async
  # Create a new thread to get the update cache, unless one is already in flight
  if @update_thread.nil? then
    begin
      Thread.new { refresh }
    rescue AuthenticationError, UpdateError => exc
      signal(:error, exc.message)
    end
  end
  nil
end

#send_update_requestObject



39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/clients/caching_client.rb', line 39

def send_update_request
  new_cache = {}
  return {} if self.class.cache_methods.nil?
  self.class.cache_methods.each do |method, defn|
    if defn[:params][:authenticate] then
      raise(AuthenticationError, "Unable to authenticate") unless authenticate_if_needed
    end

    request = defn[:block].nil? ? nil : defn[:block].call
    new_cache[method] = update_from_method(method, request)
  end
  new_cache
end

#timestamp_expired?(ts) ⇒ Boolean

Returns:

  • (Boolean)


122
123
124
# File 'lib/clients/caching_client.rb', line 122

def timestamp_expired?(ts)
  ts.nil? or (Time.now - ts) > @timeout
end

#update_from_method(method, request, type = nil) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/clients/caching_client.rb', line 53

def update_from_method(method, request, type=nil)
  type ||= talk.expand_name(talk.method_named(method.to_s)[0][:request])
  inject_token(request, false, type)
  body = request.nil? ? empty_query(method) : wrapped_message(method.to_s, type, request)
  result = send_message(body)
  begin
    parsed = symbolify(JSON.parse(result.body))
    raise(UpdateError, "#{method}: #{parsed[:error][:code]} #{parsed[:error][:message]}") unless parsed[:error].nil?
    raise(UpdateError, "#{method}: Server did not include a response field") if parsed[:result].nil?
    toplevel = parsed[:result]
    if toplevel.has_key?(:body) and toplevel.has_key?(:className) then
      toplevel = toplevel[:body]
      toplevel[:__class] = parsed[:result][:className] # strip namedobjectwrapper
    end
    toplevel
  rescue JSON::ParserError
    truncated = result.body.length > 100 ? result.body[0..99] + "..." : result.body
    raise(UpdateError, "#{method}: Server returned non-JSON response: (#{result.body.length} bytes) '#{truncated}'")
  end
end