suj-pusher 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -59,7 +59,7 @@ To start the server run:
59
59
  To stop the server run:
60
60
 
61
61
  ```
62
- /path/to/bin/pusher start -H localhost -P 6379 -b 0 -n pusher -p /var/run/pids
62
+ /path/to/bin/pusher stop -H localhost -P 6379 -b 0 -n pusher -p /var/run/pids
63
63
  ```
64
64
 
65
65
  If you set a piddir (using -p option) when starting the server then you must supply the same option when stopping or restarting the server.
@@ -1,12 +1,15 @@
1
1
  require "eventmachine"
2
2
  require "iobuffer"
3
-
4
3
  require "base64"
4
+ require 'suj/pusher/monkey/vash'
5
+
5
6
  module Suj
6
7
  module Pusher
7
8
  class APNConnection < EM::Connection
8
9
  include Suj::Pusher::Logger
9
10
 
11
+ @@last_id = 0
12
+
10
13
  ERRORS = {
11
14
  0 => "No errors encountered",
12
15
  1 => "Processing error",
@@ -26,12 +29,16 @@ module Suj
26
29
  @disconnected = true
27
30
  @pool = pool
28
31
  @options = options
32
+ @processing_ids = Vash.new
29
33
  @cert_key = Digest::SHA1.hexdigest(@options[:cert])
30
34
  @cert_file = File.join(Suj::Pusher.config.certs_path, @cert_key)
31
35
  @buffer = IO::Buffer.new
36
+ self.comm_inactivity_timeout = 60 # Close after 60 sec of inactivity
37
+
32
38
  File.open(@cert_file, "w") do |f|
33
39
  f.write @options[:cert]
34
40
  end
41
+
35
42
  @ssl_options = {
36
43
  private_key_file: @cert_file,
37
44
  cert_chain_file: @cert_file,
@@ -39,6 +46,10 @@ module Suj
39
46
  }
40
47
  end
41
48
 
49
+ def options
50
+ @options
51
+ end
52
+
42
53
  def disconnected?
43
54
  @disconnected
44
55
  end
@@ -47,22 +58,39 @@ module Suj
47
58
  begin
48
59
  @notifications = []
49
60
  data[:apn_ids].each do |apn_id|
50
- @notifications << Suj::Pusher::ApnNotification.new(data.merge({token: apn_id}))
61
+ if ! @pool.valid_token?(@cert_key, apn_id)
62
+ warn "Skipping invalid #{apn_id} APN id"
63
+ next
64
+ end
65
+ notification = Suj::Pusher::ApnNotification.new(data.merge({token: apn_id, id: @@last_id}))
66
+ @processing_ids[@@last_id.to_s] = apn_id
67
+ @@last_id += 1
68
+ @notifications << notification
51
69
  end
52
- if ! disconnected?
53
- info "APN delivering data"
54
- send_data(@notifications.join)
55
- info "APN push notification sent"
56
- @notifications = nil
57
- info "APN delivered data"
58
- else
59
- info "APN connection unavailable"
70
+
71
+ @processing_ids.cleanup!
72
+ info "Processing list size #{@processing_ids.size}"
73
+
74
+ if @notifications.empty?
75
+ warn "No valid tokens, skip message"
76
+ return
60
77
  end
78
+
79
+ if disconnected?
80
+ warn "Connection not ready yet, queue notifications for later"
81
+ return
82
+ end
83
+
84
+ info "APN delivering data"
85
+ send_data(@notifications.join)
86
+ @notifications = nil
87
+ info "APN delivered data"
61
88
  rescue Suj::Pusher::ApnNotification::PayloadTooLarge => e
62
89
  error "APN notification payload too large."
63
90
  debug @notifications.join.inspect
64
91
  rescue => ex
65
92
  error "APN notification error : #{ex}"
93
+ error ex.backtrace
66
94
  end
67
95
  end
68
96
 
@@ -85,6 +113,10 @@ module Suj
85
113
  cmd, status, id = data.unpack("CCN")
86
114
  if cmd != 8
87
115
  error "APN push response command differs from 8"
116
+ elsif status == 8
117
+ token = @processing_ids[id.to_s]
118
+ @pool.invalidate_token(@cert_key, token)
119
+ warn "APN invalid (id: #{id}) token #{token}"
88
120
  elsif status != 0
89
121
  error "APN push error received: #{ERRORS[status]} for id #{id}"
90
122
  end
@@ -94,13 +126,13 @@ module Suj
94
126
  def connection_completed
95
127
  info "APN Connection established..."
96
128
  @disconnected = false
97
- if ! @notifications.nil?
98
- info "EST - APN delivering data"
99
- send_data(@notifications.join)
100
- info "APN push notification sent"
101
- @notifications = nil
102
- info "EST - APN delivered data"
103
- end
129
+
130
+ return if @notifications.nil? || @notifications.empty?
131
+
132
+ info "EST - APN delivering data"
133
+ send_data(@notifications.join)
134
+ @notifications = nil
135
+ info "EST - APN delivered data"
104
136
  end
105
137
 
106
138
  def unbind
@@ -12,7 +12,7 @@ module Suj
12
12
  @disconnected = true
13
13
  @options = options
14
14
  @pool = pool
15
- @cert_key = Digest::SHA1.hexdigest("FEEDBACK" + @options[:cert])
15
+ @cert_key = Digest::SHA1.hexdigest(@options[:cert])
16
16
  @cert_file = File.join(Suj::Pusher.config.certs_path, @cert_key)
17
17
  @buffer = IO::Buffer.new
18
18
  self.comm_inactivity_timeout = 10 # Close after 10 sec of inactivity
@@ -24,6 +24,7 @@ module Suj
24
24
  cert_chain_file: @cert_file,
25
25
  verify_peer: false
26
26
  }
