tac_scribe 0.5.0 → 0.6.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 485c9e87c957fd0e66db7cc762a36283baa2c4852546008b2d4dbd5e2372a53a
4
- data.tar.gz: 95576f042c7455ac1e5b87bcab21d6a5ff555d8f67a83b4c7cd2531b56f696c1
3
+ metadata.gz: 3f357b6fb0a4a45accdc07e20ff29fe6a93c66d7b88665ea51a652ca28a2b1d4
4
+ data.tar.gz: b01e4759b8e523fee4541b71b43102fcb62d228a15b3c87bb6b969fc33e6e206
5
5
  SHA512:
6
- metadata.gz: 1aefe11c62a16ac70377bc48f262b699f2aebb4c38480bbe677b1490e5d14320c0e213d1976e0b4a477e1687e24b8bccae1b9a7f111bdaaeca1c0ebba63f9d62
7
- data.tar.gz: 2d54201ba482399aafd29a4633b3b7361220457e85e8aae0447be8920191fc708e3582617affe5176d6bacd6682688c73e315772e094a71ada60af1101b6a2f5
6
+ metadata.gz: fc97730d7e4b521a46987f2ea01d2680715933571ce33218e8ad69033a35df0b9722fcc123af082d71cd70487d7caab60765d171c5b29301445d0ec6c3178ea8
7
+ data.tar.gz: 3e27a7a3957fab068f168dad158a6ae555faab3a40fa8535c0ee65629fe8d4915255c41890ff0d0ea79e3a36b24e8642d4df4f3747a891fd90b175e4883a9a59
data/.rubocop.yml CHANGED
@@ -14,7 +14,10 @@ Metrics/BlockLength:
14
14
  # There isn't much sense breaking up the OptionParser block since splitting
15
15
  # into db and tacview option methods will just break the method length cop
16
16
  # and splitting further doesn't aid readability
17
- - exe/start_scribe
17
+ - exe/tac_scribe
18
+ # Cannot really avoid this
19
+ - tac_scribe.gemspec
20
+
18
21
  Metrics/MethodLength:
19
22
  Exclude:
20
23
  # Breaking up the initializer doesn't really do much for readability
@@ -29,3 +32,13 @@ Style/GlobalVars:
29
32
  Exclude:
30
33
  # Using a global variable makes it available to the Pry console
31
34
  - bin/console
35
+ AllCops:
36
+ Exclude:
37
+ # These files will need a MAJOR refactoring to get rid of these warnings
38
+ - lib/tac_scribe/cache.rb
39
+ - lib/tac_scribe/datastore.rb
40
+ - lib/tac_scribe/daemon.rb
41
+ ClassVars:
42
+ Exclude:
43
+ # We are fine with this and understand the risks. We are not inheriting.
44
+ - lib/tac_scribe/event_processor.rb
data/CHANGELOG.md CHANGED
@@ -4,7 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased]
7
+ ## [0.6.0]
8
+ ### Added
9
+ - Calculates and stores speed of units
10
+
11
+ ### Fixed
12
+ - Made more robust against failures and more logging messages
8
13
 
9
14
  ## [0.5.0]
10
15
  ### Changed
@@ -12,6 +12,7 @@ Sequel.migration do
12
12
  String :group, null: true
13
13
  Integer :coalition
14
14
  Integer :heading
15
+ Integer :speed
15
16
  Time :updated_at
16
17
  Boolean :deleted
17
18
  # TODO: GIST Index on the position
@@ -5,8 +5,11 @@ require 'sequel'
5
5
  require 'sequel-postgis-georuby'
6
6
  require 'singleton'
7
7
  require 'concurrent-ruby'
8
+ require 'haversine'
8
9
 
9
10
  module TacScribe
11
+ # The in-memory cache that is updated by events in real-time before the data
12
+ # is synced to the DB
10
13
  class Cache
11
14
  include Singleton
12
15
  include GeoRuby::SimpleFeatures
@@ -48,9 +51,11 @@ module TacScribe
48
51
 
49
52
  if cache_object
50
53
  object[:heading] = calculate_heading(cache_object, object)
54
+ object[:speed] = calculate_speed(cache_object, object)
51
55
  cache_object.merge!(object)
52
56
  else
53
57
  object[:heading] = -1
58
+ object[:speed] = 0
54
59
  cache_object = object
