acpc_table_manager 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,54 @@
1
+ require 'awesome_print'
2
+ require 'logger'
3
+ require 'fileutils'
4
+
5
+ # @todo Move to its own gem the next time I find I need easier logging faculties
6
+
7
+ class Logger
8
+ # Defaults correspond to Logger#new defaults
9
+ def self.from_file_name(file_name, shift_age = 0, shift_size = 1048576)
10
+ unless File.exists?(file_name)
11
+ FileUtils.mkdir_p File.dirname(file_name)
12
+ FileUtils.touch file_name
13
+ end
14
+
15
+ logger = new(file_name, shift_age, shift_size)
16
+ end
17
+
18
+ def path
19
+ @logdev.filename
20
+ end
21
+ end
22
+
23
+ module SimpleLogging
24
+ module MessageFormatting
25
+ refine Logger do
26
+ def sanitize_all_messages!
27
+ original_formatter = Logger::Formatter.new
28
+ @formatter = proc { |severity, datetime, progname, msg|
29
+ original_formatter.call(severity, datetime, progname, msg.dump)
30
+ }
31
+ self
32
+ end
33
+ def with_metadata!
34
+ original_formatter = Logger::Formatter.new
35
+ @formatter = proc { |severity, datetime, progname, msg|
36
+ original_formatter.call(severity, datetime, progname, msg)
37
+ }
38
+ self
39
+ end
40
+ end
41
+ end
42
+
43
+ def logger(stream = STDOUT)
44
+ @logger ||= Logger.new(stream)
45
+ end
46
+ def log_with(logger_, method, variables = nil, msg_type = Logger::Severity::INFO)
47
+ msg = "#{self.class}: #{method}"
48
+ msg << ": #{variables.awesome_inspect}" if variables
49
+ logger_.log(msg_type, msg)
50
+ end
51
+ def log(method, variables = nil, msg_type = Logger::Severity::INFO)
52
+ log_with(logger, method, variables, msg_type)
53
+ end
54
+ end
@@ -0,0 +1,260 @@
1
+ require_relative 'dealer'
2
+ require_relative 'match'
3
+
4
+ require_relative 'simple_logging'
5
+ using SimpleLogging::MessageFormatting
6
+
7
+ module AcpcTableManager
8
+ class Null
9
+ def method_missing(*args, &block) self end
10
+ end
11
+ module HandleException
12
+ protected
13
+
14
+ # @param [String] match_id The ID of the match in which the exception occurred.
15
+ # @param [Exception] e The exception to log.
16
+ def handle_exception(match_id, e)
17
+ log(
18
+ __method__,
19
+ {
20
+ match_id: match_id,
21
+ message: e.message,
22
+ backtrace: e.backtrace
23
+ },
24
+ Logger::Severity::ERROR
25
+ )
26
+ end
27
+ end
28
+
29
+ class Maintainer
30
+ include ParamRetrieval
31
+ include SimpleLogging
32
+ include HandleException
33
+
34
+ def initialize(logger_)
35
+ @logger = logger_
36
+
37
+ @table_queues = {}
38
+ enqueue_waiting_matches
39
+
40
+ log(__method__)
41
+ end
42
+
43
+ def enqueue_waiting_matches(game_definition_key=nil)
44
+ if game_definition_key
45
+ @table_queues[game_definition_key] ||= ::AcpcTableManager::TableQueue.new(game_definition_key)
46
+ @table_queues[game_definition_key].my_matches.not_running.and.not_started.each do |m|
47
+ @table_queues[game_definition_key].enqueue! m.id.to_s, m.dealer_options
48
+ end
49
+ else
50
+ ::AcpcTableManager.exhibition_config.games.keys.each do |game_definition_key|
51
+ enqueue_waiting_matches game_definition_key
52
+ end
53
+ end
54
+ end
55
+
56
+ def maintain!
57
+ log __method__, msg: "Starting maintenance"
58
+
59
+ begin
60
+ enqueue_waiting_matches
61
+ @table_queues.each { |key, queue| queue.check_queue! }
62
+ clean_up_matches!
63
+ rescue => e
64
+ handle_exception nil, e
65
+ Rusen.notify e # Send an email notification
66
+ end
67
+ log __method__, msg: "Finished maintenance"
68
+ end
69
+
70
+ def kill_match!(match_id)
71
+ log(__method__, match_id: match_id)
72
+
73
+ @table_queues.each do |key, queue|
74
+ queue.kill_match!(match_id)
75
+ end
76
+ end
77
+
78
+ def clean_up_matches!
79
+ ::AcpcTableManager::Match.delete_matches_older_than! 1.day
80
+ end
81
+
82
+ def enqueue_match!(match_id, options)
83
+ begin
84
+ m = ::AcpcTableManager::Match.find match_id
85
+ rescue Mongoid::Errors::DocumentNotFound
86
+ return kill_match!(match_id)
87
+ else
88
+ @table_queues[m.game_definition_key.to_s].enqueue! match_id, options
89
+ end
90
+ end
91
+
92
+ def start_proxy!(match_id)
93
+ begin
94
+ match = ::AcpcTableManager::Match.find match_id
95
+ rescue Mongoid::Errors::DocumentNotFound
96
+ return kill_match!(match_id)
97
+ else
98
+ @table_queues[match.game_definition_key.to_s].start_proxy match
99
+ end
100
+ end
101
+
102
+ def play_action!(match_id, action)
103
+ log __method__, {
104
+ match_id: match_id,
105
+ action: action
106
+ }
107
+ begin
108
+ match = ::AcpcTableManager::Match.find match_id
109
+ rescue Mongoid::Errors::DocumentNotFound
110
+ log(
111
+ __method__,
112
+ {
113
+ msg: "Request to play in match #{match_id} when no such proxy exists! Killed match.",
114
+ match_id: match_id,
115
+ action: action
116
+ },
117
+ Logger::Severity::ERROR
118
+ )
119
+ return kill_match!(match_id)
120
+ end
121
+ unless @table_queues[match.game_definition_key.to_s].running_matches[match_id]
122
+ log(
123
+ __method__,
124
+ {
125
+ msg: "Request to play in match #{match_id} in seat #{match.seat} when no such proxy exists! Killed match.",
126
+ match_id: match_id,
127
+ match_name: match.name,
128
+ last_updated_at: match.updated_at,
129
+ running?: match.running?,
130
+ last_slice_viewed: match.last_slice_viewed,
131
+ last_slice_present: match.slices.length - 1,
132
+ action: action
133
+ },
134
+ Logger::Severity::ERROR
135
+ )
136
+ return kill_match!(match_id)
137
+ end
138
+ log __method__, {
139
+ match_id: match_id,
140
+ action: action,
141
+ running?: !@table_queues[match.game_definition_key.to_s].running_matches[match_id].nil?
142
+ }
143
+ proxy = @table_queues[match.game_definition_key.to_s].running_matches[match_id][:proxy]
144
+ if proxy
145
+ proxy.play! action
146
+ else
147
+ log(
148
+ __method__,
149
+ {
150
+ msg: "Request to play in match #{match_id} in seat #{match.seat} when no such proxy exists! Killed match.",
151
+ match_id: match_id,
152
+ match_name: match.name,
153
+ last_updated_at: match.updated_at,
154
+ running?: match.running?,
155
+ last_slice_viewed: match.last_slice_viewed,
156
+ last_slice_present: match.slices.length - 1,
157
+ action: action
158
+ },
159
+ Logger::Severity::ERROR
160
+ )
161
+ end
162
+ kill_match!(match_id) if proxy.nil? || proxy.match_ended?
163
+ end
164
+ end
165
+
166
+ class TableManager
167
+ include ParamRetrieval
168
+ include SimpleLogging
169
+ include HandleException
170
+
171
+ attr_accessor :maintainer
172
+
173
+ def initialize
174
+ @logger = AcpcTableManager.new_log 'table_manager.log'
175
+ log __method__, "Starting new #{self.class()}"
176
+ @maintainer = Maintainer.new @logger
177
+ end
178
+
179
+ def maintain!
180
+ begin
181
+ @maintainer.maintain!
182
+ rescue => e
183
+ log(
184
+ __method__,
185
+ {
186
+ message: e.message,
187
+ backtrace: e.backtrace
188
+ },
189
+ Logger::Severity::ERROR
190
+ )
191
+ Rusen.notify e # Send an email notification
192
+ end
193
+ end
194
+
195
+ def perform!(request, params=nil)
196
+ match_id = nil
197
+ begin
198
+ log(__method__, {request: request, params: params})
199
+
200
+ case request
201
+ # when START_MATCH_REQUEST_CODE
202
+ # @todo Put bots in erb yaml and have them reread here
203
+ when ::AcpcTableManager.config.delete_irrelevant_matches_request_code
204
+ return @maintainer.clean_up_matches!
205
+ end
206
+
207
+ match_id = retrieve_match_id_or_raise_exception params
208
+
209
+ log(__method__, {request: request, match_id: match_id})
210
+
211
+ do_request!(request, match_id, params)
212
+ rescue => e
213
+ handle_exception match_id, e
214
+ Rusen.notify e # Send an email notification
215
+ end
216
+ end
217
+
218
+ protected
219
+
220
+ def do_request!(request, match_id, params)
221
+ case request
222
+ when ::AcpcTableManager.config.start_match_request_code
223
+ log(__method__, {request: request, match_id: match_id, msg: 'Enqueueing match'})
224
+
225
+ @maintainer.enqueue_match!(
226
+ match_id,
227
+ retrieve_parameter_or_raise_exception(params, ::AcpcTableManager.config.options_key)
228
+ )
229
+ when ::AcpcTableManager.config.start_proxy_request_code
230
+ log(
231
+ __method__,
232
+ request: request,
233
+ match_id: match_id,
234
+ msg: 'Starting proxy'
235
+ )
236
+
237
+ @maintainer.start_proxy! match_id
238
+ when ::AcpcTableManager.config.play_action_request_code
239
+ log(
240
+ __method__,
241
+ request: request,
242
+ match_id: match_id,
243
+ msg: 'Taking action'
244
+ )
245
+
246
+ @maintainer.play_action! match_id, retrieve_parameter_or_raise_exception(params, ::AcpcTableManager.config.action_key)
247
+ when ::AcpcTableManager.config.kill_match
248
+ log(
249
+ __method__,
250
+ request: request,
251
+ match_id: match_id,
252
+ msg: "Killing match #{match_id}"
253
+ )
254
+ @maintainer.kill_match! match_id
255
+ else
256
+ raise StandardError.new("Unrecognized request: #{request}")
257
+ end
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,379 @@
1
+ require 'acpc_poker_types'
2
+ require 'acpc_dealer'
3
+ require 'timeout'
4
+
5
+ require_relative 'proxy'
6
+
7
+ require_relative 'dealer'
8
+ require_relative 'opponents'
9
+ require_relative 'config'
10
+ require_relative 'match'
11
+
12
+ require_relative 'simple_logging'
13
+ using SimpleLogging::MessageFormatting
14
+
15
+ require 'contextual_exceptions'
16
+ using ContextualExceptions::ClassRefinement
17
+
18
+ module AcpcTableManager
19
+ class TableQueue
20
+ include SimpleLogging
21
+
22
+ attr_reader :running_matches
23
+
24
+ exceptions :no_port_for_dealer_available
25
+
26
+ def initialize(game_definition_key_)
27
+ @logger = AcpcTableManager.new_log 'queue.log'
28
+ @matches_to_start = []
29
+ @running_matches = {}
30
+ @game_definition_key = game_definition_key_
31
+
32
+ log(
33
+ __method__,
34
+ {
35
+ game_definition_key: @game_definition_key,
36
+ max_num_matches: AcpcTableManager.exhibition_config.games[@game_definition_key]['max_num_matches']
37
+ }
38
+ )
39
+
40
+ # Clean up old matches
41
+ my_matches.running_or_started.each do |m|
42
+ m.delete
43
+ end
44
+ end
45
+
46
+ def start_players!(match)
47
+ opponents = match.bots(AcpcTableManager.config.dealer_host)
48
+
49
+ if opponents.empty?
50
+ kill_match! match.id.to_s
51
+ raise StandardError.new("No opponents found to start for #{match.id.to_s}! Killed match.")
52
+ end
53
+
54
+ Opponents.start(
55
+ *opponents.map { |name, info| [info[:runner], info[:host], info[:port]] }
56
+ )
57
+ log(__method__, msg: "Opponents started for #{match.id.to_s}")
58
+
59
+ start_proxy match
60
+ self
61
+ end
62
+
63
+ def start_proxy(match)
64
+ log(__method__, msg: "Starting proxy for #{match.id.to_s}")
65
+ @running_matches[match.id.to_s][:proxy] = Proxy.start(match)
66
+ end
67
+
68
+ def my_matches
69
+ Match.where(game_definition_key: @game_definition_key.to_sym)
70
+ end
71
+
72
+ def change_in_number_of_running_matches?
73
+ prevNumMatchesRunning = @running_matches.length
74
+ yield if block_given?
75
+ prevNumMatchesRunning != @running_matches.length
76
+ end
77
+
78
+ def length
79
+ @matches_to_start.length
80
+ end
81
+
82
+ def ports_in_use
83
+ @running_matches.values.inject([]) do |ports, m|
84
+ if m[:dealer] && m[:dealer][:port_numbers]
85
+ m[:dealer][:port_numbers].each { |n| ports << n.to_i }
86
+ end
87
+ ports
88
+ end
89
+ end
90
+
91
+ def available_special_ports
92
+ if AcpcTableManager.exhibition_config.special_ports_to_dealer
93
+ AcpcTableManager.exhibition_config.special_ports_to_dealer - ports_in_use
94
+ else
95
+ []
96
+ end
97
+ end
98
+
99
+ # @return (@see #dequeue!)
100
+ def enqueue!(match_id, dealer_options)
101
+ log(
102
+ __method__,
103
+ {
104
+ match_id: match_id,
105
+ running_matches: @running_matches.map { |r| r.first },
106
+ game_definition_key: @game_definition_key,
107
+ max_num_matches: AcpcTableManager.exhibition_config.games[@game_definition_key]['max_num_matches']
108
+ }
109
+ )
110
+
111
+ if @running_matches[match_id]
112
+ return log(
113
+ __method__,
114
+ msg: "Match #{match_id} already started!"
115
+ )
116
+ end
117
+
118
+ @matches_to_start << {match_id: match_id, options: dealer_options}
119
+
120
+ check_queue!
121
+ end
122
+
123
+ # @return (@see #dequeue!)
124
+ def check_queue!
125
+ log __method__
126
+
127
+ kill_matches!
128
+
129
+ log __method__, {num_running_matches: @running_matches.length, num_matches_to_start: @matches_to_start.length}
130
+
131
+ if @running_matches.length < AcpcTableManager.exhibition_config.games[@game_definition_key]['max_num_matches']
132
+ dequeue!
133
+ else
134
+ nil
135
+ end
136
+ end
137
+
138
+ # @todo Shouldn't be necessary, so this method isn't called right now, but I've written it so I'll leave it for now
139
+ def fix_running_matches_statuses!
140
+ log __method__
141
+ my_matches.running do |m|
142
+ if !(@running_matches[m.id.to_s] && AcpcDealer::dealer_running?(@running_matches[m.id.to_s][:dealer]))
143
+ m.is_running = false
144
+ m.save
145
+ end
146
+ end
147
+ end
148
+
149
+ def kill_match!(match_id)
150
+ return unless match_id
151
+
152
+ begin
153
+ match = Match.find match_id
154
+ rescue Mongoid::Errors::DocumentNotFound
155
+ else
156
+ match.is_running = false
157
+ match.save!
158
+ end
159
+
160
+ match_info = @running_matches[match_id]
161
+ if match_info
162
+ @running_matches.delete(match_id)
163
+ end
164
+ @matches_to_start.delete_if { |m| m[:match_id] == match_id }
165
+
166
+ kill_dealer!(match_info[:dealer]) if match_info && match_info[:dealer]
167
+
168
+ log __method__, match_id: match_id, msg: 'Match successfully killed'
169
+ end
170
+
171
+ def force_kill_match!(match_id)
172
+ log __method__, match_id: match_id
173
+ kill_match! match_id
174
+ ::AcpcTableManager::Match.delete_match! match_id
175
+ log __method__, match_id: match_id, msg: 'Match successfully deleted'
176
+ end
177
+
178
+ protected
179
+
180
+ def kill_dealer!(dealer_info)
181
+ log(
182
+ __method__,
183
+ pid: dealer_info[:pid],
184
+ was_running?: true,
185
+ dealer_running?: AcpcDealer::dealer_running?(dealer_info)
186
+ )
187
+
188
+ if AcpcDealer::dealer_running? dealer_info
189
+ AcpcDealer.kill_process dealer_info[:pid]
190
+
191
+ sleep 1 # Give the dealer a chance to exit
192
+
193
+ log(
194
+ __method__,
195
+ pid: dealer_info[:pid],
196
+ msg: 'After TERM signal',
197
+ dealer_still_running?: AcpcDealer::dealer_running?(dealer_info)
198
+ )
199
+
200
+ if AcpcDealer::dealer_running?(dealer_info)
201
+ AcpcDealer.force_kill_process dealer_info[:pid]
202
+ sleep 1
203
+
204
+ log(
205
+ __method__,
206
+ pid: dealer_info[:pid],
207
+ msg: 'After KILL signal',
208
+ dealer_still_running?: AcpcDealer::dealer_running?(dealer_info)
209
+ )
210
+
211
+ if AcpcDealer::dealer_running?(dealer_info)
212
+ raise(
213
+ StandardError.new(
214
+ "Dealer process #{dealer_info[:pid]} couldn't be killed!"
215
+ )
216
+ )
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ def kill_matches!
223
+ log __method__
224
+ running_matches_array = @running_matches.to_a
225
+ running_matches_array.each_index do |i|
226
+ match_id, match_info = running_matches_array[i]
227
+
228
+ unless (AcpcDealer::dealer_running?(match_info[:dealer]) && Match.id_exists?(match_id))
229
+ log(
230
+ __method__,
231
+ {
232
+ match_id_being_killed: match_id
233
+ }
234
+ )
235
+
236
+ kill_match! match_id
237
+ end
238
+ end
239
+ @matches_to_start.delete_if do |m|
240
+ !Match.id_exists?(m[:match_id])
241
+ end
242
+ end
243
+
244
+ def match_queued?(match_id)
245
+ @matches_to_start.any? { |m| m[:match_id] == match_id }
246
+ end
247
+
248
+ def port(available_ports_)
249
+ port_ = available_ports_.pop
250
+ while !AcpcDealer::port_available?(port_)
251
+ if available_ports_.empty?
252
+ raise NoPortForDealerAvailable.new("None of the special ports (#{available_special_ports}) are open")
253
+ end
254
+ port_ = available_ports_.pop
255
+ end
256
+ unless port_
257
+ raise NoPortForDealerAvailable.new("None of the special ports (#{available_special_ports}) are open")
258
+ end
259
+ port_
260
+ end
261
+
262
+ # @return [Object] The match that has been started or +nil+ if none could
263
+ # be started.
264
+ def dequeue!
265
+ log(
266
+ __method__,
267
+ num_matches_to_start: @matches_to_start.length
268
+ )
269
+ return nil if @matches_to_start.empty?
270
+
271
+ match_info = nil
272
+ match_id = nil
273
+ match = nil
274
+ loop do
275
+ match_info = @matches_to_start.shift
276
+ match_id = match_info[:match_id]
277
+ begin
278
+ match = Match.find match_id
279
+ rescue Mongoid::Errors::DocumentNotFound
280
+ return self if @matches_to_start.empty?
281
+ else
282
+ break
283
+ end
284
+ end
285
+ return self unless match_id
286
+
287
+ options = match_info[:options]
288
+
289
+ log(
290
+ __method__,
291
+ msg: "Starting dealer for match #{match_id}",
292
+ options: options
293
+ )
294
+
295
+ @running_matches[match_id] ||= {}
296
+
297
+ special_port_requirements = match.bot_special_port_requirements
298
+
299
+ # Add user's port
300
+ special_port_requirements.insert(match.seat - 1, false)
301
+
302
+ available_ports_ = available_special_ports
303
+ ports_to_be_used = special_port_requirements.map do |r|
304
+ if r then port(available_ports_) else 0 end
305
+ end
306
+
307
+ match.is_running = true
308
+ match.save!
309
+
310
+ num_repetitions = 0
311
+ while @running_matches[match_id][:dealer].nil? do
312
+ log(
313
+ __method__,
314
+ msg: "Added #{match_id} list of running matches",
315
+ available_special_ports: available_ports_,
316
+ special_port_requirements: special_port_requirements,
317
+ :'ports_to_be_used_(zero_for_random)' => ports_to_be_used
318
+ )
319
+ begin
320
+ @running_matches[match_id][:dealer] = Dealer.start(
321
+ options,
322
+ match,
323
+ port_numbers: ports_to_be_used
324
+ )
325
+ rescue Timeout::Error => e
326
+ log(
327
+ __method__,
328
+ {warning: "The dealer for match #{match_id} timed out."},
329
+ Logger::Severity::WARN
330
+ )
331
+ begin
332
+ ports_to_be_used = special_port_requirements.map do |r|
333
+ if r then port(available_ports_) else 0 end
334
+ end
335
+ rescue NoPortForDealerAvailable => e
336
+ available_ports_ = available_special_ports
337
+ log(
338
+ __method__,
339
+ {warning: "#{ports_to_be_used} ports unavailable, retrying with all special ports, #{available_ports_}."},
340
+ Logger::Severity::WARN
341
+ )
342
+ end
343
+ if num_repetitions < 1
344
+ sleep 1
345
+ log(
346
+ __method__,
347
+ {warning: "Retrying with all special ports, #{available_ports_}."},
348
+ Logger::Severity::WARN
349
+ )
350
+ num_repetitions += 1
351
+ else
352
+ log(
353
+ __method__,
354
+ {warning: "Unable to start match after retry, force killing match."},
355
+ Logger::Severity::ERROR
356
+ )
357
+ force_kill_match! match_id
358
+ raise e
359
+ end
360
+ end
361
+ end
362
+
363
+ begin
364
+ match = Match.find match_id
365
+ rescue Mongoid::Errors::DocumentNotFound => e
366
+ kill_match! match_id
367
+ raise e
368
+ end
369
+
370
+ log(
371
+ __method__,
372
+ msg: "Dealer started for #{match_id} with pid #{@running_matches[match_id][:dealer][:pid]}",
373
+ ports: match.port_numbers
374
+ )
375
+
376
+ start_players! match
377
+ end
378
+ end
379
+ end