27
+ info "APN feedback #{@cert_key}: Creating"
27
28
  end
28
29
 
29
30
  def disconnected?
@@ -31,7 +32,7 @@ module Suj
31
32
  end
32
33
 
33
34
  def post_init
34
- info "APN Feedback Connection init "
35
+ info "APN feedback #{@cert_key}: Connection init "
35
36
  start_tls(@ssl_options)
36
37
  end
37
38
 
@@ -48,19 +49,20 @@ module Suj
48
49
  while @buffer.size >= 38
49
50
  timestamp, size = @buffer.read(6).unpack("Nn")
50
51
  token = @buffer.read(size)
51
- puts " FEEDBACK TIMESTAMP: #{timestamp} SIZE: #{size} TOKEN: #{token}"
52
+ info "APN feedback #{@cert_key}: TIMESTAMP: #{timestamp} SIZE: #{size} TOKEN: #{token}"
53
+ @pool.invalidate_token(@cert_key, token)
52
54
  end
53
55
  end
54
56
 
55
57
  def connection_completed
56
- info "APN Feedback Connection established..."
58
+ info "APN feedback #{@cert_key}: Connection established..."
57
59
  @disconnected = false
58
60
  end
59
61
 
60
62
  def unbind
61
- info "APN Feedback Connection closed..."
63
+ info "APN feedback #{@cert_key}: Connection closed..."
62
64
  @disconnected = true
63
- @pool.remove_connection(@cert_key)
65
+ @pool.remove_feedback_connection(@cert_key)
64
66
  end
65
67
  end
66
68
  end
@@ -9,6 +9,7 @@ module Suj
9
9
 
10
10
  def initialize(options = {})
11
11
  @token = options[:token]
12
+ @id = options[:id]
12
13
  @ttl = options[:time_to_live] || 0
13
14
  @options = options
14
15
  raise InvalidToken if @token.nil? || (@token.length != 64)
@@ -38,17 +39,10 @@ module Suj
38
39
  end
39
40
 
40
41
  def encode_data
41
- # identifier = 0
42
- # expiry = get_expiry
43
- # size = [payload].pack("a*").size
44
- # data_array = [1, identifier, expiry, 32, @token, size, payload]
45
- # info("PAYLOAD: #{data_array}")
46
- # data_array.pack("cNNnH*na*")
47
-
48
42
  items = [
49
43
  [1, 32, @token ], # token
50
44
  [2, payload.bytesize, payload ], # payload
51
- [3, 4, OpenSSL::Random::random_bytes(4) ], # random identifier
45
+ [3, 4, @id], # id identifier
52
46
  [4, 4, get_expiry ], # expiration date
53
47
  [5, 1, 10 ] # high priority
54
48
  ]
