fluq 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/.gitignore +3 -0
  2. data/.travis.yml +6 -0
  3. data/Gemfile +6 -0
  4. data/Gemfile.lock +39 -0
  5. data/MIT-LICENCE +19 -0
  6. data/README.md +10 -0
  7. data/Rakefile +11 -0
  8. data/benchmark/logging.rb +37 -0
  9. data/benchmark/socket.rb +52 -0
  10. data/bin/fluq-rb +8 -0
  11. data/examples/common.rb +3 -0
  12. data/examples/simple.rb +5 -0
  13. data/fluq.gemspec +33 -0
  14. data/lib/fluq.rb +50 -0
  15. data/lib/fluq/buffer.rb +6 -0
  16. data/lib/fluq/buffer/base.rb +51 -0
  17. data/lib/fluq/buffer/file.rb +68 -0
  18. data/lib/fluq/cli.rb +142 -0
  19. data/lib/fluq/dsl.rb +49 -0
  20. data/lib/fluq/dsl/options.rb +27 -0
  21. data/lib/fluq/error.rb +2 -0
  22. data/lib/fluq/event.rb +55 -0
  23. data/lib/fluq/feed.rb +6 -0
  24. data/lib/fluq/feed/base.rb +18 -0
  25. data/lib/fluq/feed/json.rb +28 -0
  26. data/lib/fluq/feed/msgpack.rb +27 -0
  27. data/lib/fluq/feed/tsv.rb +30 -0
  28. data/lib/fluq/handler.rb +6 -0
  29. data/lib/fluq/handler/base.rb +80 -0
  30. data/lib/fluq/handler/log.rb +67 -0
  31. data/lib/fluq/handler/null.rb +4 -0
  32. data/lib/fluq/input.rb +6 -0
  33. data/lib/fluq/input/base.rb +59 -0
  34. data/lib/fluq/input/socket.rb +50 -0
  35. data/lib/fluq/input/socket/connection.rb +41 -0
  36. data/lib/fluq/mixins.rb +6 -0
  37. data/lib/fluq/mixins/loggable.rb +7 -0
  38. data/lib/fluq/mixins/logger.rb +26 -0
  39. data/lib/fluq/reactor.rb +76 -0
  40. data/lib/fluq/testing.rb +26 -0
  41. data/lib/fluq/url.rb +16 -0
  42. data/lib/fluq/version.rb +3 -0
  43. data/spec/fluq/buffer/base_spec.rb +21 -0
  44. data/spec/fluq/buffer/file_spec.rb +47 -0
  45. data/spec/fluq/dsl/options_spec.rb +24 -0
  46. data/spec/fluq/dsl_spec.rb +43 -0
  47. data/spec/fluq/event_spec.rb +25 -0
  48. data/spec/fluq/feed/base_spec.rb +15 -0
  49. data/spec/fluq/feed/json_spec.rb +27 -0
  50. data/spec/fluq/feed/msgpack_spec.rb +27 -0
  51. data/spec/fluq/feed/tsv_spec.rb +27 -0
  52. data/spec/fluq/handler/base_spec.rb +70 -0
  53. data/spec/fluq/handler/log_spec.rb +68 -0
  54. data/spec/fluq/handler/null_spec.rb +11 -0
  55. data/spec/fluq/input/base_spec.rb +29 -0
  56. data/spec/fluq/input/socket/connection_spec.rb +35 -0
  57. data/spec/fluq/input/socket_spec.rb +45 -0
  58. data/spec/fluq/mixins/loggable_spec.rb +10 -0
  59. data/spec/fluq/mixins/logger_spec.rb +25 -0
  60. data/spec/fluq/reactor_spec.rb +58 -0
  61. data/spec/fluq/url_spec.rb +16 -0
  62. data/spec/fluq_spec.rb +11 -0
  63. data/spec/scenario/config/nested/common.rb +3 -0
  64. data/spec/scenario/config/test.rb +3 -0
  65. data/spec/scenario/lib/fluq/handler/custom/test_handler.rb +4 -0
  66. data/spec/spec_helper.rb +12 -0
  67. data/spec/support/configuration.rb +25 -0
  68. metadata +242 -0
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ log/
2
+ tmp/
3
+ *.gem
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ gemfile:
6
+ - Gemfile
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+ gemspec
3
+
4
+ group :development do
5
+ gem "oj"
6
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,39 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ fluq (0.7.0)
5
+ eventmachine-le
6
+ msgpack (~> 0.5.0)
7
+ oj (>= 2.0.10)
8
+ timed_lru
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ diff-lcs (1.2.4)
14
+ eventmachine-le (1.1.5)
15
+ msgpack (0.5.5)
16
+ oj (2.1.3)
17
+ rake (10.1.0)
18
+ rspec (2.13.0)
19
+ rspec-core (~> 2.13.0)
20
+ rspec-expectations (~> 2.13.0)
21
+ rspec-mocks (~> 2.13.0)
22
+ rspec-core (2.13.1)
23
+ rspec-expectations (2.13.0)
24
+ diff-lcs (>= 1.1.3, < 2.0)
25
+ rspec-mocks (2.13.1)
26
+ timed_lru (0.3.1)
27
+ yard (0.8.6.2)
28
+
29
+ PLATFORMS
30
+ java
31
+ ruby
32
+
33
+ DEPENDENCIES
34
+ bundler
35
+ fluq!
36
+ oj
37
+ rake
38
+ rspec
39
+ yard
data/MIT-LICENCE ADDED
@@ -0,0 +1,19 @@
1
+ Permission is hereby granted, free of charge, to any person obtaining
2
+ a copy of this software and associated documentation files (the
3
+ "Software"), to deal in the Software without restriction, including
4
+ without limitation the rights to use, copy, modify, merge, publish,
5
+ distribute, sublicense, and/or sell copies of the Software, and to
6
+ permit persons to whom the Software is furnished to do so, subject to
7
+ the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be
10
+ included in all copies or substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
13
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
16
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
17
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
18
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19
+
data/README.md ADDED
@@ -0,0 +1,10 @@
1
+ FluQ
2
+ ====
3
+
4
+ Description coming soon ...
5
+
6
+ Licence
7
+ -------
8
+
9
+ Copyright 2013 Black Square Media Ltd.
10
+ It is free software, and may be redistributed under the terms specified in the MIT-LICENCE file.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'rake'
2
+
3
+ require 'rspec/mocks/version'
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ require 'yard'
8
+ YARD::Rake::YardocTask.new
9
+
10
+ desc 'Default: run specs.'
11
+ task :default => :spec
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift(File.expand_path('../../lib', __FILE__))
4
+
5
+ require 'bundler/setup'
6
+ require 'fluq'
7
+ require 'benchmark'
8
+
9
+ ITER = 100_000
10
+
11
+ MultiJson.use :oj
12
+ FileUtils.rm_rf FluQ.root.join("log/benchmark")
13
+
14
+ events = (1..ITER).map do
15
+ FluQ::Event.new "a.b#{rand(4)}.c#{rand(100)}.d#{rand(100)}", Time.now.to_i, "k1" => "value", "k2" => "value", "k3" => "value"
16
+ end
17
+
18
+ handler = FluQ::Handler::Log.new \
19
+ path: "log/benchmark/%Y%m/%d/%H/%t.log",
20
+ rewrite: lambda {|t| t.split(".")[1] }
21
+
22
+ puts "--> Started benchmark"
23
+ processed = Benchmark.realtime do
24
+ num = 0
25
+ events.each_slice(1_000) do |slice|
26
+ handler.on_events(slice)
27
+ num += slice.size
28
+ if (num % 10_000).zero?
29
+ puts "--> Processed : #{num}"
30
+ end
31
+ end
32
+ end
33
+
34
+ puts "--> Processed : #{events.size} in #{processed.round(1)}s"
35
+ files = Dir[FluQ.root.join("log/benchmark/**/*.log").to_s]
36
+ lines = `cat #{files.join(' ')} | wc -l`.strip
37
+ puts "--> Written : #{lines} lines"
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift(File.expand_path('../../lib', __FILE__))
4
+
5
+ require 'bundler/setup'
6
+ require 'fluq'
7
+
8
+ BATCH_SIZE = 100_000
9
+ BATCHES = 50
10
+ ROOT = FluQ.root.join("log/benchmark")
11
+ EVENT = FluQ::Event.new("a.b.c.d", Time.now.to_i, "k1" => "value", "k2" => "value", "k3" => "value").to_msgpack
12
+
13
+ FileUtils.rm_rf ROOT.to_s
14
+ FileUtils.mkdir_p ROOT.to_s
15
+
16
+ class FluQ::Handler::Counter < FluQ::Handler::Base
17
+ attr_reader :count
18
+ def initialize(*)
19
+ super
20
+ @count = 0
21
+ end
22
+ def on_events(events)
23
+ @count += events.size
24
+ EM.stop if @count >= BATCHES * BATCH_SIZE
25
+ end
26
+ end
27
+
28
+ puts "--> Preparing"
29
+ BATCHES.times do |i|
30
+ ROOT.join("batch.#{i}").open("wb:ASCII-8BIT") do |file|
31
+ BATCH_SIZE.times { file.write(EVENT) }
32
+ end
33
+ end
34
+
35
+ processed = 0
36
+ handler = nil
37
+ start = Time.now
38
+ FluQ::Reactor.run do |reactor|
39
+ reactor.listen FluQ::Input::Socket, bind: "tcp://127.0.0.1:8765"
40
+ handler = reactor.register FluQ::Handler::Counter
41
+
42
+ sleep(0.1)
43
+ start = Time.now
44
+ puts "--> Started benchmark"
45
+ BATCHES.times do |i|
46
+ file = ROOT.join("batch.#{i}")
47
+ spawn("nc 127.0.0.1 8765 < #{file}")
48
+ end
49
+ end
50
+
51
+ puts "--> Accepted : #{BATCHES * BATCH_SIZE} in #{(Time.now - start).round(1)}s"
52
+ puts "--> Processed : #{handler.count} events"
data/bin/fluq-rb ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ if File.exists?(File.expand_path('../../lib/fluq.rb', __FILE__))
4
+ $:.unshift(File.expand_path('../../lib', __FILE__))
5
+ end
6
+ require 'fluq/cli'
7
+
8
+ FluQ::CLI.run
@@ -0,0 +1,3 @@
1
+ input :socket do
2
+ bind "tcp://127.0.0.1:6790"
3
+ end
@@ -0,0 +1,5 @@
1
+ input :socket do
2
+ bind "tcp://127.0.0.1:6789"
3
+ end
4
+
5
+ handler :log
data/fluq.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ $:.push File.expand_path("../lib", __FILE__)
4
+ require "fluq/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.required_ruby_version = '>= 1.9.1'
8
+ s.required_rubygems_version = ">= 1.8.0"
9
+
10
+ s.name = File.basename(__FILE__, '.gemspec')
11
+ s.summary = "FluQ"
12
+ s.description = "The minimalistic stream processor"
13
+ s.version = FluQ::VERSION.dup
14
+
15
+ s.authors = ["Black Square Media"]
16
+ s.email = "info@blacksquaremedia.com"
17
+ s.homepage = "https://github.com/bsm/fluq"
18
+
19
+ s.require_path = 'lib'
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.files = `git ls-files`.split("\n")
22
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
+
24
+ s.add_dependency "msgpack", "~> 0.5.0"
25
+ s.add_dependency "eventmachine-le"
26
+ s.add_dependency "oj", ">= 2.0.10"
27
+ s.add_dependency "timed_lru"
28
+
29
+ s.add_development_dependency "rake"
30
+ s.add_development_dependency "bundler"
31
+ s.add_development_dependency "rspec"
32
+ s.add_development_dependency "yard"
33
+ end
data/lib/fluq.rb ADDED
@@ -0,0 +1,50 @@
1
+ require 'pathname'
2
+ require 'uri'
3
+ require 'fileutils'
4
+ require 'securerandom'
5
+ require 'forwardable'
6
+ require 'logger'
7
+ require 'eventmachine'
8
+ require 'msgpack'
9
+ require 'oj'
10
+ require 'timed_lru'
11
+
12
+ module FluQ
13
+ %w'version error mixins'.each do |name|
14
+ require "fluq/#{name}"
15
+ end
16
+
17
+ class << self
18
+
19
+ # @attr_reader [String] env runtime environemnt
20
+ # @attr_reader [Pathname] root project root
21
+ # @attr_reader [Logger] logger the main logger
22
+ attr_reader :env, :root, :logger
23
+
24
+ # @param [Logger] instance the thread-safe logger instance
25
+ def logger=(instance)
26
+ instance.extend(FluQ::Mixins::Logger)
27
+ @logger = instance
28
+ end
29
+
30
+ def init!
31
+ # Detect environment
32
+ @env = ENV['FLUQ_ENV'] || "development"
33
+
34
+ # Set root path
35
+ @root = Pathname.new(ENV['FLUQ_ROOT'] || ".")
36
+
37
+ # Setup logger
38
+ self.logger = ::Logger.new(STDOUT)
39
+ logger.level = ::Logger::INFO if env == "production"
40
+ end
41
+ protected :init!
42
+
43
+ end
44
+
45
+ init!
46
+ end
47
+
48
+ %w'url event reactor handler input buffer feed dsl'.each do |name|
49
+ require "fluq/#{name}"
50
+ end
@@ -0,0 +1,6 @@
1
+ module FluQ::Buffer
2
+ end
3
+
4
+ %w'base file'.each do |name|
5
+ require "fluq/buffer/#{name}"
6
+ end
@@ -0,0 +1,51 @@
1
+ class FluQ::Buffer::Base
2
+ MAX_SIZE = 256 * 1024 * 1024 # 256M
3
+
4
+ # @attr_reader [Hash] config
5
+ attr_reader :config
6
+
7
+ # @param [Hash] options various configuration options
8
+ def initialize(options = {})
9
+ super()
10
+ @config = defaults.merge(options)
11
+ end
12
+
13
+ # @return [String] name identifier
14
+ def name
15
+ @name ||= self.class.name.split("::").last.downcase
16
+ end
17
+
18
+ # @abstract
19
+ # @yield over io object
20
+ # @yieldparam [IO] io
21
+ def drain
22
+ yield StringIO.new
23
+ end
24
+
25
+ # @abstract
26
+ # @return [Integer] the size
27
+ def size
28
+ 0
29
+ end
30
+
31
+ # @return [Boolean] true if size exceeds limit
32
+ def full?
33
+ size >= config[:max_size]
34
+ end
35
+
36
+ # @abstract data writer
37
+ # @param [String] data binary string
38
+ def write(data)
39
+ end
40
+
41
+ # @abstract callback, close buffer
42
+ def close
43
+ end
44
+
45
+ protected
46
+
47
+ def defaults
48
+ { max_size: MAX_SIZE }
49
+ end
50
+
51
+ end
@@ -0,0 +1,68 @@
1
+ class FluQ::Buffer::File < FluQ::Buffer::Base
2
+
3
+ # @attr_reader [File] file instance
4
+ attr_reader :file
5
+
6
+ # @see FluQ::Buffer::Base#initialize
7
+ def initialize(*)
8
+ super
9
+ @file = new_file
10
+ @size = 0
11
+ end
12
+
13
+ # @see FluQ::Buffer::Base#name
14
+ def name
15
+ @name ||= [super, File.basename(file.path)].join("-")
16
+ end
17
+
18
+ # @see FluQ::Buffer::Base#write
19
+ def write(data)
20
+ file.write(data)
21
+ end
22
+
23
+ # @see FluQ::Buffer::Base#size
24
+ def size
25
+ file.size
26
+ end
27
+
28
+ # @see FluQ::Buffer::Base#close
29
+ def close
30
+ file.close unless file.closed?
31
+ File.unlink(file.path) if File.exists?(file.path)
32
+ end
33
+
34
+ # @see FluQ::Buffer::Base#drain
35
+ def drain
36
+ file.close unless file.closed?
37
+ io = File.open(file.path, 'rb', encoding: Encoding::BINARY)
38
+ yield(io)
39
+ ensure
40
+ io.close if io
41
+ end
42
+
43
+ protected
44
+
45
+ def defaults
46
+ super.merge(path: "tmp/buffers")
47
+ end
48
+
49
+ def new_file
50
+ path = nil
51
+ incr = 0
52
+ path = root.join(generate_name(incr+=1)) until path && !path.exist?
53
+ file = path.open("wb", encoding: Encoding::BINARY)
54
+ file.sync = true
55
+ file
56
+ end
57
+
58
+ def root
59
+ @root ||= FluQ.root.join(config[:path]).tap do |full_path|
60
+ FileUtils.mkdir_p full_path.to_s
61
+ end
62
+ end
63
+
64
+ def generate_name(index)
65
+ "fb-#{(Time.now.utc.to_f * 1000).round}.#{index}"
66
+ end
67
+
68
+ end
data/lib/fluq/cli.rb ADDED
@@ -0,0 +1,142 @@
1
+ require "optparse"
2
+ require "fileutils"
3
+ require "socket"
4
+
5
+ module FluQ
6
+ class CLI
7
+ SIGNALS = [ :QUIT, :INT, :TERM, :HUP ]
8
+
9
+ # attr_reader [Hash] options
10
+ attr_reader :options
11
+
12
+ # Runs the CLI
13
+ def self.run
14
+ if BasicSocket.respond_to?(:do_not_reverse_lookup=)
15
+ BasicSocket.do_not_reverse_lookup = true
16
+ end
17
+ new.run
18
+ end
19
+
20
+ # Constructor
21
+ def initialize
22
+ super
23
+
24
+ # Parse options
25
+ @options = {}
26
+ parser.parse!(ARGV)
27
+ end
28
+
29
+ def run
30
+ # Exit if not configured correctly
31
+ unless configured?
32
+ puts parser
33
+ exit
34
+ end
35
+
36
+ # Set the environment
37
+ if options[:env]
38
+ ENV["FLUQ_ENV"] = options[:env]
39
+ end
40
+
41
+ # Boot and add project's lib/ dir to load path
42
+ require 'fluq'
43
+ $LOAD_PATH.unshift FluQ.root.join('lib')
44
+ procline "(starting)"
45
+
46
+ # Setup logger
47
+ if options[:log]
48
+ FileUtils.mkdir_p(File.dirname(options[:log]))
49
+ FluQ.logger = ::Logger.new(options[:log])
50
+ end
51
+ if options[:verbose]
52
+ FluQ.logger.level = ::Logger::DEBUG
53
+ end
54
+
55
+ # Write PID file
56
+ @pidfile = options[:pidfile] || FluQ.root.join("tmp", "pids", "fluq.pid")
57
+ FileUtils.mkdir_p(File.dirname(@pidfile))
58
+ File.open(@pidfile, "w") {|f| f.write Process.pid }
59
+
60
+ # Trap signals
61
+ SIGNALS.each do |signal|
62
+ trap(signal) {|*| shutdown! }
63
+ end
64
+
65
+ # Start
66
+ log "Starting FluQ #{FluQ::VERSION} (#{FluQ.env})"
67
+ FluQ::Reactor.run do |reactor|
68
+ FluQ::DSL.new(reactor, options[:config]).run
69
+ procline
70
+ end
71
+ end
72
+
73
+ # @return [Boolean] true if configured
74
+ def configured?
75
+ options[:config] && File.file?(options[:config])
76
+ end
77
+
78
+ private
79
+
80
+ # Shut down
81
+ def shutdown!
82
+ FluQ.logger.info "Shutting down FluQ" if defined?(FluQ)
83
+ FileUtils.rm_f(@pidfile) if @pidfile
84
+ exit
85
+ end
86
+
87
+ def log(message)
88
+ FluQ.logger.info(message)
89
+ end
90
+
91
+ def procline(message = nil)
92
+ $0 = ["fluq-rb", FluQ::VERSION, message].compact.join(" ")
93
+ end
94
+
95
+ def parser
96
+ @parser ||= OptionParser.new do |o|
97
+ o.banner = "Usage: #{File.basename($0)} [OPTIONS]"
98
+
99
+ o.separator ""
100
+ o.separator "Required:"
101
+
102
+ o.on "-C", "--config FILE", "Use this config file" do |val|
103
+ @options[:config] = val
104
+ end
105
+
106
+ o.separator ""
107
+ o.separator "Optional:"
108
+
109
+ o.on("-e", "--environment ENV", "Runtime environment (default: development)") do |val|
110
+ @options[:env] = val
111
+ end
112
+
113
+ o.on("-l", "--log FILE", "File to log to (default: STDOUT)") do |val|
114
+ @options[:log] = val
115
+ end
116
+
117
+ o.on("-v", "--verbose", "Use verbose output") do |val|
118
+ @options[:verbose] = true
119
+ end
120
+
121
+ o.separator ""
122
+
123
+ o.on "--pidfile FILE", "Path to pidfile (default: tmp/pids/fluq.pid)" do |val|
124
+ @options[:pidfile] = val
125
+ end
126
+
127
+ o.separator ""
128
+
129
+ o.on("-h", "--help", "Show this message") do
130
+ puts o
131
+ exit
132
+ end
133
+
134
+ o.on("-V", "--version", "Show version") do
135
+ puts FluQ::VERSION
136
+ exit
137
+ end
138
+ end
139
+ end
140
+
141
+ end
142
+ end