rocket_sms 0.1.1

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.
Files changed (76) hide show
  1. data/.gitignore +19 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +29 -0
  6. data/Rakefile +1 -0
  7. data/bin/scheduler_runner.rb +11 -0
  8. data/bin/transceiver_runner.rb +10 -0
  9. data/examples/gateway.rb +7 -0
  10. data/examples/gateway.yml +40 -0
  11. data/examples/test.rb +60 -0
  12. data/lib/rocket_sms.rb +111 -0
  13. data/lib/rocket_sms/did.rb +25 -0
  14. data/lib/rocket_sms/gateway.rb +123 -0
  15. data/lib/rocket_sms/lock.rb +0 -0
  16. data/lib/rocket_sms/message.rb +30 -0
  17. data/lib/rocket_sms/scheduler.rb +212 -0
  18. data/lib/rocket_sms/transceiver.rb +260 -0
  19. data/lib/rocket_sms/version.rb +3 -0
  20. data/rocket_sms.gemspec +34 -0
  21. data/spec/spec_helper.rb +17 -0
  22. data/vendor/ruby-smpp/CHANGELOG +52 -0
  23. data/vendor/ruby-smpp/CONTRIBUTORS.txt +11 -0
  24. data/vendor/ruby-smpp/Gemfile +8 -0
  25. data/vendor/ruby-smpp/LICENSE +20 -0
  26. data/vendor/ruby-smpp/README.rdoc +89 -0
  27. data/vendor/ruby-smpp/Rakefile +53 -0
  28. data/vendor/ruby-smpp/VERSION +1 -0
  29. data/vendor/ruby-smpp/config/environment.rb +2 -0
  30. data/vendor/ruby-smpp/examples/PDU1.example +26 -0
  31. data/vendor/ruby-smpp/examples/PDU2.example +26 -0
  32. data/vendor/ruby-smpp/examples/sample_gateway.rb +137 -0
  33. data/vendor/ruby-smpp/examples/sample_smsc.rb +102 -0
  34. data/vendor/ruby-smpp/lib/smpp.rb +25 -0
  35. data/vendor/ruby-smpp/lib/smpp/base.rb +308 -0
  36. data/vendor/ruby-smpp/lib/smpp/encoding/utf8_encoder.rb +37 -0
  37. data/vendor/ruby-smpp/lib/smpp/optional_parameter.rb +35 -0
  38. data/vendor/ruby-smpp/lib/smpp/pdu/base.rb +183 -0
  39. data/vendor/ruby-smpp/lib/smpp/pdu/bind_base.rb +25 -0
  40. data/vendor/ruby-smpp/lib/smpp/pdu/bind_receiver.rb +4 -0
  41. data/vendor/ruby-smpp/lib/smpp/pdu/bind_receiver_response.rb +4 -0
  42. data/vendor/ruby-smpp/lib/smpp/pdu/bind_resp_base.rb +17 -0
  43. data/vendor/ruby-smpp/lib/smpp/pdu/bind_transceiver.rb +4 -0
  44. data/vendor/ruby-smpp/lib/smpp/pdu/bind_transceiver_response.rb +4 -0
  45. data/vendor/ruby-smpp/lib/smpp/pdu/deliver_sm.rb +142 -0
  46. data/vendor/ruby-smpp/lib/smpp/pdu/deliver_sm_response.rb +12 -0
  47. data/vendor/ruby-smpp/lib/smpp/pdu/enquire_link.rb +11 -0
  48. data/vendor/ruby-smpp/lib/smpp/pdu/enquire_link_response.rb +11 -0
  49. data/vendor/ruby-smpp/lib/smpp/pdu/generic_nack.rb +20 -0
  50. data/vendor/ruby-smpp/lib/smpp/pdu/submit_multi.rb +68 -0
  51. data/vendor/ruby-smpp/lib/smpp/pdu/submit_multi_response.rb +49 -0
  52. data/vendor/ruby-smpp/lib/smpp/pdu/submit_sm.rb +91 -0
  53. data/vendor/ruby-smpp/lib/smpp/pdu/submit_sm_response.rb +31 -0
  54. data/vendor/ruby-smpp/lib/smpp/pdu/unbind.rb +11 -0
  55. data/vendor/ruby-smpp/lib/smpp/pdu/unbind_response.rb +12 -0
  56. data/vendor/ruby-smpp/lib/smpp/receiver.rb +27 -0
  57. data/vendor/ruby-smpp/lib/smpp/server.rb +223 -0
  58. data/vendor/ruby-smpp/lib/smpp/transceiver.rb +109 -0
  59. data/vendor/ruby-smpp/lib/sms.rb +9 -0
  60. data/vendor/ruby-smpp/ruby-smpp.gemspec +96 -0
  61. data/vendor/ruby-smpp/test/delegate.rb +28 -0
  62. data/vendor/ruby-smpp/test/encoding_test.rb +232 -0
  63. data/vendor/ruby-smpp/test/optional_parameter_test.rb +30 -0
  64. data/vendor/ruby-smpp/test/pdu_parsing_test.rb +111 -0
  65. data/vendor/ruby-smpp/test/receiver_test.rb +232 -0
  66. data/vendor/ruby-smpp/test/responsive_delegate.rb +53 -0
  67. data/vendor/ruby-smpp/test/server.rb +56 -0
  68. data/vendor/ruby-smpp/test/smpp_test.rb +239 -0
  69. data/vendor/ruby-smpp/test/submit_sm_test.rb +40 -0
  70. data/vendor/ruby-smpp/test/transceiver_test.rb +35 -0
  71. data/vendor/smscsim/License.txt +61 -0
  72. data/vendor/smscsim/smpp.jar +0 -0
  73. data/vendor/smscsim/smscsim.jar +0 -0
  74. data/vendor/smscsim/start.sh +3 -0
  75. data/vendor/smscsim/users.txt +46 -0
  76. metadata +299 -0
