bitgirder-platform 0.1.7

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.
Files changed (51) hide show
  1. data/LICENSE.txt +176 -0
  2. data/bin/ensure-test-db +117 -0
  3. data/bin/install-mysql +375 -0
  4. data/bin/tomcat7 +569 -0
  5. data/lib/bitgirder/concurrent.rb +400 -0
  6. data/lib/bitgirder/core.rb +1235 -0
  7. data/lib/bitgirder/etl.rb +58 -0
  8. data/lib/bitgirder/event/file.rb +485 -0
  9. data/lib/bitgirder/event/logger/testing.rb +140 -0
  10. data/lib/bitgirder/event/logger.rb +137 -0
  11. data/lib/bitgirder/event/testing.rb +88 -0
  12. data/lib/bitgirder/http.rb +255 -0
  13. data/lib/bitgirder/io/testing.rb +33 -0
  14. data/lib/bitgirder/io.rb +959 -0
  15. data/lib/bitgirder/irb.rb +35 -0
  16. data/lib/bitgirder/mysql.rb +60 -0
  17. data/lib/bitgirder/ops/java.rb +117 -0
  18. data/lib/bitgirder/ops/ruby.rb +235 -0
  19. data/lib/bitgirder/testing.rb +152 -0
  20. data/lib/doc-gen0.rb +0 -0
  21. data/lib/doc-gen1.rb +0 -0
  22. data/lib/doc-gen10.rb +0 -0
  23. data/lib/doc-gen11.rb +0 -0
  24. data/lib/doc-gen12.rb +0 -0
  25. data/lib/doc-gen13.rb +0 -0
  26. data/lib/doc-gen14.rb +0 -0
  27. data/lib/doc-gen15.rb +0 -0
  28. data/lib/doc-gen16.rb +0 -0
  29. data/lib/doc-gen17.rb +14 -0
  30. data/lib/doc-gen18.rb +0 -0
  31. data/lib/doc-gen19.rb +0 -0
  32. data/lib/doc-gen2.rb +0 -0
  33. data/lib/doc-gen20.rb +182 -0
  34. data/lib/doc-gen21.rb +0 -0
  35. data/lib/doc-gen3.rb +0 -0
  36. data/lib/doc-gen4.rb +0 -0
  37. data/lib/doc-gen5.rb +0 -0
  38. data/lib/doc-gen6.rb +0 -0
  39. data/lib/doc-gen7.rb +0 -0
  40. data/lib/doc-gen8.rb +0 -0
  41. data/lib/doc-gen9.rb +0 -0
  42. data/lib/mingle/bincodec.rb +512 -0
  43. data/lib/mingle/codec.rb +54 -0
  44. data/lib/mingle/http.rb +156 -0
  45. data/lib/mingle/io/stream.rb +142 -0
  46. data/lib/mingle/io.rb +160 -0
  47. data/lib/mingle/json.rb +257 -0
  48. data/lib/mingle/service.rb +110 -0
  49. data/lib/mingle-em.rb +92 -0
  50. data/lib/mingle.rb +2917 -0
  51. metadata +100 -0