@@ -58,7 +52,7 @@ module Suj
58
52
  frame_data =
59
53
  items[0].pack("CnH*") +
60
54
  items[1].pack("CnA*") +
61
- items[2].pack("CnA*") +
55
+ items[2].pack("CnN") +
62
56
  items[3].pack("CnN") +
63
57
  items[4].pack("CnC")
64
58
 
@@ -12,15 +12,105 @@ module Suj
12
12
  APN_GATEWAY = "gateway.push.apple.com"
13
13
  FEEDBACK_SANDBOX = "feedback.sandbox.push.apple.com"
14
14
  FEEDBACK_GATEWAY = "feedback.push.apple.com"
15
+ CONNECTION_EXPIRY = 60*60*24*30 # Approx 1 month
16
+ TOKEN_EXPIRY = 60*60*24*7 # Approx 1 week
15
17
  APN_PORT = 2195
16
18
  FEEDBACK_PORT = 2196
17
19
 
20
+ class UnknownConnection < StandardError; end
21
+
18
22
  def initialize(daemon)
19
23
  @pool = {}
24
+ @feedback_pool = {}
20
25
  @daemon = daemon
21
26
  @mutex = Mutex.new
27
+ @feedback_mutex = Mutex.new
28
+ @invalid_tokens = Vash.new
29
+ end
30
+
31
+ def get_connection(options = {})
32
+ if options.has_key?(:cert)
33
+ if options.has_key?(:development) && options[:development]
34
+ return apn_sandbox_connection(options)
35
+ else
36
+ return apn_connection(options)
37
+ end
38
+ elsif options.has_key?(:api_key)
39
+ return gcm_connection(options)
40
+ elsif options.has_key?(:secret) && options.has_key?(:sid)
41
+ return wns_connection(options)
42
+ elsif options.has_key?(:wpnotificationclass)
43
+ return wpns_connection(options)
44
+ end
45
+ raise UnknownConnection
46
+ end
47
+
48
+ def get_feedback_connection(options = {})
49
+ if options.has_key?(:cert)
50
+ if options.has_key?(:development) && options[:development]
51
+ return feedback_sandbox_connection(options)
52
+ else
53
+ return feedback_connection(options)
54
+ end
55
+ end
56
+ return nil
57
+ end
58
+
59
+ def remove_feedback_connection(key)
60
+ @feedback_mutex.synchronize {
61
+ info "Removing feedback connection #{key}"
62
+ @feedback_pool.delete(key)
63
+ }
64
+ end
65
+
66
+ def remove_connection(key)
67
+ @mutex.synchronize {
68
+ info "Removing connection #{key}"
69
+ info "Connection not found" unless @pool.delete(key)
70
+ }
71
+ end
72
+
73
+ def invalidate_token(conn, token)
74
+ @invalid_tokens[conn, CONNECTION_EXPIRY] = Vash.new if @invalid_tokens[conn].nil?
75
+ @invalid_tokens[conn][token, TOKEN_EXPIRY] = Time.now.to_s
76
+ end
77
+
78
+ def valid_token?(conn, token)
79
+ return true if ! @invalid_tokens[conn]
80
+ return false if @invalid_tokens[conn].has_key?(token)
81
+ return true
82
+ end
83
+
84
+ # Method that creates APN feedback connections
85
+ def feedback
86
+ @invalid_tokens.cleanup!
87
+ @invalid_tokens.each { |k,v| v.cleanup! if v }
88
+
89
+ @pool.each do |k, conn|
90
+ next if ! conn.is_a?(Suj::Pusher::APNConnection)
91
+ conn = get_feedback_connection(conn.options)
92
+ end
22
93
  end
23
94
 
95
+ def close
96
+ @mutex.synchronize {
97
+ @pool.each do |k, conn|
98
+ conn.close_connection_after_writing
99
+ end
100
+ }
101
+ @feedback_mutex.synchronize {
102
+ @feedback_pool.each do |k, conn|
103
+ conn.close_connection
104
+ end
105
+ }
106
+ end
107
+
108
+ def pending_connections?
109
+ @pool.size + @feedback_pool.size > 0
110
+ end
111
+
112
+ private
113
+
24
114
  def apn_connection(options = {})
