# frozen_string_literal: true require 'json' require 'tacview_client' require 'set' require_relative 'datastore' require_relative 'cache' require_relative 'event_queue' require_relative 'event_processor' module TacScribe # Main entry-point into Tac Scribe. Instantiate this class to start # processing events. class Daemon def initialize(db_host:, db_port:, db_name:, db_user:, db_password:, tacview_host:, tacview_port:, tacview_password:, tacview_client_name:, verbose_logging:, thread_count:, populate_airfields:, whitelist: nil) Datastore.instance.configure do |config| config.host = db_host config.port = db_port config.database = db_name config.username = db_user config.password = db_password end Datastore.instance.connect @event_queue = EventQueue.new @verbose_logging = verbose_logging @populate_airfields = populate_airfields @thread_count = thread_count @threads = {} @whitelist = Set.new(IO.read(whitelist).split) if whitelist @client = TacviewClient::Client.new( host: tacview_host, port: tacview_port, password: tacview_password, processor: @event_queue, client_name: tacview_client_name ) end # Starts processing and reconnects if the client was disconnected. # Because connecting to Tacview always gives an initial unit dump # we truncate the table each time we reconnect. This will make sure # there are no ghost units hanging around after server restart # for example def start_processing loop do puts 'Starting processing loop' @event_queue.clear Datastore.instance.truncate_table Cache.instance.clear start_processing_threads start_db_sync_thread start_reporting_thread populate_airfields if @populate_airfields @threads.each_pair do |key, _value| puts "#{key} thread started" end @client.connect # If this code is executed it means we have been disconnected without # exceptions. We will still sleep to stop a retry storm. Just not as # long. puts "Disconnected from Tacview" kill_threads sleep 10 # Rescuing reliably from Net::HTTP is a complete bear so rescue # StandardError. It ain't pretty but it is reliable. We will puts # the exception just in case # https://stackoverflow.com/questions/5370697/what-s-the-best-way-to-handle-exceptions-from-nethttp rescue StandardError => e puts 'Exception in processing loop' puts e.message puts e.backtrace kill_threads sleep 30 next end end def kill_threads puts 'Killing Threads' @threads.each_pair do |key, thread| puts "Killing #{key} thread" thread.kill thread.join end end def start_processing_threads @event_processor = EventProcessor.new(cache: Cache.instance, datastore: Datastore.instance, event_queue: @event_queue, whitelist: @whitelist) event_processor_thread = Thread.new do @event_processor.start end event_processor_thread.name = 'Event Processor' @threads[:processing] = event_processor_thread end def start_db_sync_thread db_write_thread = Thread.new do loop do sleep 1 deleted = Datastore.instance.write_objects(Cache.instance.data.values) deleted.each { |id| Cache.instance.data.delete(id) } rescue StandardError next end end db_write_thread.name = 'Database Writing' @threads[:database] = db_write_thread end def start_reporting_thread return unless @verbose_logging reporting_thread = Thread.new do sleep 5 loop do puts "#{Time.now.strftime('%FT%T')}\t" \ "Events Incoming: #{@event_queue.events_written}\t" \ "Processed: #{@event_processor.events_processed}\t" \ "Ignored: #{@event_processor.events_ignored}\t" \ "Queue Size: #{@event_queue.size}\t" \ "Objects Written: #{Datastore.instance.written}\t" \ "Deleted: #{Datastore.instance.deleted}" @event_queue.events_written = 0 @event_processor.events_processed = 0 @event_processor.events_ignored = 0 Datastore.instance.written = 0 Datastore.instance.deleted = 0 sleep 1 end end reporting_thread.name = 'Reporting' @threads[:reporting] = reporting_thread end def populate_airfields json = File.read(File.join(File.dirname(__FILE__), '../../data/airfields.json')) airfields = JSON.parse(json) airfields.each_with_index do |airfield, i| @event_queue.update_object( object_id: (45_000_000 + i).to_s, latitude: BigDecimal(airfield['lat'].to_s), longitude: BigDecimal(airfield['lon'].to_s), altitude: BigDecimal(airfield['alt'].to_s), type: 'Ground+Static+Aerodrome', name: airfield['name'], coalition: 0 # Neutral ) end end end end