File without changes
@@ -0,0 +1,30 @@
1
+ module RocketSMS
2
+ class Message
3
+
4
+ attr_reader :params
5
+
6
+ def initialize(params)
7
+ @params = OpenStruct.new(params)
8
+ @params.pass ||= 0
9
+ end
10
+
11
+ def to_json
12
+ MultiJson.dump(@params.marshal_dump)
13
+ end
14
+
15
+ def add_pass
16
+ @params.pass += 1
17
+ end
18
+
19
+ def self.from_json(json)
20
+ params = MultiJson.load(json, symbolize_keys: true)
21
+ msg = Message.new(params)
22
+ return msg
23
+ end
24
+
25
+ def method_missing(sym, *args, &block)
26
+ @params.send(sym, *args, &block)
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,212 @@
1
+ module RocketSMS
2
+
3
+ class Scheduler
4
+ include Singleton
5
+
6
+ attr_accessor :redis_url, :log_location
7
+
8
+ def initialize
9
+ @redis_url, @log_location = nil, nil
10
+ @active = true
11
+ @fast = false
12
+ @dids = {}
13
+ @transceivers = {}
14
+ @throughput = 0
15
+ end
16
+
17
+ def redis
18
+ @redis ||= EM::Hiredis.connect(@redis_url)
19
+ end
20
+
21
+ def logger
22
+ @logger ||= Logger.new(@log_location)
23
+ end
24
+
25
+ def log(msg, level = 'info')
26
+ if EM.reactor_running?
27
+ EM.defer{ logger.send(level, msg) }
28
+ else
29
+ logger.send(level, msg)
30
+ end
31
+ end
32
+
33
+ def queues
34
+ RocketSMS.queues
35
+ end
36
+
37
+ def start
38
+ EM.threadpool_size = 128
39
+ EM.set_max_timers(100_000)
40
+ EM.run do
41
+ log "Starting Scheduler"
42
+ configure
43
+ detect
44
+ # Trap exit-related signals
45
+ Signal.trap("INT") { |signal| stop(signal) }
46
+ Signal.trap("TERM") { |signal| stop(signal) }
47
+ end
48
+ end
49
+
50
+ def stop(signal = nil)
51
+ if @kill
52
+ log "Forcing Exit. Check your data for losses."
53
+ shutdown
54
+ else
55
+ log "Stopping. Waiting 5 seconds for pending operations to finish."
56
+ @kill = true
57
+ @active = false
58
+ EM::Timer.new(5){ shutdown }
59
+ end
60
+ end
61
+
62
+ def shutdown
63
+ log "Scheduler DOWN."
64
+ EM.stop
65
+ end
66
+
67
+ def configure
68
+ return unless @active
69
+ @configurator = EM::Timer.new(1){ configure }
70
+ redis.keys("gateway:transceivers:*") do |keys|
71
+ if keys
72
+ tids = keys.map{ |key| key.split(':')[2] }.uniq
73
+ @transceivers.keys.each{ |k| @transceivers.delete(k) unless tids.include?(k) }
74
+ tids.each do |tid|
75
+ redis.hget("gateway:transceivers:#{tid}","status") do |resp|
76
+ if resp == 'online'
77
+ redis.hget("gateway:transceivers:#{tid}", "throughput") do |payload|
78
+ if payload
79
+ throughput = payload.to_f
80
+ log "Adding Transceiver #{tid}" if !@transceivers[tid]
81
+ @transceivers[tid] = throughput
82
+ set_throughput
83
+ elsif @transceivers[tid]
84
+ log "Removing Transceiver #{tid}"
85
+ @transceivers.delete(tid)
86
+ set_throughput
87
+ end
88
+ end
89
+ else
90
+ if @transceivers[tid]
91
+ log "Removing Transceiver #{tid}"
92
+ @transceivers.delete(tid)
93
+ set_throughput
94
+ end
95
+ end
96
+ end
97
+ end
98
+ else
99
+ @transceivers = {}
100
+ @throughput = 0
101
+ end
102
+ end
103
+ end
104
+
105
+ def set_throughput
106
+ @throughput = @transceivers.keys.map{ |tid| @transceivers[tid] }.reduce(&:+).to_f
107
+ end
108
+
109
+ def detect
110
+ return unless @active
111
+ interval = @fast ? 0.001 : 1
112
+ @detector = EM::Timer.new(interval){ detect }
113
+ redis.multi
114
+ redis.zrange(queues[:mt][:pending], 0, 0, "WITHSCORES")
115
+ redis.zremrangebyrank(queues[:mt][:pending], 0, 0)
116
+ redis.exec do |response|
117
+ if response
118
+ (payload, score) = response[0]
119
+ if payload and score
120
+ @fast = true
121
+ now = Time.now.to_i
122
+ if score.to_i <= now
123
+ process_payload(payload)
124
+ else
125
+ redis.zadd(queues[:mt][:pending], score, payload)
126
+ @fast = false
127
+ end
128
+ else
129
+ @fast = false
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ def process_payload(msg_payload)
136
+ begin
137
+ message = Message.from_json(msg_payload)
138
+ if message.pass > 5
139
+ log "Message #{message.id} has exceeded maximum retries. Send it to Failed queue."
140
+ redis.lpush(queues[:mt][:failure], message.to_json)
141
+ else
142
+ did_number = message.sender
143
+ redis.get("gateway:dids:#{did_number}") do |payload|
144
+ if payload
145
+ log "Scheduling Message #{message.id} to be sent through DID #{did_number}"
146
+ schedule(message, payload)
147
+ else
148
+ log "The DID #{did_number} for message #{message.id} is not configured. Retrying."
149
+ retry_message(message)
150
+ end
151
+ end
152
+ end
153
+ rescue
154
+ log 'Invalid Message.'
155
+ redis.lpush(queues[:mt][:failure])
156
+ end
157
+ end
158
+
159
+ def schedule(message, did_payload)
160
+ if @active
161
+ if @transceivers.keys.empty? or @throughput == 0
162
+ retry_message(message)
163
+ else
164
+ did = Did.from_json(did_payload)
165
+ if !@dids[did.number]
166
+ @dids[did.number] = {}
167
+ @dids[did.number][:last_send] = Time.now.to_f + 1
168
+ end
169
+ interval = ((did.throughput.to_f)**-1)*1.1
170
+ last_send = @dids[did.number][:last_send]
171
+ if Time.now.to_f - last_send > interval
172
+ base_time = Time.now.to_f + 1
173
+ else
174
+ base_time = last_send
175
+ end
176
+ message.send_at = base_time + interval
177
+ message.expires_at = message.send_at + 50*interval
178
+ @dids[did.number][:last_send] = message.send_at
179
+ score = (message.send_at * 1000).to_i
180
+ transceiver_id = pick_transceiver
181
+ redis.zadd("gateway:transceivers:#{transceiver_id}:dispatch", score, message.to_json)
182
+ end
183
+ else
184
+ score = message.send_at.to_i + 15
185
+ redis.zadd(queues[:mt][:pending], score, message.to_json)
186
+ end
187
+ end
188
+
189
+ def pick_transceiver
190
+ tids = []
191
+ @transceivers.each do |k,v|
192
+ weight = (v.to_f/@throughput*100).to_i
193
+ weight.times{ tids << k }
194
+ end
195
+ tids.flatten!
196
+ tids.sample
197
+ end
198
+
199
+ def retry_message(message)
200
+ if message.pass > 5
201
+ log "Message #{message.id} has exceeded maximum retries. Send it to Failed queue."
202
+ redis.lpush(queues[:mt][:failure], message.to_json)
203
+ else
204
+ message.add_pass
205
+ score = Time.now.to_i + 15
206
+ redis.zadd(queues[:mt][:pending], score, message.to_json)
207
+ end
208
+ end
209
+
210
+ end
211
+
212
+ end
@@ -0,0 +1,260 @@
1
+ module RocketSMS
2
+
3
+ class Transceiver
4
+
5
+ def initialize(id, redis_url, log_location)
6
+ @id, @redis_url, @log_location = id, redis_url, log_location
7
+ @active = true
8
+ @online = false
9
+ @fast = false
10
+ @settings = {}
11
+ @mts = {}
12
+ end
13
+
14
+ def redis
15
+ @redis ||= EM::Hiredis.connect(@redis_url)
16
+ end
17
+
18
+ def dredis
19
+ @dredis ||= EM::Hiredis.connect(@redis_url)
20
+ end
21
+
22
+ def logger
23
+ @logger ||= Logger.new(@log_location)
24
+ end
25
+
26
+ def log(msg, level = 'info')
27
+ if EM.reactor_running?
28
+ EM.defer{ logger.send(level, msg) }
29
+ else
30
+ logger.send(level, msg)
31
+ end
32
+ end
33
+
34
+ def queues
35
+ RocketSMS.queues
36
+ end
37
+
38
+ def throughput
39
+ @settings && @settings[:throughput] ||= 1.0
40
+ end
41
+
42
+ def start
43
+ EM.threadpool_size = 128
44
+ EM.set_max_timers(100_000)
45
+ EM.run do
46
+ log "Starting Transceiver #{@id}"
47
+ # Set quantum to 10 milliseconds to support throughputs up to 100 MTs/sec
48
+ EM.set_quantum(10)
49
+ # Detect transceiver configuration from Redis.
50
+ configure
51
+ # Connect
52
+ connect
53
+ # Start detecting and dispatching MTs
54
+ dispatch
55
+ # Trap exit-related signals
56
+ Signal.trap("INT") { |signal| stop(signal) }
57
+ Signal.trap("TERM") { |signal| stop(signal) }
58
+ end
59
+ end
60
+
61
+ def stop(signal = nil)
62
+ if @kill
63
+ log "#{@id} - Forcing Exit. Check your data for losses."
64
+ shutdown
65
+ else
66
+ log "#{@id} - Stopping. Waiting for pending operations to finish."
67
+ @kill = true
68
+ @active = false
69
+ @connection.close_connection_after_writing if @connection
70
+ @dispatcher.cancel if @dispatcher
71
+ @configurator.cancel if @configurator
72
+ @reconnector.cancel if @reconnector
73
+ redis.del("gateways:transceivers:#{@id}")
74
+ cleanup
75
+ end
76
+ end
77
+
78
+ def shutdown
79
+ log "Transceiver #{@id} DOWN."
80
+ EM.stop
81
+ end
82
+
83
+ def cleanup
84
+ redis.zrangebyscore("gateway:transceivers:#{@id}:dispatch", '-inf', '+inf') do |payloads|
85
+ if payloads and !payloads.empty?
86
+ op = Proc.new do |payload, iter|
87
+ message = Message.from_json(payload)
88
+ message.send_at, message.expires_at = nil, nil
89
+ score = (Time.now.to_f*1000).to_i
90
+ redis.multi
91
+ redis.zrem("gateway:transceivers:#{@id}:dispatch", payload)
92
+ redis.zadd(queues[:mt][:pending], score, payload)
93
+ redis.exec do |resp|
94
+ iter.next
95
+ end
96
+ end
97
+ cb = Proc.new do |responses|
98
+ EM::Timer.new(3){ shutdown }
99
+ end
100
+ EM::Iterator.new(payloads).each(op,cb)
101
+ else
102
+ EM::Timer.new(3){ shutdown }
103
+ end
104
+ end
105
+ end
106
+
107
+ def configure
108
+ return unless @active
109
+ @configurator = EM::Timer.new(1){ configure }
110
+ redis.multi
111
+ redis.hget("gateway:transceivers:#{@id}", "throughput")
112
+ redis.hget("gateway:transceivers:#{@id}", "connection")
113
+ redis.exec do |response|
114
+ if response and !response.flatten.include?(nil)
115
+ throughput_payload = response[0]
116
+ connection_payload = response[1]
117
+ @settings[:throughput] = throughput_payload.to_f
118
+ @settings[:connection] = MultiJson.load(connection_payload, symbolize_keys: true)
119
+ else
120
+ stop
121
+ end
122
+ end
123
+ end
124
+
125
+ def connect
126
+ return unless @active
127
+ if @settings and @settings[:connection]
128
+ log "Connecting transceiver #{@id}."
129
+ @connection = EM.connect(
130
+ @settings[:connection][:host],
131
+ @settings[:connection][:port],
132
+ Smpp::Transceiver,
133
+ @settings[:connection],
134
+ self
135
+ )
136
+ else
137
+ EM::Timer.new(1){ connect }
138
+ end
139
+ end
140
+
141
+ def dispatch
142
+ return unless @active
143
+ interval = @fast ? ((throughput.to_f)**-1)*1.1 : 0.5
144
+ @dispatcher = EM::Timer.new(interval){ dispatch }
145
+ redis.multi
146
+ redis.zrange("gateway:transceivers:#{@id}:dispatch", 0, 0)
147
+ redis.zremrangebyrank("gateway:transceivers:#{@id}:dispatch", 0, 0)
148
+ redis.exec do |response|
149
+ if response
150
+ payload = response[0][0]
151
+ if payload
152
+ @fast = true
153
+ now = Time.now.to_f
154
+ message = Message.from_json(payload)
155
+ if message.send_at > now
156
+ score = (message.send_at * 1000).to_i
157
+ redis.zadd("gateway:transceivers:#{@id}:dispatch", score , payload)
158
+ elsif message.send_at <= now and now < message.expires_at
159
+ log "Message #{message.id} detected on #{@id}. Sending."
160
+ send_message(message)
161
+ elsif message.expires_at <= now
162
+ log "Message #{message.id} detected on #{@id} but has expired. Retrying."
163
+ message.add_pass
164
+ score = (( Time.now.to_f + 15 ) * 1000).to_i
165
+ redis.zadd(queues[:mt][:pending], score, message.to_json)
166
+ @dispatcher.cancel if @dispatcher
167
+ EM.next_tick{ dispatch }
168
+ end
169
+ else
170
+ @fast = false
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ def register
177
+ stat = @online ? 'online' : 'offline'
178
+ redis.hset("gateway:transceivers:#{@id}", 'status', stat)
179
+ end
180
+
181
+ def send_message(message)
182
+ begin
183
+ if @online
184
+ log "Sending Message #{message.id} through DID #{message.sender} via #{@id}."
185
+ @mts[message.id.to_s] = message
186
+ @connection.send_mt(message.id,message.sender,message.receiver,message.body)
187
+ else
188
+ log "#{@id} is not connected. Pushing message #{message.id} to dispatch queue."
189
+ score = (message.send_at * 1000).to_i
190
+ redis.zadd("gateway:transceivers:#{@id}:dispatch", score , payload)
191
+ end
192
+ rescue Exception
193
+ log "### Error Sending MT #{message.id} with DID #{message.sender} through Transceiver #{@id}. Retrying message."
194
+ message.add_pass
195
+ score = Time.now.to_i + 15
196
+ redis.zadd(queues[:mt][:pending], score, message.to_json)
197
+ end
198
+ end
199
+
200
+ def mo_received(transceiver, pdu)
201
+ log "#{@id} - Message Received"
202
+ ticket = { pdu: { source_addr: pdu.source_addr, short_message: pdu.short_message, destination_addr: pdu.destination_addr } }
203
+ EM.next_tick { redis.lpush(queues[:mo],MultiJson.dump(ticket)) }
204
+ end
205
+
206
+ def delivery_report_received(transceiver, pdu)
207
+ log "#{@id} - DR Received"
208
+ ticket = { pdu: { source_addr: pdu.source_addr, short_message: pdu.short_message, destination_addr: pdu.destination_addr } }
209
+ EM.next_tick { redis.lpush(queues[:dr],MultiJson.dump(ticket)) }
210
+ end
211
+
212
+ def message_accepted(transceiver, mt_message_id, pdu)
213
+ message = @mts.delete(mt_message_id.to_s)
214
+ if message
215
+ log "#{@id} - Message #{message.id} - Accepted"
216
+ message.accepted_at = Time.now.to_i
217
+ EM.next_tick { redis.lpush(queues[:mt][:success],message.to_json) }
218
+ else
219
+ log "#{@id} - Untracked MT Accepted #{mt_message_id}"
220
+ end
221
+ end
222
+
223
+ def message_rejected(transceiver, mt_message_id, pdu)
224
+ message = @mts.delete(mt_message_id.to_s)
225
+ if message
226
+ log "#{@id} - Message #{message.id} - Rejected"
227
+ message.add_pass
228
+ message.rejected_at = Time.now.to_i
229
+ if message.pass <= 5
230
+ score = Time.now.to_i + 10
231
+ EM.next_tick{ redis.zadd(queues[:mt][:pending], score, message.to_json) }
232
+ else
233
+ EM.next_tick { redis.lpush(queues[:mt][:failure],message.to_json) }
234
+ end
235
+ else
236
+ log "#{@id} - Untracked MT Rejected #{mt_message_id}"
237
+ end
238
+ end
239
+
240
+ def bound(transceiver)
241
+ log "#{@id} - Transceiver Bound"
242
+ @online = true
243
+ @reconnector = nil
244
+ register
245
+ end
246
+
247
+ def unbound(transceiver)
248
+ log "#{@id} - Transceiver Unbound"
249
+ if @active
250
+ log "#{@id} is not connected. Retrying in 10 seconds."
251
+ @reconnector.cancel if @reconnector
252
+ @reconnector = EM::Timer.new(10){ connect }
253
+ end
254
+ @online = false
255
+ register
256
+ end
257
+
258
+ end
259
+
260
+ end