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 +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
|