omf_oml 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,302 @@
1
+
2
+ require 'omf_oml/tuple'
3
+
4
+ module OMF::OML
5
+
6
+
7
+
8
+ # Read the content of a table and feeds it out as a tuple store.
9
+ # After creation of the object. The actual tuple feed is started
10
+ # with a call to +run+.
11
+ #
12
+ class OmlSqlRow < OmlTuple
13
+
14
+ # *opts:
15
+ # - offset: Ignore first +offset+ rows. If negative or zero serve +offset+ rows initially
16
+ # - limit: Number of rows to fetch each time [1000]
17
+ # - check_interval: Interval in seconds when to check for new data. If 0, only run once.
18
+ #
19
+ def initialize(table_name, db_file, source, opts = {})
20
+ @sname = table_name
21
+ @db_file = db_file
22
+ @source = source
23
+
24
+ unless @offset = opts[:offset]
25
+ @offset = 0
26
+ end
27
+ @limit = opts[:limit]
28
+ @limit = 1000 unless @limit
29
+
30
+ @check_interval = opts[:check_interval]
31
+ @check_interval = 0 unless @check_interval
32
+
33
+
34
+ @on_new_vector_proc = {}
35
+
36
+ schema = find_schema
37
+ super table_name, schema
38
+ end
39
+
40
+
41
+ # Return a specific element of the vector identified either
42
+ # by it's name, or its col index
43
+ #
44
+ def [](name_or_index)
45
+ @vprocs[name_or_index].call(@raw)
46
+ end
47
+
48
+ # Return the elements of the vector as an array
49
+ def to_a(include_oml_internals = false)
50
+ include_oml_internals ? @row.dup : @row[4 .. -1]
51
+ end
52
+
53
+ # Return an array including the values for the names elements
54
+ # given as parameters.
55
+ #
56
+ def select(*col_names)
57
+ r = @row
58
+ col_names.collect do |n|
59
+ p = @vprocs[n]
60
+ #puts "#{n}::#{p}"
61
+ p ? p.call(r) : nil
62
+ end
63
+ end
64
+
65
+ def ts
66
+ self[:oml_ts_server]
67
+ end
68
+
69
+ def seq_no
70
+ self[:oml_seq]
71
+ end
72
+
73
+ # Register a proc to be called when a new tuple arrived
74
+ # on this stream.
75
+ #
76
+ def on_new_tuple(key = :_, &proc)
77
+ if proc
78
+ @on_new_vector_proc[key] = proc
79
+ else
80
+ @on_new_vector_proc.delete key
81
+ end
82
+ run() unless @on_new_vector_proc.empty?
83
+ end
84
+
85
+ # Create and return an +OmlTable+ which captures this tuple stream
86
+ #
87
+ # The argument to this method are either a list of columns to
88
+ # to capture in the table, or an array of column names and
89
+ # an option hash or just
90
+ # the option hash to be provided to the +OmlTable+ constructor.
91
+ #
92
+ # If a block is provided, any arriving tuple is executed by the block
93
+ # which is expected to return an array which is added to the table
94
+ # or nil in which case nothing is added. If a selector array is given the
95
+ # block is called with an array of values in the order of the columns
96
+ # listed in the selector. Otherwise, the block is called directly
97
+ # with the tuple.
98
+ #
99
+ # opts:
100
+ # :schema - use this schema instead for the table
101
+ # :name - name to use for table
102
+ # .... - remaining options to be passed to table constructur
103
+ #
104
+ def capture_in_table(*args, &block)
105
+ if args.length == 1
106
+ if args[0].kind_of?(Array)
107
+ select = args[0]
108
+ elsif args[0].kind_of?(Hash)
109
+ opts = args[0]
110
+ end
111
+ elsif args.length == 2 && args[1].kind_of?(Hash)
112
+ select = args[0]
113
+ opts = args[1]
114
+ else
115
+ opts = {}
116
+ select = args
117
+ end
118
+
119
+ if (tschema = opts.delete(:schema))
120
+ # unless tschema[0].kind_of? Hash
121
+ # tschema = tschema.collect do |cname| {:name => cname} end
122
+ # end
123
+ else
124
+ tschema = select.collect do |cname| {:name => cname} end
125
+ end
126
+ tname = opts.delete(:name) || stream_name
127
+ t = OMF::OML::OmlTable.new(tname, tschema, opts)
128
+ if block
129
+ self.on_new_tuple() do |v|
130
+ #puts "New vector(#{tname}): #{v.schema.inspect} ---- #{v.select(*select).size} <#{v.select(*select).join('|')}>"
131
+ if select
132
+ row = block.call(v.select(*select))
133
+ else
134
+ row = block.call(v)
135
+ end
136
+ if row
137
+ raise "Expected kind of Array, but got '#{row.inspect}'" unless row.kind_of?(Array)
138
+ t.add_row(row)
139
+ end
140
+ end
141
+ else
142
+ self.on_new_tuple() do |v|
143
+ #puts "New vector(#{tname}): #{v.select(*select).join('|')}"
144
+ t.add_row(v.select(*select))
145
+ end
146
+ end
147
+ t
148
+ end
149
+
150
+ def to_table(name = nil, opts = {})
151
+ unless name
152
+ name = @sname
153
+ end
154
+ t = OMF::OML::OmlTable.new(name, self.schema)
155
+ include_oml_internals = opts[:include_oml_internals] || true
156
+ self.on_new_tuple() do |v|
157
+ r = v.to_a(include_oml_internals)
158
+ t.add_row(r)
159
+ end
160
+ t
161
+ end
162
+
163
+
164
+ protected
165
+
166
+ def find_schema()
167
+ stmt = _statement
168
+ cnames = stmt.columns
169
+ ctypes = stmt.types
170
+ schema = []
171
+ #schema << {:name => :oml_sender, :type => 'STRING'}
172
+ cnames.size.times do |i|
173
+ name = cnames[i].to_sym
174
+ schema << {:name => name, :type => ctypes[i]}
175
+ end
176
+ # Rename first col
177
+ first = schema[0]
178
+ raise "BUG: Should be 'name'" if first[:name] != :name
179
+ first[:name] = :oml_sender
180
+
181
+ OmlSchema.new(schema)
182
+ end
183
+
184
+ # override
185
+ def process_schema(schema)
186
+ i = 0
187
+ @vprocs = {}
188
+ schema.each_column do |col|
189
+ name = col[:name]
190
+ j = i + 0
191
+ l = @vprocs[name] = lambda do |r| r[j] end
192
+ @vprocs[i - 4] = l if i > 4
193
+ i += 1
194
+ end
195
+ end
196
+
197
+ def run(in_thread = true)
198
+ return if @running
199
+ if in_thread
200
+ if @db
201
+ # force opening of database in new thread
202
+ begin
203
+ @db.close
204
+ rescue Exception
205
+ # ALERT: issues with finalising statments, don't know how to deal with it
206
+ end
207
+ @db = nil
208
+ @stmt = nil
209
+ end
210
+ Thread.new do
211
+ begin
212
+ _run
213
+ rescue Exception => ex
214
+ error "Exception in OmlSqlRow: #{ex}"
215
+ debug "Exception in OmlSqlRow: #{ex.backtrace.join("\n\t")}"
216
+ end
217
+ end
218
+ else
219
+ _run
220
+ end
221
+ end
222
+
223
+ private
224
+
225
+ def _run
226
+ if @check_interval <= 0
227
+ _run_once
228
+ else
229
+ @running = true
230
+ while (@running)
231
+ begin
232
+ unless _run_once
233
+ # All rows read, wait a bit for news to show up
234
+ sleep @check_interval
235
+ end
236
+ rescue Exception => ex
237
+ warn ex
238
+ debug "\t", ex.backtrace.join("\n\t")
239
+ end
240
+ end
241
+ end
242
+ end
243
+
244
+ # Run a query on database an serve all rows found one at a time.
245
+ # Return true if there might be more rows in the database
246
+ def _run_once
247
+ row_cnt = 0
248
+ _statement.execute(@limit, @offset).each do |r|
249
+ @row = r
250
+ @on_new_vector_proc.each_value do |proc|
251
+ proc.call(self)
252
+ end
253
+ row_cnt += 1
254
+ end
255
+ @offset += row_cnt
256
+ debug "Read #{row_cnt}/#{@offset} rows from '#{@sname}'"
257
+ row_cnt >= @limit # there could be more to read
258
+ end
259
+
260
+ def _statement
261
+ unless @stmt
262
+ db = @db = SQLite3::Database.new(@db_file)
263
+ @db.type_translation = true
264
+ table_name = t = @sname
265
+ if @offset < 0
266
+ cnt = db.execute("select count(*) from #{table_name};")[0][0].to_i
267
+ #debug "CNT: #{cnt}.#{cnt.class} offset: #{@offset}"
268
+ @offset = cnt + @offset # @offset was negative here
269
+ debug("Initial offset #{@offset} in '#{table_name}' with #{cnt} rows")
270
+ @offset = 0 if @offset < 0
271
+ end
272
+ #@stmt = db.prepare("SELECT * FROM #{table_name} LIMIT ? OFFSET ?;")
273
+ @stmt = db.prepare("SELECT _senders.name, #{t}.* FROM #{t} JOIN _senders WHERE #{t}.oml_sender_id = _senders.id LIMIT ? OFFSET ?;")
274
+ end
275
+ @stmt
276
+ end
277
+ end # OmlSqlRow
278
+
279
+
280
+ end
281
+
282
+ if $0 == __FILE__
283
+
284
+ require 'omf_oml/table'
285
+ ep = OMF::OML::OmlSqlSource.new('brooklynDemo.sq3')
286
+ ep.on_new_stream() do |s|
287
+ puts ">>>>>>>>>>>> New stream #{s.stream_name}: #{s.names.join(', ')}"
288
+ case s.stream_name
289
+ when 'wimaxmonitor_wimaxstatus'
290
+ select = [:oml_ts_server, :sender_hostname, :frequency, :signal, :rssi, :cinr, :avg_tx_pw]
291
+ when 'GPSlogger_gps_data'
292
+ select = [:oml_ts_server, :oml_sender_id, :lat, :lon]
293
+ end
294
+
295
+ s.on_new_vector() do |v|
296
+ puts "New vector(#{s.stream_name}): #{v.select(*select).join('|')}"
297
+ end
298
+ end
299
+ ep.run()
300
+
301
+ end
302
+
@@ -0,0 +1,131 @@
1
+
2
+ require 'sqlite3'
3
+
4
+ require 'omf_common/lobject'
5
+ require 'omf_oml/endpoint'
6
+ require 'omf_oml/tuple'
7
+ require 'omf_oml/sql_row'
8
+
9
+ module OMF::OML
10
+
11
+ # This class fetches the content of an sqlite3 database and serves it as multiple
12
+ # OML streams.
13
+ #
14
+ # After creating the object, the @run@ method needs to be called to
15
+ # start producing the streams.
16
+ #
17
+ class OmlSqlSource < OMF::Common::LObject
18
+
19
+ # +opts+ - passed on as +opts+ to the OmlSqlRow constructor
20
+ #
21
+ def initialize(db_file, opts = {})
22
+ raise "Can't find database '#{db_file}'" unless File.readable?(db_file)
23
+ @db_file = db_file
24
+ @running = false
25
+ @on_new_stream_procs = {}
26
+ @tables = {}
27
+ @table_opts = opts
28
+ end
29
+
30
+ # Register a proc to be called when a new stream was
31
+ # discovered on this endpoint.
32
+ #
33
+ def on_new_stream(key = :_, &proc)
34
+ if proc
35
+ @on_new_stream_procs[key] = proc
36
+ else
37
+ @on_new_stream_procs.delete key
38
+ end
39
+ end
40
+
41
+
42
+ # def report_new_stream(stream)
43
+ # @on_new_stream_procs.each_value do |proc|
44
+ # proc.call(stream)
45
+ # end
46
+ # end
47
+
48
+ # Start checking the database for tables and create a new stream
49
+ # by calling the internal +report_new_table+ method.
50
+ # If +check_every+ > 0 continue checking every +check_every+ seconds
51
+ # for new tables in the database, otherwise it's only checked once
52
+ #
53
+ #
54
+ def run(check_every = -1)
55
+ if check_every <= 0
56
+ run_once()
57
+ else
58
+ Thread.new do
59
+ @running = true
60
+ while (@running)
61
+ begin
62
+ run_once()
63
+ rescue Exception => ex
64
+ error "Exception in OmlSqlSource#run: #{ex}"
65
+ debug "Exception in OmlSqlSource#run: #{ex.backtrace.join("\n\t")}"
66
+ end
67
+ sleep check_every
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ def run_once()
74
+ unless @db
75
+ @db = SQLite3::Database.new(@db_file)
76
+ @db.type_translation = true
77
+ end
78
+
79
+ # first find tables
80
+ @db.execute( "SELECT * FROM sqlite_master WHERE type='table';") do |r|
81
+ table_name = r[1]
82
+ report_new_table(table_name, @table_opts) unless table_name.start_with?('_')
83
+ end
84
+ end
85
+
86
+
87
+ protected
88
+
89
+ # THis method is being called for every table detected in the database.
90
+ # It creates a new +OmlSqlRow+ object with +opts+ as the only argument.
91
+ # The tables is then streamed as a tuple stream.
92
+ # After the stream has been created, each block registered with
93
+ # +on_new_stream+ is then called with the new stream as its single
94
+ # argument.
95
+ #
96
+ def report_new_table(table_name, opts = {})
97
+ return if @tables.key?(table_name) # check if already reported before
98
+ debug "Found table: #{table_name}"
99
+ t = @tables[table_name] = OmlSqlRow.new(table_name, @db_file, self, opts)
100
+ @on_new_stream_procs.each_value do |proc|
101
+ proc.call(t)
102
+ end
103
+ end
104
+
105
+ end
106
+
107
+
108
+
109
+ end
110
+
111
+ if $0 == __FILE__
112
+
113
+ require 'omf_oml/table'
114
+ ep = OMF::OML::OmlSqlSource.new('brooklynDemo.sq3')
115
+ ep.on_new_stream() do |s|
116
+ puts ">>>>>>>>>>>> New stream #{s.stream_name}: #{s.names.join(', ')}"
117
+ case s.stream_name
118
+ when 'wimaxmonitor_wimaxstatus'
119
+ select = [:oml_ts_server, :sender_hostname, :frequency, :signal, :rssi, :cinr, :avg_tx_pw]
120
+ when 'GPSlogger_gps_data'
121
+ select = [:oml_ts_server, :oml_sender_id, :lat, :lon]
122
+ end
123
+
124
+ s.on_new_vector() do |v|
125
+ puts "New vector(#{s.stream_name}): #{v.select(*select).join('|')}"
126
+ end
127
+ end
128
+ ep.run()
129
+
130
+ end
131
+
@@ -0,0 +1,227 @@
1
+
2
+ require 'monitor'
3
+
4
+ require 'omf_common/lobject'
5
+ require 'omf_oml'
6
+ require 'omf_oml/schema'
7
+
8
+
9
+
10
+ module OMF::OML
11
+
12
+ # This class represents a database like table holding a sequence of OML measurements (rows) according
13
+ # a common schema.
14
+ #
15
+ class OmlTable < OMF::Common::LObject
16
+
17
+ def self.create(tname, schema, opts = {}, &on_before_row_added)
18
+ if (index = opts.delete(:index))
19
+ require 'omf_oml/indexed_table'
20
+ OmlIndexedTable.new(tname, index, schema, &on_before_row_added)
21
+ else
22
+ OmlTable.new(tname, schema, opts, &on_before_row_added)
23
+ end
24
+ end
25
+ include MonitorMixin
26
+
27
+ attr_reader :name
28
+ attr_accessor :max_size
29
+ attr_reader :schema
30
+ attr_reader :offset
31
+
32
+ #
33
+ # tname - Name of table
34
+ # schema - OmlSchema or Array containing [name, type*] for every column in table
35
+ # Table adds a '__id__' column at the beginning which keeps track of the rows unique id
36
+ # opts -
37
+ # :max_size - keep table to that size by dropping older rows
38
+ # :index - only keep the latest inserted row for a unique col value - messes with row order
39
+ #
40
+ def initialize(tname, schema, opts = {}, &on_before_row_added)
41
+ super tname
42
+
43
+ #@endpoint = endpoint
44
+ @name = tname
45
+ @schema = OmlSchema.create(schema)
46
+ unless @schema.name_at(0) == :__id__
47
+ @schema.insert_column_at(0, [:__id__, 'int'])
48
+ end
49
+ @opts = opts
50
+ if (index = opts[:index])
51
+ throw "No longer supported, use IndexedTable instead"
52
+ # @indexed_rows = {}
53
+ # @index_col = @schema.index_for_col(index)
54
+ end
55
+ @on_before_row_added = on_before_row_added
56
+ @offset = 0 # number of rows skipped before the first one recorded here
57
+ @rows = []
58
+ @row_id = 0 # Each new row is assigned an id
59
+ @max_size = opts[:max_size]
60
+ @on_content_changed = {}
61
+ end
62
+
63
+ def rows
64
+ #@indexed_rows ? @indexed_rows.values : @rows
65
+ @rows
66
+ end
67
+
68
+ # Register +callback+ to be called to process any newly
69
+ # offered row before it being added to internal storage.
70
+ # The callback's argument is the new row (TODO: in what form)
71
+ # and should return what is being added instead of the original
72
+ # row. If the +callback+ returns nil, nothing is being added.
73
+ #
74
+ def on_before_row_added(&callback)
75
+ @on_before_row_added = callback
76
+ end
77
+
78
+ # Register callback for when the content of the table is changes. The key
79
+ # allows for the callback to be removed by calling this method
80
+ # without a block. . If the
81
+ # optional 'offset' value is set to zero or a positive value,
82
+ # then the currently stored values starting at this index are being
83
+ # immediately sent to 'proc'. The 'proc' is expected to receive two
84
+ # parameters, an 'action' and the content changed. The 'action' is either
85
+ # ':added', or ':removed' and the content is an array of rows.
86
+ #
87
+ def on_content_changed(key, offset = -1, &proc)
88
+ #puts ">>>>>>> #{offset}"
89
+ if proc
90
+ @on_content_changed[key] = proc
91
+ if offset >= 0
92
+ #with_offset = proc.arity == 2
93
+ proc.call(:added, @rows[offset .. -1])
94
+ #.each_with_index do |r, i|
95
+ # with_offset ? proc.call(r, offset + i) : proc.call(r)
96
+ # end
97
+ end
98
+ else
99
+ @on_content_changed.delete key
100
+ end
101
+ end
102
+
103
+ # NOTE: +on_row_added+ callbacks are done within the monitor.
104
+ #
105
+ def add_row(row, needs_casting = false)
106
+ synchronize do
107
+ if row = _add_row(row, needs_casting)
108
+ _notify_content_changed(:added, [row])
109
+ end
110
+ end
111
+ end
112
+
113
+ # Return a new table which shadows this table but only contains
114
+ # rows with unique values in the column 'col_name' and of these the
115
+ # latest added rows to this table.
116
+ #
117
+ # col_name - Name of column to use for indexing
118
+ #
119
+ def indexed_by(col_name)
120
+ require 'omf_oml/indexed_table'
121
+ OmlIndexedTable.shadow(self, col_name)
122
+ end
123
+
124
+ # Add an array of rows to this table
125
+ #
126
+ def add_rows(rows, needs_casting = false)
127
+ synchronize do
128
+ added = rows.map { |row| _add_row(row, needs_casting) }
129
+ added = added.compact
130
+ unless added.empty?
131
+ _notify_content_changed(:added, added)
132
+ end
133
+ end
134
+ end
135
+
136
+ # Return a new table which only contains the rows of this
137
+ # table whose value in column 'col_name' is equal to 'col_value'
138
+ #
139
+ def create_sliced_table(col_name, col_value, table_opts = {})
140
+ sname = "#{@name}_slice_#{Kernel.rand}"
141
+
142
+ st = self.class.new(name, @schema, table_opts)
143
+ st.instance_variable_set(:@sname, sname)
144
+ st.instance_variable_set(:@master_ds, self)
145
+ def st.release
146
+ @master_ds.on_content_changed(@sname) # release callback
147
+ end
148
+
149
+ index = @schema.index_for_col(col_name)
150
+ on_content_changed(sname, 0) do |action, rows|
151
+ if action == :removed
152
+ warn "No support for removing rows from sliced table '#{sname}'."
153
+ next
154
+ end
155
+ rows.each do |row|
156
+ if row[index] == col_value
157
+ row = row[1 .. -1] # remove the row_id
158
+ debug "Add row '#{row.inspect}'"
159
+ st.add_row(row)
160
+ end
161
+ end
162
+ end
163
+ debug "Created sliced table from '#{@name}' (rows: #{st.rows.length}-#{@rows.length})"
164
+ st
165
+ end
166
+
167
+ def describe()
168
+ rows
169
+ end
170
+
171
+ def data_sources
172
+ self
173
+ end
174
+
175
+ private
176
+
177
+ # NOT synchronized
178
+ #
179
+ def _add_row(row, needs_casting = false)
180
+ if needs_casting
181
+ row = @schema.cast_row(row)
182
+ end
183
+ #puts row.inspect
184
+ if @on_before_row_added
185
+ row = @on_before_row_added.call(row)
186
+ end
187
+ return nil unless row
188
+
189
+ row.insert(0, @row_id += 1)
190
+ _add_row_finally(row)
191
+ end
192
+
193
+ # Finally add 'row' to internal storage. This would be hte method to
194
+ # overide in sub classes as this is thread safe and all other pre-storage
195
+ # test have been performed. Should return the row added, or nil if nothing
196
+ # was ultimately added.
197
+ #
198
+ def _add_row_finally(row)
199
+ # if @indexed_rows
200
+ # @indexed_rows[row[@index_col]] = row
201
+ # return
202
+ # end
203
+
204
+ @rows << row
205
+ if @max_size && @max_size > 0 && (s = @rows.size) > @max_size
206
+ if (removed_row = @rows.shift) # not necessarily fool proof, but fast
207
+ _notify_content_changed(:removed, [removed_row])
208
+ end
209
+ @offset = @offset + 1
210
+ end
211
+ row
212
+ end
213
+
214
+ def _notify_content_changed(action, rows)
215
+ @on_content_changed.each_value do |proc|
216
+ #puts "call: #{proc.inspect}"
217
+ #if proc.arity == 1
218
+ proc.call(action, rows)
219
+ #else
220
+ #proc.call(row, @offset)
221
+ #end
222
+ end
223
+ end
224
+
225
+ end # OMLTable
226
+
227
+ end