25
115
  cert = Digest::SHA1.hexdigest options[:cert]
26
116
  info "APN connection #{cert}"
@@ -31,26 +121,25 @@ module Suj
31
121
 
32
122
  def apn_sandbox_connection(options = {})
33
123
  cert = Digest::SHA1.hexdigest options[:cert]
34
- info "APN connection #{cert}"
124
+ info "APN sandbox connection #{cert}"
35
125
  @mutex.synchronize do
36
126
  @pool[cert] ||= EM.connect(APN_SANDBOX, APN_PORT, APNConnection, self, options)
37
127
  end
38
128
  end
39
129
 
40
130
  def feedback_connection(options = {})
41
- cert = Digest::SHA1.hexdigest("FEEDBACK" + options[:cert])
42
- info "APN Feedback connection #{cert}"
43
- @mutex.synchronize do
44
- @pool[cert] ||= EM.connect(FEEDBACK_GATEWAY, FEEDBACK_PORT, APNFeedbackConnection, self, options)
131
+ cert = Digest::SHA1.hexdigest(options[:cert])
132
+ info "Get APN feedback connection #{cert}"
133
+ @feedback_mutex.synchronize do
134
+ @feedback_pool[cert] ||= EM.connect(FEEDBACK_GATEWAY, FEEDBACK_PORT, APNFeedbackConnection, self, options)
45
135
  end
46
136
  end
47
137
 
48
138
  def feedback_sandbox_connection(options = {})
49
- info "Feedback connection"
50
- cert = Digest::SHA1.hexdigest("FEEDBACK" + options[:cert])
51
- info "APN Sandbox Feedback connection #{cert}"
52
- @mutex.synchronize do
53
- @pool[cert] ||= EM.connect(FEEDBACK_SANDBOX, FEEDBACK_PORT, APNFeedbackConnection, self, options)
139
+ cert = Digest::SHA1.hexdigest(options[:cert])
140
+ info "Get APN sandbox feedback connection #{cert}"
141
+ @feedback_mutex.synchronize do
142
+ @feedback_pool[cert] ||= EM.connect(FEEDBACK_SANDBOX, FEEDBACK_PORT, APNFeedbackConnection, self, options)
54
143
  end
55
144
  end
56
145
 
@@ -63,14 +152,6 @@ module Suj
63
152
  end
64
153
  end
65
154
 
66
- def apn_connection(options = {})
67
- cert = Digest::SHA1.hexdigest options[:cert]
68
- info "APN connection #{cert}"
69
- @mutex.synchronize do
70
- @pool[cert] ||= EM.connect(APN_GATEWAY, APN_PORT, APNConnection, self, options)
71
- end
72
- end
73
-
74
155
  def wns_connection(options = {})
75
156
  cert = Digest::SHA1.hexdigest options[:secret]
76
157
  info "WNS connection #{cert}"
@@ -89,11 +170,6 @@ module Suj
89
170
  end
90
171
  end
91
172
 
92
- def remove_connection(key)
93
- info "Removing connection #{key}"
94
- info "Connection not found" unless @pool.delete(key)
95
- end
96
-
97
173
  end
98
174
 
99
175
  end
@@ -5,6 +5,7 @@ require 'openssl'
5
5
  require 'em-hiredis'
6
6
  require "multi_json"
7
7
  require 'fileutils'
8
+ require 'fiber'
8
9
 
9
10
  module Suj
10
11
  module Pusher
@@ -16,125 +17,92 @@ module Suj
16
17
  def start
17
18
  info "Starting pusher daemon"
18
19
  info " subsribe to push messages from #{redis_url} namespace #{redis_namespace}"
19
- @last_feedback = Time.now
20
- @last_sandbox_feedback = Time.now
21
20
  EM.run do
22
- wait_msg
23
- end
24
- end
25
21
 
