acpc_table_manager 0.0.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.
@@ -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