suj-pusher 0.2.3 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +1 -1
- data/lib/suj/pusher/apn_connection.rb +49 -17
- data/lib/suj/pusher/apn_feedback_connection.rb +8 -6
- data/lib/suj/pusher/apn_notification.rb +3 -9
- data/lib/suj/pusher/connection_pool.rb +99 -23
- data/lib/suj/pusher/daemon.rb +67 -99
- data/lib/suj/pusher/monkey/vash.rb +110 -0
- data/lib/suj/pusher/version.rb +1 -1
- metadata +13 -12
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
|
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
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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(
|
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
|
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
|
-
|
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
|
58
|
+
info "APN feedback #{@cert_key}: Connection established..."
|
57
59
|
@disconnected = false
|
58
60
|
end
|
59
61
|
|
60
62
|
def unbind
|
61
|
-
info "APN
|
63
|
+
info "APN feedback #{@cert_key}: Connection closed..."
|
62
64
|
@disconnected = true
|
63
|
-
@pool.
|
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,
|
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("
|
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(
|
42
|
-
info "APN
|
43
|
-
@
|
44
|
-
@
|
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
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
data/lib/suj/pusher/daemon.rb
CHANGED
@@ -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
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
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
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
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
|
100
|
-
|
101
|
-
|
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
|
-
|
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
|
116
|
-
|
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
|
123
|
-
|
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
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
135
|
-
info "
|
136
|
-
|
137
|
-
|
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
|
data/lib/suj/pusher/version.rb
CHANGED
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.
|
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:
|
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: &
|
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: *
|
24
|
+
version_requirements: *10452580
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: em-hiredis
|
27
|
-
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: *
|
35
|
+
version_requirements: *10451520
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: multi_json
|
38
|
-
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: *
|
46
|
+
version_requirements: *10451080
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: daemon-spawn
|
49
|
-
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: *
|
57
|
+
version_requirements: *10450660
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: iobuffer
|
60
|
-
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: *
|
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
|