55
60
  end
56
61
 
@@ -75,7 +80,7 @@ module TacScribe
75
80
  def delete_object(id)
76
81
  @@cache[id][:deleted] = true if @@cache[id]
77
82
  end
78
-
83
+
79
84
  def clear
80
85
  @@cache.clear
81
86
  end
@@ -104,11 +109,16 @@ module TacScribe
104
109
  cache_object[:position].x
105
110
  end
106
111
 
112
+ # This "Point" class is not lat/long aware which is why we are
113
+ # flipping things up here because when it converts to SQL we need
114
+ # the long to be first since that is what postgresql expects.
107
115
  Point.from_x_y(longitude, latitude)
108
116
  end
109
117
 
110
118
  def calculate_heading(cache_object, object)
111
- return cache_object[:heading] if cache_object[:position] == object[:position]
119
+ if cache_object[:position] == object[:position]
120
+ return cache_object[:heading]
121
+ end
112
122
 
113
123
  begin
114
124
  cache_object[:position].bearing_to(object[:position]).to_i
@@ -118,5 +128,21 @@ module TacScribe
118
128
  puts 'New Position: ' + object[:position].inspect
119
129
  end
120
130
  end
131
+
132
+ def calculate_speed(cache_object, object)
133
+ time = object[:game_time] - cache_object[:game_time]
134
+
135
+ start_point = cache_object[:position]
136
+ end_point = object[:position]
137
+
138
+ # Because of the above issues with Point and Lat/Lon
139
+ # the values are reverse here :(
140
+ distance = Haversine.distance(start_point.y, start_point.x,
141
+ end_point.y, end_point.x).to_meters
142
+
143
+ speed = distance / time
144
+
145
+ speed.to_i
146
+ end
121
147
  end
122
148
  end
@@ -31,7 +31,7 @@ module TacScribe
31
31
 
32
32
  @populate_airfields = populate_airfields
33
33
  @thread_count = thread_count
34
- @threads = []
34
+ @threads = {}
35
35
  @whitelist = Set.new(IO.read(whitelist).split) if whitelist
36
36
 