26
- def stop
27
- info "Stopping daemon process"
28
- begin
29
- EM.stop
30
- rescue
31
- end
32
- info "Stopped daemon process"
33
- end
34
-
35
- private
36
-
37
- def wait_msg
38
- defer = redis.brpop "#{redis_namespace}:#{MSG_QUEUE}", 0
39
- defer.callback do |_, msg|
40
- begin
41
- info "RECEIVED MESSAGE"
42
- data = Hash.symbolize_keys(MultiJson.load(msg))
43
- send_notification(data)
44
- info "SENT MESSAGE"
45
- retrieve_feedback(data)
46
- rescue MultiJson::LoadError
47
- warn("Received invalid json data, discarding msg")
48
- rescue => e
49
- error("Error sending notification : #{e}")
50
- error e.backtrace
22
+ EM.add_periodic_timer(FEEDBACK_TIME) do
23
+ info "Starting APN feedback service"
24
+ pool.feedback
51
25
  end
52
- EM.next_tick { wait_msg }
53
- end
54
- defer.errback do |e|
55
- error e
56
- EM.next_tick { wait_msg }
57
- end
58
- end
59
26
 
60
- def send_notification(msg)
61
- if msg.has_key?(:cert)
62
- if msg.has_key?(:development) && msg[:development]
63
- send_apn_sandbox_notification(msg)
64
- else
65
- send_apn_notification(msg)
66
- end
67
- elsif msg.has_key?(:api_key)
68
- send_gcm_notification(msg)
69
- elsif msg.has_key?(:secret) && msg.has_key?(:sid)
70
- #wns push notification
71
- send_wns_notification(msg)
72
- elsif msg.has_key?(:wpnotificationclass)
73
- #send wpns push notification
74
- send_wpns_notification(msg)
75
- else
76
- warn "Could not determine push notification service."
27
+ Fiber.new {
28
+ loop
29
+ }.resume
77
30
  end
78
31
  end
79
32
 
80
- def retrieve_feedback(msg)
81
- if msg.has_key?(:cert)
82
- if msg.has_key?(:development) && msg[:development]
83
- feedback_sandbox_connection(msg)
84
- else
85
- feedback_connection(msg)
86
- end
33
+ def stop
34
+ pool.close
35
+ redis.close
36
+
37
+ if ! pool.pending_connections?
38
+ EM.stop
39
+ info "Stopped daemon process"
40
+ else
41
+ info "Waiting for pending connections"
42
+ EM.add_periodic_timer(5) {
43
+ if ! pool.pending_connections?
44
+ EM.stop
45
+ info "Stopped daemon process"
46
+ end
47
+ }
87
48
  end
88
49
  end
89
50
 
90
- def send_apn_notification(msg)
91
- info "Sending APN notification via connection #{Digest::SHA1.hexdigest(msg[:cert])}"
92
- conn = pool.apn_connection(msg)
93
- conn.deliver(msg)
94
- # msg[:apn_ids].each do |apn_id|
95
- # conn.deliver(msg.merge({token: apn_id}))
96
- # end
97
- end
51
+ private
98
52
 
99
- def send_apn_sandbox_notification(msg)
100
- info "Sending APN sandbox notification via connection #{Digest::SHA1.hexdigest(msg[:cert])}"
101
- conn = pool.apn_sandbox_connection(msg)
53
+ def loop
54
+ msg = get_msg
55
+ return next_tick { loop } if msg.nil?
56
+ conn = get_connection(msg)
57
+ return next_tick { loop } if conn.nil?
102
58
  conn.deliver(msg)
103
- # msg[:apn_ids].each do |apn_id|
104
- # conn.deliver(msg.merge({token: apn_id}))
105
- # end
106
- end
107
-
108
- def feedback_connection(msg)
109
- return Time.now - @last_feedback < FEEDBACK_TIME
110
- info "Get feedback information"
111
- conn = pool.feedback_connection(msg)
112
- @last_feedback = Time.now
59
+ next_tick { loop }
113
60
  end
114
61
 
115
- def feedback_sandbox_connection(msg)
116
- return if Time.now - @last_sandbox_feedback < FEEDBACK_TIME
117
- info "Get feedback sandbox information"
118
- conn = pool.feedback_sandbox_connection(msg)
119
- @last_sandbox_feedback = Time.now
62
+ def next_tick(&blk)
63
+ EM.next_tick { Fiber.new { blk.call }.resume }
120
64
  end