@@ -0,0 +1,58 @@
1
+ require 'bitgirder/core'
2
+
3
+ module BitGirder
4
+ module Etl
5
+
6
+ include BitGirder::Core
7
+
8
+ PHASE_EXTRACT = :extract
9
+ PHASE_TRANSFORM = :transform
10
+ PHASE_LOAD = :load
11
+
12
+ class BlockScanner < BitGirderClass
13
+
14
+ bg_attr :records
15
+ bg_attr :block
16
+
17
+ public
18
+ def each_with_id
19
+ @records.each_with_index { |rec, idx| yield( rec, @block.ids[ idx ] ) }
20
+ end
21
+ end
22
+
23
+ class RecordBlock < BitGirderClass
24
+
25
+ bg_attr :ids
26
+ bg_attr :records
27
+ bg_attr :next_read
28
+
29
+ private
30
+ def impl_initialize
31
+
32
+ id_len = @ids.size
33
+
34
+ @records.each_pair do |coding, recs|
35
+ unless recs.size == @ids.size
36
+ raise "Block has #{@ids.size} ids but #{@recs.size} records " \
37
+ "for coding #@coding"
38
+ end
39
+ end
40
+ end
41
+
42
+ public
43
+ def size
44
+ @ids.size
45
+ end
46
+
47
+ public
48
+ def coding( nm )
49
+
50
+ recs =
51
+ ( @records[ nm ] or raise "Block has no records for coding: #{nm}" )
52
+
53
+ BlockScanner.new( :records => recs, :block => self )
54
+ end
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,485 @@
1
+ require 'bitgirder/core'
2
+ include BitGirder::Core
3
+
4
+ require 'bitgirder/io'
5
+
6
+ require 'stringio'
7
+
8
+ module BitGirder
9
+ module Event
10
+ module File
11
+
12
+ include BitGirder::Io
13
+
14
+ FILE_MAGIC = "3vEntF!L"
15
+ FILE_VERSION = "event-file20120703"
16
+
17
+ class EventFileIo < BitGirderClass
18
+
19
+ bg_attr :codec
20
+
21
+ HEADER_SIZE = 4
22
+
23
+ DEFAULT_BUFFER_SIZE = Io::DataSize.as_instance( "4m" )
24
+ end
25
+
26
+ class OpenResult < BitGirderClass
27
+
28
+ bg_attr :io
29
+ bg_attr :is_reopen, :default => false
30
+ bg_attr :pos, :default => 0
31
+ end
32
+
33
+ class WriteError < StandardError; end
34
+
35
+ class ClosedError < StandardError; end
36
+
37
+ class EventFileWriter < EventFileIo
38
+
39
+ DEFAULT_ROTATE_SIZE = Io::DataSize.as_instance( "64m" )
40
+
41
+ bg_attr :file_factory
42
+
43
+ bg_attr :rotate_size,
44
+ :processor => Io::DataSize,
45
+ :default => DEFAULT_ROTATE_SIZE
46
+
47
+ bg_attr :buffer_size, :processor => Io::DataSize, :required => false
48
+
49
+ bg_attr :event_handler, :required => false
50
+
51
+ private
52
+ def impl_initialize
53
+
54
+ @buffer_size ||= [ @rotate_size, DEFAULT_BUFFER_SIZE ].min
55
+
56
+ if @buffer_size > @rotate_size
57
+ raise ArgumentError,
58
+ "Buffer size #@buffer_size > rotate size #@rotate_size"
59
+ end
60
+
61
+ @conv = Io::BinaryConverter.new( :order => Io::ORDER_LITTLE_ENDIAN )
62
+ end
63
+
64
+ public
65
+ def closed?
66
+ @closed
67
+ end
68
+
69
+ private
70
+ def send_event( ev, *argv )
71
+
72
+ if @event_handler && @event_handler.respond_to?( ev )
73
+ @event_handler.send( ev, *argv )
74
+ end
75
+ end
76
+
77
+ private
78
+ def new_string_io
79
+ RubyVersions.when_19x( StringIO.new ) do |io|
80
+ io.set_encoding( Encoding::BINARY )
81
+ end
82
+ end
83
+
84
+ private
85
+ def reset_bin_writer
86
+
87
+ @bin =
88
+ Io::BinaryWriter.new(
89
+ :io => StringIO.new( @buf ),
90
+ :order => Io::ORDER_LITTLE_ENDIAN
91
+ )
92
+ end
93
+
94
+ private
95
+ def write_file_header
96
+
97
+ @bin.write_full( FILE_MAGIC )
98
+ @bin.write_utf8( FILE_VERSION )
99
+
100
+ if ( hdr_len = @bin.pos ) >= ( rs = @rotate_size.bytes )
101
+
102
+ @closed = true
103
+
104
+ raise WriteError,
105
+ "File header length #{hdr_len} >= rotate size #{rs}"
106
+ end
107
+
108
+ @buf_remain -= @bin.pos
109
+ end
110
+
111
+ private
112
+ def update_remain_counts( file_remain )
113
+
114
+ @file_remain = file_remain
115
+ @buf_remain = [ @file_remain, @buffer_size.bytes ].min
116
+ end
117
+
118
+ private
119
+ def ensure_io
120
+
121
+ @buf ||= ( "\x00" * @buffer_size.bytes )
122
+
123
+ unless @io
124
+
125
+ open_res = @file_factory.open_file
126
+ @io = open_res.io
127
+
128
+ update_remain_counts( @rotate_size.bytes - open_res.pos )
129
+ reset_bin_writer
130
+ write_file_header unless open_res.is_reopen
131
+ end
132
+ end
133
+
134
+ private
135
+ def close_file
136
+
137
+ if @io
138
+
139
+ @file_factory.close_file( @io )
140
+ @buf = @bin.io.string
141
+ @io, @bin, @file_remain, @buf_remain = nil, nil, -1, -1
142
+ end
143
+ end
144
+
145
+ private
146
+ def impl_flush
147
+
148
+ completes_file = @file_remain <= @buffer_size.bytes
149
+
150
+ @buf.slice!( @bin.io.pos, @buf.size )
151
+
152
+ @io.write( @buf )
153
+ send_event( :wrote_buffer, @buf.size )
154
+
155
+ # Order matters: need to use @buf before reset_bin_writer
156
+ update_remain_counts( @file_remain - @buf.size )
157
+ reset_bin_writer
158
+
159
+ close_file if completes_file || @closed
160
+ end
161
+
162
+ private
163
+ def write_ev_header( str_io, dest )
164
+ dest.write( @conv.write_int32( str_io.size ) )
165
+ end
166
+
167
+ private
168
+ def write_serialized_event( str_io, dest )
169
+
170
+ write_ev_header( str_io, dest )
171
+ dest.write( str_io.string[ 0, str_io.size ] )
172
+ end
173
+
174
+ private
175
+ def write_overflow( str_io, io_sz )
176
+
177
+ impl_flush
178
+
179
+ if io_sz > @buffer_size.bytes
180
+
181
+ if io_sz > @rotate_size.bytes
182
+ raise WriteError,
183
+ "Record is too large for rotate size #@rotate_size"
184
+ else
185
+ write_serialized_event( str_io, @io )
186
+ end
187
+ else
188
+ impl_write_event( str_io, io_sz )
189
+ end
190
+ end
191
+
192
+ private
193
+ def impl_write_event( str_io, io_sz )
194
+
195
+ ensure_io
196
+
197
+ if io_sz > @buf_remain
198
+ write_overflow( str_io, io_sz )
199
+ else
200
+ write_serialized_event( str_io, @bin )
201
+ if ( @buf_remain -= io_sz ) == 0 then impl_flush end
202
+ end
203
+ end
204
+
205
+ public
206
+ def write_event( ev )
207
+
208
+ raise ClosedError if @closed
209
+
210
+ str_io = new_string_io
211
+ @codec.encode_event( ev, str_io )
212
+
213
+ io_sz = str_io.pos + HEADER_SIZE
214
+
215
+ impl_write_event( str_io, io_sz )
216
+ end
217
+
218
+ public
219
+ def close
220
+
221
+ @closed = true
222
+ impl_flush if @io
223
+ end
224
+ end
225
+
226
+ class EventFileLogger < BitGirderClass
227
+
228
+ require 'thread'
229
+
230
+ bg_attr :writer
231
+
232
+ @@shutdown_sentinel = Object.new
233
+
234
+ private
235
+ def impl_initialize
236
+ @queue = Queue.new
237
+ end
238
+
239
+ private
240
+ def process_queue
241
+
242
+ until ( ev = @queue.pop ) == @@shutdown_sentinel
243
+ @writer.write_event( ev )
244
+ end
245
+
246
+ @writer.close
247
+ end
248
+
249
+ public
250
+ def start
251
+ @worker = Thread.start { process_queue }
252
+ end
253
+
254
+ public
255
+ def event_logged( ev )
256
+ @queue << ev
257
+ end
258
+
259
+ public
260
+ def shutdown
261
+
262
+ @queue << @@shutdown_sentinel
263
+ @worker.join
264
+
265
+ nil
266
+ end
267
+
268
+ def self.start( *argv )
269
+ self.new( *argv ).tap { |l| l.start }
270
+ end
271
+ end
272
+
273
+ class EventFileExistsError < StandardError; end
274
+
275
+ class NoOpCodec < BitGirderClass
276
+
277
+ public
278
+ def encode_event( io ); end
279
+
280
+ public
281
+ def decode_event( io, len )
282
+ Io.read_full( io, len )
283
+ end
284
+ end
285
+
286
+ class PathGenerator < BitGirderClass
287
+
288
+ bg_attr :format
289
+
290
+ public
291
+ def generate( t = Time.now )
292
+
293
+ millis = ( t.to_f * 1000 ).to_i
294
+ millis_hex = sprintf( "%016x", millis )
295
+
296
+ eval( @format, binding )
297
+ end
298
+
299
+ map_instance_of( String ) { |s| self.new( :format => s ) }
300
+ end
301
+
302
+ class EventFileFactory < BitGirderClass
303
+
304
+ bg_attr :dir
305
+
306
+ bg_attr :path_generator
307
+
308
+ bg_attr :event_handler, :required => false
309
+
310
+ # We can find a way to let callers customize this later with a block or
311
+ # regex as needed
312
+ private
313
+ def find_reopen_target
314
+ Dir.glob( "#@dir/**/*" ).max
315
+ end
316
+
317
+ private
318
+ def reopen_target_corrupt( f, e )
319
+
320
+ if ( eh = @event_handler ) && eh.respond_to?( :reopen_target_corrupt )
321
+ eh.send( :reopen_target_corrupt, f, e )
322
+ else
323
+ warn( e, "Reopen target #{f} was invalid; skipping to next file" )
324
+ end
325
+ end
326
+
327
+ private
328
+ def init_reopen_target( f )
329
+
330
+ ::File.open( f, "r+b" ) do |io|
331
+
332
+ rd = EventFileReader.new( :codec => NoOpCodec.new, :io => io )
333
+
334
+ trunc_at = 0
335
+
336
+ until io.eof? || f == nil do
337
+ begin
338
+ rd.read_event
339
+ trunc_at = io.pos
340
+ rescue FileFormatError => e
341
+ reopen_target_corrupt( f, e )
342
+ f = nil
343
+ rescue EOFError
344
+ io.truncate( trunc_at )
345
+ end
346
+ end
347
+ end
348
+
349
+ f
350
+ end
351
+
352
+ private
353
+ def init_reopen
354
+
355
+ if @reopen_targ = find_reopen_target
356
+ @reopen_targ = init_reopen_target( @reopen_targ )
357
+ end
358
+ end
359
+
360
+ private
361
+ def gen_path
362
+
363
+ base = case pg = @path_generator
364
+ when Proc then pg.call
365
+ when PathGenerator then pg.generate
366
+ else raise TypeError, "Unhandled path generator: #{pg.class}"
367
+ end
368
+
369
+ "#@dir/#{base}"
370
+ end
371
+
372
+ public
373
+ def open_file
374
+
375
+ if @reopen_targ
376
+
377
+ io = ::File.open( @reopen_targ, "a+b" )
378
+ @reopen_targ = nil
379
+
380
+ OpenResult.new(
381
+ :io => io,
382
+ :is_reopen => true,
383
+ :pos => Io.fsize( io )
384
+ )
385
+ else
386
+ if ::File.exist?( path = gen_path )
387
+ raise EventFileExistsError, "File already exists: #{path}"
388
+ else
389
+ OpenResult.new( :io => ::File.open( path, "wb" ) )
390
+ end
391
+ end
392
+ end
393
+
394
+ public
395
+ def close_file( io )
396
+ io.close
397
+ end
398
+
399
+ def self.open( opts )
400
+ self.new( opts ).tap { |ff| ff.send( :init_reopen ) }
401
+ end
402
+ end
403
+
404
+ class FileFormatError < StandardError; end
405
+
406
+ class FileVersionError < FileFormatError; end
407
+
408
+ class FileMagicError < FileFormatError; end
409
+
410
+ class EventFileReader < EventFileIo
411
+
412
+ include Enumerable
413
+
414
+ bg_attr :io
415
+
416
+ private
417
+ def impl_initialize
418
+
419
+ super
420
+
421
+ @bin = Io::BinaryReader.new(
422
+ :io => @io,
423
+ :order => Io::ORDER_LITTLE_ENDIAN
424
+ )
425
+
426
+ @read_file_header = false
427
+ end
428
+
429
+ private
430
+ def read_file_header
431
+
432
+ begin
433
+ unless ( magic = @bin.read_full( 8 ) ) == FILE_MAGIC
434
+ raise FileMagicError,
435
+ "Unrecognized file magic: #{magic.inspect}"
436
+ end
437
+ rescue EOFError
438
+ raise FileMagicError, "Missing or incomplete file magic"
439
+ end
440
+
441
+ unless ( ver_str = @bin.read_utf8 ) == FILE_VERSION
442
+ raise FileVersionError,
443
+ "Unrecognized file version: #{ver_str.inspect}"
444
+ end
445
+
446
+ @read_file_header = true
447
+ end
448
+
449
+ # Returns the next [ ev, loc ] pair if there is one, EOFException if
450
+ # incomplete, and nil if this is the first read of a file which contains
451
+ # only a valid header.
452
+ public
453
+ def read_event
454
+
455
+ read_file_header unless @read_file_header
456
+ return nil if @io.eof?
457
+
458
+ loc = @bin.pos
459
+
460
+ len = @bin.read_int32
461
+ buf = @bin.read_full( len )
462
+
463
+ ev = @codec.decode_event( StringIO.new( buf, "r" ), len )
464
+
465
+ [ ev, loc ]
466
+ end
467
+
468
+ public
469
+ def each_with_loc
470
+
471
+ until @io.eof?
472
+ ev, loc = *( read_event )
473
+ yield( ev, loc ) if loc
474
+ end
475
+ end
476
+
477
+ public
478
+ def each
479
+ each_with_loc { |ev, loc| yield( ev ) }
480
+ end
481
+ end
482
+
483
+ end
484
+ end
485
+ end
@@ -0,0 +1,140 @@
1
+ require 'bitgirder/event/logger'
2
+ require 'bitgirder/event/testing'
3
+
4
+ require 'bitgirder/core'
5
+ include BitGirder::Core
6
+
7
+ require 'bitgirder/testing'
8
+
9
+ require 'thread'
10
+
11
+ module BitGirder
12
+ module Event
13
+ module Logger
14
+
15
+ # Provides helpers for testing event flow in an application. In addition to
16
+ # testing the basic operation of an event listener (does it serialize correctly,
17
+ # filter appropriately, etc), thoroughly tested applications will want to add
18
+ # coverge of specific events.
19
+ #
20
+ # For example, suppose an application expects to log an event upon every login
21
+ # attempt, including the user id, login completion time, and result (_success_,
22
+ # <i>unknown user</i>, <i>bad password</i>, etc). This data might be used in
23
+ # realtime to look for ongoing attacks on an account, or over time to track user
24
+ # engagement. In any event, the events are an important part of the application,
25
+ # but an inadvertent change in the login handling code might cause logins to not
26
+ # be logged. This module helps make it easy to include assertions about event
27
+ # delivery right alongside other assertions (that a login succeeded or failed as
28
+ # expected).
29
+ #
30
+ module Testing
31
+
32
+ class CodecRoundtripper < BitGirderClass
33
+
34
+ bg_attr :codec
35
+ bg_attr :listener, :required => false
36
+
37
+ public
38
+ def event_logged( ev )
39
+
40
+ ev = BitGirder::Event::Testing.roundtrip( ev, @codec )
41
+ @listener.event_logged( ev ) if @listener
42
+ end
43
+ end
44
+
45
+ # A simple event listener (see BitGirder::EventLogger) which accumulates all
46
+ # events into an unbounded list. Test classses should make sure to remove this
47
+ # listener from its associated engine after assertions are complete, either
48
+ # explicitly or via ensure_removed, lest it continue to amass large amounts of
49
+ # unneeded events.
50
+ class EventAccumulator < BitGirderClass
51
+
52
+ include BitGirder::Testing::AssertMethods
53
+
54
+ bg_attr :engine
55
+
56
+ # Creates a new instance associated with the given engine
57
+ def impl_initialize
58
+
59
+ @mut = Mutex.new
60
+ @events = []
61
+ end
62
+
63
+ # Adds +ev+ to this instance's event list.
64
+ public
65
+ def event_logged( ev )
66
+ @mut.synchronize { @events << ev }
67
+ end
68
+
69
+ # Gets a snapshot of the events accumulated so far.
70
+ public
71
+ def events
72
+ @mut.synchronize { Array.new( @events ) }
73
+ end
74
+
75
+ public
76
+ def assert_logged( expct = nil, &blk )
77
+
78
+ if expct
79
+ if blk
80
+ raise "Illegal combination of expect val and block"
81
+ else
82
+ blk = lambda { |ev| ev == expct }
83
+ end
84
+ else
85
+ raise "Block missing" unless blk
86
+ end
87
+
88
+ assert events.find( &blk ), "Block did not match any events"
89
+ end
90
+
91
+ # Executes an arbitrary block, ensuring that this instance is removed from
92
+ # the engine with which it is associated whether or not the block completes
93
+ # normally. This method returns the block's result or raises its exception.
94
+ #
95
+ # This instance is itself provided to the block, enabling (in conjunction
96
+ # with EventAccumulator.create()) test code to easily and reliably wrap an
97
+ # entire test:
98
+ #
99
+ # def test_login_success
100
+ #
101
+ # EventAccumulator.create( ev_eng ).ensure_removed do |acc|
102
+ #
103
+ # # Do a login and first check that it succeeds as wanted
104
+ # login_res = do_login( "ezra", "somepass" )
105
+ # assert( login_res.ok? )
106
+ #
107
+ # # Now also check that login event was generated
108
+ # ev_expct = { :event => :login, :user => "ezra", :result => :ok }
109
+ # acc.assert_logged { |ev| ev == ev_expct }
110
+ #
111
+ # ... # Possibly more test code, cleanup, etc
112
+ # end
113
+ # end
114
+ #
115
+ public
116
+ def ensure_removed
117
+ begin
118
+ yield( self )
119
+ ensure
120
+ @engine.remove_listener( self )
121
+ end
122
+ end
123
+
124
+ # Convenience method to create, register, and return an instance which
125
+ # accumulates events from the given EventLogger::Engine
126
+ def self.create( *argv )
127
+ self.new( *argv ).tap { |acc| acc.engine.add_listener( acc ) }
128
+ end
129
+
130
+ def self.while_accumulating( *argv )
131
+
132
+ acc = self.create( *argv )
133
+ acc.ensure_removed { yield( acc ) }
134
+ end
135
+ end
136
+
137
+ end
138
+ end
139
+ end
140
+ end