37
37
  @client = TacviewClient::Client.new(
@@ -50,7 +50,7 @@ module TacScribe
50
50
  # for example
51
51
  def start_processing
52
52
  loop do
53
- kill_threads
53
+ puts 'Starting processing loop'
54
54
  @event_queue.clear
55
55
  Datastore.instance.truncate_table
56
56
  Cache.instance.clear
@@ -58,23 +58,34 @@ module TacScribe
58
58
  start_db_sync_thread
59
59
  start_reporting_thread
60
60
  populate_airfields if @populate_airfields
61
+ @threads.each_pair do |key, _value|
62
+ puts "#{key} thread started"
63
+ end
61
64
  @client.connect
65
+ # If this code is executed it means we have been disconnected without
66
+ # exceptions. We will still sleep to stop a retry storm. Just not as
67
+ # long.
68
+ sleep 10
62
69
  # Rescuing reliably from Net::HTTP is a complete bear so rescue
63
70
  # StandardError. It ain't pretty but it is reliable. We will puts
64
71
  # the exception just in case
65
72
  # https://stackoverflow.com/questions/5370697/what-s-the-best-way-to-handle-exceptions-from-nethttp
66
73
  rescue StandardError => e
74
+ puts 'Exception in processing loop'
67
75
  puts e.message
68
76
  puts e.backtrace
77
+ kill_threads
69
78
  sleep 30
70
79
  next
71
80
  end
72
81
  end
73
82
 
74
83
  def kill_threads
75
- @threads.each do |thr|
76
- thr.kill
77
- thr.join
84
+ puts 'Killing Threads'
85
+ @threads.each_pair do |key, thread|
86
+ puts "Killing #{key} thread"
87
+ thread.kill
88
+ thread.join
78
89
  end
79
90
  end
80
91
 
@@ -87,7 +98,7 @@ module TacScribe
87
98
  @event_processor.start
88
99
  end
89
100
  event_processor_thread.name = 'Event Processor'
90
- @threads << event_processor_thread
101
+ @threads[:processing] = event_processor_thread
91
102
  end
92
103
 
93
104
  def start_db_sync_thread
@@ -96,16 +107,19 @@ module TacScribe
96
107
  sleep 1
97
108
  deleted = Datastore.instance.write_objects(Cache.instance.data.values)
98
109
  deleted.each { |id| Cache.instance.data.delete(id) }
110
+ rescue StandardError
111
+ next
99
112
  end
100
113
  end
101
114
  db_write_thread.name = 'Database Writing'
102
- @threads << db_write_thread
115
+ @threads[:database] = db_write_thread
103
116
  end
104
117
 
105
118
  def start_reporting_thread
106
119
  return unless @verbose_logging
107
120
 
108
121
  reporting_thread = Thread.new do
122
+ sleep 5
109
123
  loop do
110
124
  puts "#{Time.now.strftime('%FT%T')}\t" \
111
125
  "Events Incoming: #{@event_queue.events_written}\t" \
@@ -123,11 +137,12 @@ module TacScribe
123
137
  end
124
138
  end
125
139
  reporting_thread.name = 'Reporting'
126
- @threads << reporting_thread
140
+ @threads[:reporting] = reporting_thread
127
141
  end
128
142
 
129
143
  def populate_airfields
130
- json = File.read(File.join(File.dirname(__FILE__), '../../data/airfields.json'))
144
+ json = File.read(File.join(File.dirname(__FILE__),
145
+ '../../data/airfields.json'))
131
146
  airfields = JSON.parse(json)
132
147
  airfields.each_with_index do |airfield, i|
133
148
  @event_queue.update_object(
@@ -53,11 +53,18 @@ module TacScribe
53
53
  end
54
54
 
55
55
  def write_objects(objects)
56
+ objects = objects.map do |object|
57
+ obj = object.clone
58
+ obj.delete(:game_time)
59
+ obj
60
+ end
61
+
56
62
  @db[:units].insert_conflict(
57
63
  constraint: :units_pkey,
58
64
  update: { position: Sequel[:excluded][:position],
59
65
  altitude: Sequel[:excluded][:altitude],
60
66
  heading: Sequel[:excluded][:heading],
67
+ speed: Sequel[:excluded][:speed],
61
68
  updated_at: Sequel[:excluded][:updated_at],
62
69
  deleted: Sequel[:excluded][:deleted] }
63
70
  )
@@ -23,7 +23,7 @@ module TacScribe
23
23
 
24
24
  def start
25
25
  loop do
26
- wrapped_event = @event_queue.get_event
26
+ wrapped_event = @event_queue.shift
27
27
  process_event(wrapped_event)
28
28
  rescue StandardError => e
29
29
  puts wrapped_event
@@ -38,6 +38,10 @@ module TacScribe
38
38
  update_object(wrapped_event[:event], wrapped_event[:time])
39
39
  when :delete_object
40
40
  delete_object(wrapped_event[:object_id])
41
+ when :set_latitude
42
+ update_latitude wrapped_event[:value]
43
+ when :set_longitude
44
+ update_longitude wrapped_event[:value]
41
45
  end
42
46
  end
43
47
 
@@ -60,20 +64,28 @@ module TacScribe
60
64
  # format.
61
65
  # @option event [BigDecimal] :altitude The object altitude above sea level
62
66
  # in meters to 1 decimal place.
63
- def update_object(event, _time)
67
+ def update_object(event, time)
68
+ return if ignore_unit?(even)
69
+
70
+ event[:game_time] = time
71
+
72
+ self.events_processed += 1
73
+ @cache.write_object(event)
74
+ end
75
+
76
+ def ignore_unit?(event)
64
77
  if @@ignored_units.include?(event[:object_id])
65
78
  self.events_ignored += 1
66
- return
79
+ return true
67
80
  end
68
81
 
69
82
  if @whitelist && event[:type] && !@whitelist.include?(event[:type])
70
83
  @@ignored_units << event[:object_id]
71
84
  self.events_ignored += 1
72
- return
85
+ return true
73
86
  end
74
87
 
75
- self.events_processed += 1
76
- @cache.write_object(event)
88
+ false
77
89
  end
78
90
 
79
91
  # Process a delete event for an object
@@ -81,12 +93,19 @@ module TacScribe
81
93
  # @param id [String] A hexadecimal number representing the object ID
82
94
  def delete_object(id)
83
95
  if @@ignored_units.delete?(id)
84
- self.events_processed += 1
85
96
  nil
86
97
  else
87
- self.events_processed += 1
88
98
  @cache.delete_object(id)
89
99
  end
100
+ self.events_processed += 1
101
+ end
102
+
103
+ def update_latitude(value)
104
+ Cache.instance.reference_latitude = value
105
+ end
106
+
107
+ def update_longitude(value)
108
+ Cache.instance.reference_longitude = value
90
109
  end
91
110
  end
92
111
  end
@@ -21,7 +21,7 @@ module TacScribe
21
21
  @events.size
22
22
  end
23
23
 
24
- def get_event
24
+ def shift
25
25
  @events.shift
26
26
  end
27
27
 
@@ -71,9 +71,9 @@ module TacScribe
71
71
  def set_property(property:, value:)
72
72
  case property
73
73
  when 'ReferenceLatitude'
74
- Cache.instance.reference_latitude = BigDecimal(value)
74
+ @events << { type: :set_latitude, value: BigDecimal(value) }
75
75
  when 'ReferenceLongitude'
76
- Cache.instance.reference_longitude = BigDecimal(value)
76
+ @events << { type: :set_longitude, value: BigDecimal(value) }
77
77
  when 'ReferenceTime'
78
78
  @reference_time = @time = Time.parse(value)
79
79
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TacScribe
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.0'
5
5
  end
data/tac_scribe.gemspec CHANGED
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec|
36
36
  # of an API breaking change are higher then normal. Therefore lock the
37
37
  # version
38
38
  spec.add_dependency 'concurrent-ruby', '~>1.1.5'
39
+ spec.add_dependency 'haversine', '~>0.3'
39
40
  spec.add_dependency 'sequel-postgis-georuby', '0.1.2'
40
41
  spec.add_dependency 'tacview_client', '~>0.1'
41
42
 
metadata CHANGED
@@ -1,178 +1,192 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tac_scribe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeffrey Jones
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-03-17 00:00:00.000000000 Z
11
+ date: 2020-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
+ name: pg
14
15
  requirement: !ruby/object:Gem::Requirement
15
16
  requirements:
16
- - - ">="
17
+ - - "~>"
17
18
  - !ruby/object:Gem::Version
18
- version: '0'
19
- name: activerecord-jdbcpostgresql-adapter
20
- prerelease: false
19
+ version: '1.1'
21
20
  type: :runtime
21
+ prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '1.1'
27
27
  - !ruby/object:Gem::Dependency
28
+ name: georuby
28
29
  requirement: !ruby/object:Gem::Requirement
29
30
  requirements:
30
31
  - - "~>"
31
32
  - !ruby/object:Gem::Version
32
33
  version: '2.5'
33
- name: georuby
34
- prerelease: false
35
34
  type: :runtime
35
+ prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '2.5'
41
41
  - !ruby/object:Gem::Dependency
42
+ name: sequel
42
43
  requirement: !ruby/object:Gem::Requirement
43
44
  requirements:
44
45
  - - "~>"
45
46
  - !ruby/object:Gem::Version
46
47
  version: '5.22'
47
- name: sequel
48
- prerelease: false
49
48
  type: :runtime
49
+ prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5.22'
55
55
  - !ruby/object:Gem::Dependency
56
+ name: concurrent-ruby
56
57
  requirement: !ruby/object:Gem::Requirement
57
58
  requirements:
58
59
  - - "~>"
59
60
  - !ruby/object:Gem::Version
60
61
  version: 1.1.5
61
- name: concurrent-ruby
62
- prerelease: false
63
62
  type: :runtime
63
+ prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 1.1.5
69
69
  - !ruby/object:Gem::Dependency
70
+ name: haversine
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.3'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sequel-postgis-georuby
70
85
  requirement: !ruby/object:Gem::Requirement
71
86
  requirements:
72
87
  - - '='
73
88
  - !ruby/object:Gem::Version
74
89
  version: 0.1.2
75
- name: sequel-postgis-georuby
76
- prerelease: false
77
90
  type: :runtime
91
+ prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
94
  - - '='
81
95
  - !ruby/object:Gem::Version
82
96
  version: 0.1.2
83
97
  - !ruby/object:Gem::Dependency
98
+ name: tacview_client
84
99
  requirement: !ruby/object:Gem::Requirement
85
100
  requirements:
86
101
  - - "~>"
87
102
  - !ruby/object:Gem::Version
88
103
  version: '0.1'
89
- name: tacview_client
90
- prerelease: false
91
104
  type: :runtime
105
+ prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
110
  version: '0.1'
97
111
  - !ruby/object:Gem::Dependency
112
+ name: bundler
98
113
  requirement: !ruby/object:Gem::Requirement
99
114
  requirements:
100
115
  - - "~>"
101
116
  - !ruby/object:Gem::Version
102
117
  version: '2.0'
103
- name: bundler
104
- prerelease: false
105
118
  type: :development
119
+ prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
107
121
  requirements:
108
122
  - - "~>"
109
123
  - !ruby/object:Gem::Version
110
124
  version: '2.0'
111
125
  - !ruby/object:Gem::Dependency
126
+ name: rake
112
127
  requirement: !ruby/object:Gem::Requirement
113
128
  requirements:
114
129
  - - "~>"
115
130
  - !ruby/object:Gem::Version
116
131
  version: '10.0'
117
- name: rake
118
- prerelease: false
119
132
  type: :development
133
+ prerelease: false
120
134
  version_requirements: !ruby/object:Gem::Requirement
121
135
  requirements:
122
136
  - - "~>"
123
137
  - !ruby/object:Gem::Version
124
138
  version: '10.0'
125
139
  - !ruby/object:Gem::Dependency
140
+ name: rspec
126
141
  requirement: !ruby/object:Gem::Requirement
127
142
  requirements:
128
143
  - - "~>"
129
144
  - !ruby/object:Gem::Version
130
145
  version: '3.8'
131
- name: rspec
132
- prerelease: false
133
146
  type: :development
147
+ prerelease: false
134
148
  version_requirements: !ruby/object:Gem::Requirement
135
149
  requirements:
136
150
  - - "~>"
137
151
  - !ruby/object:Gem::Version
138
152
  version: '3.8'
139
153
  - !ruby/object:Gem::Dependency
154
+ name: rubocop
140
155
  requirement: !ruby/object:Gem::Requirement
141
156
  requirements:
142
157
  - - "~>"
143
158
  - !ruby/object:Gem::Version
144
159
  version: '0.73'
145
- name: rubocop
146
- prerelease: false
147
160
  type: :development
161
+ prerelease: false
148
162
  version_requirements: !ruby/object:Gem::Requirement
149
163
  requirements:
150
164
  - - "~>"
151
165
  - !ruby/object:Gem::Version
152
166
  version: '0.73'
153
167
  - !ruby/object:Gem::Dependency
168
+ name: simplecov
154
169
  requirement: !ruby/object:Gem::Requirement
155
170
  requirements:
156
171
  - - "~>"
157
172
  - !ruby/object:Gem::Version
158
173
  version: '0.17'
159
- name: simplecov
160
- prerelease: false
161
174
  type: :development
175
+ prerelease: false
162
176
  version_requirements: !ruby/object:Gem::Requirement
163
177
  requirements:
164
178
  - - "~>"
165
179
  - !ruby/object:Gem::Version
166
180
  version: '0.17'
167
181
  - !ruby/object:Gem::Dependency
182
+ name: yard
168
183
  requirement: !ruby/object:Gem::Requirement
169
184
  requirements:
170
185
  - - "~>"
171
186
  - !ruby/object:Gem::Version
172
187
  version: '0.9'
173
- name: yard
174
- prerelease: false
175
188
  type: :development
189
+ prerelease: false
176
190
  version_requirements: !ruby/object:Gem::Requirement
177
191
  requirements:
178
192
  - - "~>"
@@ -219,7 +233,7 @@ licenses:
219
233
  - AGPL-3.0-or-later
220
234
  metadata:
221
235
  yard.run: yri
222
- post_install_message:
236
+ post_install_message:
223
237
  rdoc_options: []
224
238
  require_paths:
225
239
  - lib
@@ -234,9 +248,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
234
248
  - !ruby/object:Gem::Version
235
249
  version: '0'
236
250
  requirements: []
237
- rubyforge_project:
238
- rubygems_version: 2.7.6
239
- signing_key:
251
+ rubygems_version: 3.0.3
252
+ signing_key:
240
253
  specification_version: 4
241
254
  summary: Write Tacview data to PostGIS database
242
255
  test_files: []