121
65
 
122
- def send_gcm_notification(msg)
123
- info "Sending GCM notification via connection #{msg[:api_key]}"
124
- conn = pool.gcm_connection(msg)
125
- conn.deliver(msg)
66
+ def msg_queue
67
+ @msg_queue ||= "#{redis_namespace}:#{MSG_QUEUE}"
126
68
  end
127
69
 
128
- def send_wns_notification(msg)
129
- info "Sending WNS notification via connection #{Digest::SHA1.hexdigest(msg[:secret])}"
130
- conn = pool.wns_connection(msg)
131
- conn.deliver(msg)
70
+ def get_msg
71
+ f = Fiber.current
72
+ begin
73
+ defer = redis.brpop msg_queue, 0
74
+ defer.callback do |_, msg|
75
+ info "Received message"
76
+ begin
77
+ data = Hash.symbolize_keys(MultiJson.load(msg))
78
+ f.resume(data)
79
+ rescue MultiJson::LoadError
80
+ warn("Message has bad json format, discarding")
81
+ f.resume(nil)
82
+ rescue => e
83
+ error(e)
84
+ f.resume(nil)
85
+ end
86
+ end
87
+ defer.errback do |e|
88
+ f.resume(nil)
89
+ end
90
+ rescue EventMachine::Hiredis::Error => e
91
+ info e
92
+ rescue => ex
93
+ error ex
94
+ end
95
+ return Fiber.yield
132
96
  end
133
97
 
134
- def send_wpns_notification(msg)
135
- info "Sending WPNS notification via connection #{Digest::SHA1.hexdigest(msg[:secret])}"
136
- conn = pool.wpns_connection(msg)
137
- conn.deliver(msg)
98
+ def get_connection(options)
99
+ info "Get connection"
100
+ begin
101
+ return pool.get_connection(options)
102
+ rescue ConnectionPool::UnknownConnection
103
+ error "Could not find connection"
104
+ return nil
105
+ end
138
106
  end
139
107
 
140
108
  def redis_url
@@ -0,0 +1,110 @@
1
+ #############################################################################
2
+ # Class: Vash (Ruby Volatile Hash)
3
+ # Hash that returns values only for a short time. This is useful as a cache
4
+ # where I/O is involved. The primary goal of this object is to reduce I/O
5
+ # access and due to the nature of I/O being slower then memory, you should also
6
+ # see a gain in quicker response times.
7
+ #
8
+ # For example, if Person.first found the first person from the database & cache
9
+ # was an instance of Vash then the following would only contact the database for
10
+ # the first iteration:
11
+ #
12
+ # > cache = Vash.new
13
+ # > 1000.times {cache[:person] ||= Person.first}
14
+ #
15
+ # However if you did the following immediately following that command it would
16
+ # hit the database again:
17
+ #
18
+ # > sleep 61
19
+ # > cache[:person] ||= Person.first
20
+ #
21
+ # The reason is that there is a default Time-To-Live of 60 seconds. You can
22
+ # also set a custom TTL of 10 seconds like so:
23
+ #
24
+ # > cache[:person, 10] = Person.first
25
+ #
26
+ # The Vash object will forget any answer that is requested after the specified
27
+ # TTL. It is a good idea to manually clean things up from time to time because
28
+ # it is possible that you'll cache data but never again access it and therefor
29
+ # it will stay in memory after the TTL has expired. To clean up the Vash object,
30
+ # call the method: cleanup!
31
+ #
32
+ # > sleep 11 # At this point the prior person ttl will be expired
33
+ # # but the person key and value will still exist.
34
+ # > cache # This will still show the the entire set of keys
35
+ # # regardless of the TTL, the :person will still exist
36
+ # > cache.cleanup! # All of the TTL's will be inspected and the expired
37
+ # # :person key will be deleted.
38
+ #
39
+ # The cleanup must be manually called because the purpose of the Vash is to
40
+ # lessen needless I/O calls and gain speed not to slow it down with regular
41
+ # maintenance.
42
+ class Vash < Hash
43
+ def initialize(constructor = {})
44
+ @register ||= {} # remembers expiration time of every key
45
+ if constructor.is_a?(Hash)
46
+ super()
47
+ merge(constructor)
48
+ else
49
+ super(constructor)
50
+ end
51
+ end
52
+
53
+ alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
54
+ alias_method :regular_reader, :[] unless method_defined?(:regular_reader)
55
+
56
+ def [](key)
57
+ sterilize(key)
58
+ clear(key) if expired?(key)
59
+ regular_reader(key)
60
+ end
61
+
62
+ def []=(key, *args)
63
+ # a little bit o variable hacking to support (h[key, ttl] = value), which will come
64
+ # accross as (key, [ttl, value]) whereas (h[key]=value) comes accross as (key, [value])
65
+ if args.length == 2
66
+ value, ttl = args[1], args[0]
67
+ elsif args.length == 1
68
+ value, ttl = args[0], 60
69
+ else
70
+ raise ArgumentError, "Wrong number of arguments, expected 2 or 3, received: #{args.length+1}\n"+
71
+ "Example Usage: volatile_hash[:key]=value OR volatile_hash[:key, ttl]=value"
72
+ end
73
+ sterilize(key)
74
+ ttl(key, ttl)
75
+ regular_writer(key, value)
76
+ end
77
+
78
+ def merge(hsh)
79
+ hsh.map {|key,value| self[sterile(key)] = hsh[key]}
80
+ self
81
+ end
82
+
83
+ def cleanup!
84
+ now = Time.now.to_i
85
+ @register.map {|k,v| clear(k) if v < now}
86
+ end
87
+
88
+ def clear(key)
89
+ sterilize(key)
90
+ @register.delete key
91
+ self.delete key
92
+ end
93
+
94
+ private
95
+ def expired?(key)
96
+ Time.now.to_i > @register[key].to_i
97
+ end
98
+
99
+ def ttl(key, secs=60)
100
+ @register[key] = Time.now.to_i + secs.to_i
101
+ end
102
+
103
+ def sterile(key)
104
+ String === key ? key.chomp('!').chomp('=') : key.to_s.chomp('!').chomp('=').to_sym
105
+ end
106
+
107
+ def sterilize(key)
108
+ key = sterile(key)
109
+ end
110
+ end
@@ -1,5 +1,5 @@
1
1
  module Suj
2
2
  module Pusher
3
- VERSION = '0.2.3'
3
+ VERSION = '0.2.5'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: suj-pusher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-12-09 00:00:00.000000000 Z
12
+ date: 2014-01-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: em-http-request
16
- requirement: &10281040 !ruby/object:Gem::Requirement
16
+ requirement: &10452580 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *10281040
24
+ version_requirements: *10452580
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: em-hiredis
27
- requirement: &10280620 !ruby/object:Gem::Requirement
27
+ requirement: &10451520 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *10280620
35
+ version_requirements: *10451520
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: multi_json
38
- requirement: &10280020 !ruby/object:Gem::Requirement
38
+ requirement: &10451080 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *10280020
46
+ version_requirements: *10451080
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: daemon-spawn
49
- requirement: &10279380 !ruby/object:Gem::Requirement
49
+ requirement: &10450660 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *10279380
57
+ version_requirements: *10450660
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: iobuffer
60
- requirement: &10295080 !ruby/object:Gem::Requirement
60
+ requirement: &10450060 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,7 +65,7 @@ dependencies:
65
65
  version: '0'
66
66
  type: :runtime
67
67
  prerelease: false
68
- version_requirements: *10295080
68
+ version_requirements: *10450060
69
69
  description: Stand alone push notification server for APN, GCM, WNS and MPNS.
70
70
  email:
71
71
  - rd@skillupjapan.co.jp, n.bersano@skillupchile.cl
@@ -86,6 +86,7 @@ files:
86
86
  - lib/suj/pusher/gcm_connection.rb
87
87
  - lib/suj/pusher/logger.rb
88
88
  - lib/suj/pusher/monkey/hash.rb
89
+ - lib/suj/pusher/monkey/vash.rb
89
90
  - lib/suj/pusher/version.rb
90
91
  - lib/suj/pusher/wns_connection.rb
91
92
  - lib/suj/pusher/wns_notification.rb