teplohod 0.0.1.alpha1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0d50cedb72cdebc67d5e1f53545099061783de7d
4
+ data.tar.gz: 5af88cd316675cc4f24419de22a01b776500ff92
5
+ SHA512:
6
+ metadata.gz: 06ff005b1d8c27575757b9cf6e48aed4b0137ec2caf0959e2b8b13b8b2e9c9fec4e70bd5a10d821de694929f906d310d07f02a677b9511b7a7b0a58ea9382b22
7
+ data.tar.gz: e07d47c681221a83639ba9b7a0d0738e7644e1e3fbbee1230611b3ab73ce9320fc47aaf33951cb377d1e532c3402a95a3dcf3411427ff79a78a2fafd7571a669
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rubocop.yml ADDED
@@ -0,0 +1,3 @@
1
+ AllCops:
2
+ Exclude:
3
+ - 'dummy/**/*'
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.13.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in scrap.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Teplohod
2
+
3
+ run example with `teplohod -r ./example/main.rb -w -v`.
4
+
5
+ dashboard will work on `4567` port.
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+ require 'rubocop/rake_task'
4
+ require 'yard'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['test/**/*_test.rb']
10
+ end
11
+
12
+ RuboCop::RakeTask.new do |task|
13
+ task.fail_on_error = false
14
+ task.formatters = %w(simple)
15
+ end
16
+
17
+ YARD::Rake::YardocTask.new do |t|
18
+ t.files = ['lib/**/*.rb']
19
+ end
20
+
21
+ task default: %i(rubocop test yard)
data/bin/teplohod ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/teplohod/cli'
4
+
5
+ begin
6
+ cli = Teplohod::CLI.instance
7
+ cli.prepare ARGV
8
+ cli.run
9
+ rescue => e
10
+ raise e if $DEBUG
11
+ STDERR.puts e.message
12
+ STDERR.puts e.backtrace.join("\n")
13
+ exit 1
14
+ end
data/example/main.rb ADDED
@@ -0,0 +1,50 @@
1
+ require 'teplohod'
2
+ require 'teplohod/queue/memory/base'
3
+
4
+ class StartQueue < Teplohod::Queue::Memory::Base
5
+ end
6
+
7
+ class ResultQueue < Teplohod::Queue::Memory::Base
8
+ end
9
+
10
+ class InitialPipeline < Teplohod::Pipeline::Base
11
+ self.queue = StartQueue.new
12
+
13
+ def name
14
+ "Initial Pipeline"
15
+ end
16
+
17
+ def desription
18
+ "Initial data there"
19
+ end
20
+
21
+ def on_empty_load
22
+ 10.times do
23
+ queue.push rand(100)
24
+ end
25
+ end
26
+
27
+ def on_finish
28
+ on_empty_load
29
+ end
30
+ end
31
+
32
+ class ResultPipeline < Teplohod::Pipeline::Base
33
+ self.parent = InitialPipeline.new
34
+ self.queue = ResultQueue.new
35
+
36
+ def name
37
+ "Result Pipeline"
38
+ end
39
+
40
+ def result
41
+ "Result data there"
42
+ end
43
+ end
44
+
45
+ class DemoProvider < Teplohod::Provider::Base
46
+ self.tail = ResultPipeline.new
47
+ end
48
+
49
+ Teplohod::Manager.reload_providers!
50
+ Teplohod::Manager.register_provider DemoProvider.new
@@ -0,0 +1,63 @@
1
+ $stdout.sync = true
2
+
3
+ require 'singleton'
4
+ require 'optparse'
5
+ require 'logger'
6
+
7
+ require 'teplohod/utils'
8
+ require 'teplohod/initializers'
9
+ require 'teplohod/signal_handler'
10
+ require 'teplohod'
11
+
12
+ module Teplohod
13
+ class CLI
14
+ include Singleton
15
+ include Utils
16
+
17
+ def prepare(_args = ARGV)
18
+ Teplohod::Initializers::Options.run!
19
+ Teplohod::Initializers::Redis.run!
20
+ Teplohod::Initializers::Logger.run! unless options[:daemonize]
21
+ Teplohod::Initializers::Daemonizer.run! if options[:daemonize]
22
+ Teplohod::Initializers::Pid.run!
23
+ end
24
+
25
+ def run
26
+ Teplohod::Initializers::Boot.run!
27
+ Teplohod::Initializers::Web.run! if options[:web]
28
+ Teplohod::Initializers::Banner.run!
29
+
30
+ self_read, self_write = IO.pipe
31
+
32
+ %w(INT TERM USR1 USR2 TTIN).each do |sig|
33
+ begin
34
+ trap sig do
35
+ self_write.puts(sig)
36
+ end
37
+ rescue ArgumentError
38
+ puts "Signal #{sig} not supported"
39
+ end
40
+ end
41
+
42
+ require 'teplohod/launcher'
43
+ launcher = Teplohod::Launcher.new
44
+
45
+ begin
46
+ launcher.run
47
+
48
+ while readable_io = IO.select([self_read])
49
+ signal = readable_io.first[0].gets.strip
50
+ Teplohod::SignalHandler.handle(signal)
51
+ end
52
+ rescue Interrupt
53
+ logger.tagged('system') { logger.info 'Shutting down' }
54
+ launcher.stop
55
+ logger.tagged('system') { logger.info 'Bye!' }
56
+ exit 0
57
+ end
58
+ end
59
+
60
+ def handle_signal(sig)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,31 @@
1
+ module Teplohod
2
+ module Initializers
3
+ class Banner
4
+ include Initializable
5
+
6
+ def run
7
+ puts "\e[32m"
8
+ puts banner
9
+ puts "\e[0m"
10
+ end
11
+
12
+ private
13
+
14
+ # TODO: Add teplohod
15
+ def banner
16
+ %(
17
+ Welcome to #{Teplohod::NAME}!
18
+ Current version: #{Teplohod::VERSION}
19
+
20
+ Options:
21
+ Environment: #{environment}
22
+ Mount Point: #{options[:require]}
23
+ Log Level: #{logger.level}
24
+ Pid File Path: #{options[:pidfile]}
25
+ Daemonize: #{options[:daemonize]}
26
+ Web: #{options[:web]}
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ module Teplohod
2
+ class NoRequiredFile < ArgumentError
3
+ end
4
+
5
+ module Initializers
6
+ class Boot
7
+ include Initializable
8
+
9
+ def run
10
+ raise NoRequiredFile, "#{options[:require]} does not exist" unless File.exist?(options[:require])
11
+ require options[:require]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,51 @@
1
+ require 'teplohod/initializers/logger'
2
+
3
+ module Teplohod
4
+ class NoLogfileError < ArgumentError
5
+ end
6
+
7
+ module Initializers
8
+ class Daemonizer
9
+ include Initializable
10
+
11
+ def run
12
+ validate!
13
+
14
+ files_to_reopen = []
15
+ ObjectSpace.each_object(File) do |file|
16
+ files_to_reopen << file unless file.closed?
17
+ end
18
+
19
+ ::Process.daemon(true, true)
20
+
21
+ files_to_reopen.each do |file|
22
+ begin
23
+ file.reopen file.path, 'a+'
24
+ file.sync = true
25
+ rescue ::Exception
26
+ end
27
+ end
28
+
29
+ [$stdout, $stderr].each do |io|
30
+ File.open(options[:logfile], 'ab') do |f|
31
+ io.reopen(f)
32
+ end
33
+ io.sync = true
34
+ end
35
+ $stdin.reopen('/dev/null')
36
+
37
+ Teplohod::Initializers::Logger.run!
38
+ end
39
+
40
+ private
41
+
42
+ def logfile?
43
+ options[:logfile]
44
+ end
45
+
46
+ def validate!
47
+ raise NoLogfileError, "You should specify a logfile if you're going to daemonize" unless logfile?
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,14 @@
1
+ require 'teplohod/logging'
2
+
3
+ module Teplohod
4
+ module Initializers
5
+ class Logger
6
+ include Initializable
7
+
8
+ def run
9
+ Teplohod::Logging.initialize_logger(options[:logfile]) if options[:logfile]
10
+ Teplohod.logger.level = ::Logger::DEBUG if options[:verbose]
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,57 @@
1
+ require 'optparse'
2
+
3
+ require 'teplohod'
4
+
5
+ module Teplohod
6
+ module Initializers
7
+ class Options
8
+ include Initializable
9
+
10
+ def self.run!
11
+ new.run
12
+ end
13
+
14
+ def run
15
+ cli_options = parse
16
+ Teplohod.options = options.merge cli_options
17
+ puts "Start #{Teplohod::NAME} with #{options}"
18
+ end
19
+
20
+ private
21
+
22
+ def parse
23
+ opts = {}
24
+ parser = ::OptionParser.new do |o|
25
+ o.on '-d', '--daemon', 'Daemonize process' do |arg|
26
+ opts[:daemonize] = arg
27
+ end
28
+ o.on '-r', '--require [PATH|DIR]', 'Location to require' do |arg|
29
+ opts[:require] = arg
30
+ end
31
+ o.on '-e', '--environment ENV', 'Application environment' do |arg|
32
+ opts[:environment] = arg
33
+ end
34
+ o.on '-v', '--verbose', 'Print more verbose output' do |arg|
35
+ opts[:verbose] = arg
36
+ end
37
+ o.on '-L', '--logfile PATH', 'path to writable logfile' do |arg|
38
+ opts[:logfile] = arg
39
+ end
40
+ o.on '-P', '--pidfile PATH', 'path to pidfile' do |arg|
41
+ opts[:pidfile] = arg
42
+ end
43
+ o.on '-w', '--web', 'run web dashboard' do |arg|
44
+ opts[:web] = arg
45
+ end
46
+ end
47
+ parser.banner = 'teplohod [options]'
48
+ parser.on_tail '-h', '--help', 'Show help' do
49
+ puts parser.to_s
50
+ exit 1
51
+ end
52
+ parser.parse! ARGV
53
+ opts
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,16 @@
1
+ module Teplohod
2
+ module Initializers
3
+ class Pid
4
+ include Initializable
5
+
6
+ def run
7
+ if path = options[:pidfile]
8
+ pidfile = File.expand_path(path)
9
+ File.open(pidfile, 'w') do |f|
10
+ f.puts ::Process.pid
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ require 'redis'
2
+
3
+ module Teplohod
4
+ module Initializers
5
+ class Redis
6
+ include Initializable
7
+
8
+ def run
9
+ # TODO: add configuration
10
+ Teplohod.redis= ::Redis.new
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ require 'teplohod/web'
2
+
3
+ module Teplohod
4
+ module Initializers
5
+ class Web
6
+ include Initializable
7
+
8
+ def run
9
+ fork { Teplohod::Web.run! }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_support/concern'
2
+
3
+ require 'teplohod/utils'
4
+
5
+ module Teplohod
6
+ module Initializable
7
+ extend ActiveSupport::Concern
8
+ include Utils
9
+
10
+ module ClassMethods
11
+ def run!
12
+ new.run
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ require 'teplohod/initializers/options'
19
+ require 'teplohod/initializers/banner'
20
+ require 'teplohod/initializers/logger'
21
+ require 'teplohod/initializers/daemonizer'
22
+ require 'teplohod/initializers/pid'
23
+ require 'teplohod/initializers/redis'
24
+ require 'teplohod/initializers/boot'
25
+ require 'teplohod/initializers/web'
@@ -0,0 +1,28 @@
1
+ require 'teplohod/utils'
2
+
3
+ module Teplohod
4
+ class Launcher
5
+ include Utils
6
+
7
+ def initialize
8
+ @providers = Teplohod::Manager.providers
9
+ end
10
+
11
+ def run
12
+ log { logger.info { 'Running..' } }
13
+ @providers.each(&:run)
14
+ end
15
+
16
+ def stop
17
+ log { logger.info { 'Stopping..' } }
18
+ end
19
+
20
+ private
21
+
22
+ def log
23
+ logger.tagged 'system' do
24
+ yield
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+ require 'time'
3
+ require 'logger'
4
+ require 'fcntl'
5
+ require 'active_support/tagged_logging'
6
+
7
+ module Teplohod
8
+ module Logging
9
+ class Formatter
10
+ include ActiveSupport::TaggedLogging::Formatter
11
+
12
+ def call(severity, time, _program_name, message)
13
+ tags = current_tags.join('/')
14
+ "#{time.utc.iso8601(3)} #{::Process.pid} TID-#{Thread.current.object_id.to_s(36)} #{severity}: [#{tags}] #{message}\n"
15
+ end
16
+ end
17
+
18
+ def self.initialize_logger(log_target = STDOUT)
19
+ oldlogger = defined?(@logger) ? @logger : nil
20
+ @logger = ActiveSupport::TaggedLogging.new(Logger.new(log_target))
21
+ @logger.formatter = Formatter.new
22
+ @logger.level = Logger::INFO
23
+ oldlogger.close if oldlogger
24
+ @logger
25
+ end
26
+
27
+ def self.logger
28
+ defined?(@logger) ? @logger : initialize_logger
29
+ end
30
+
31
+ def self.logger=(log)
32
+ @logger = (log ? log : Logger.new(File::NULL))
33
+ end
34
+
35
+ # This reopens ALL logfiles in the process that have been rotated
36
+ # using logrotate(8) (without copytruncate) or similar tools.
37
+ # A +File+ object is considered for reopening if it is:
38
+ # 1) opened with the O_APPEND and O_WRONLY flags
39
+ # 2) the current open file handle does not match its original open path
40
+ # 3) unbuffered (as far as userspace buffering goes, not O_SYNC)
41
+ # Returns the number of files reopened
42
+ def self.reopen_logs
43
+ to_reopen = []
44
+ append_flags = File::WRONLY | File::APPEND
45
+
46
+ ObjectSpace.each_object(File) do |fp|
47
+ begin
48
+ if !fp.closed? && fp.stat.file? && fp.sync && (fp.fcntl(Fcntl::F_GETFL) & append_flags) == append_flags
49
+ to_reopen << fp
50
+ end
51
+ rescue IOError, Errno::EBADF
52
+ end
53
+ end
54
+
55
+ nr = 0
56
+ to_reopen.each do |fp|
57
+ orig_st = begin
58
+ fp.stat
59
+ rescue IOError, Errno::EBADF
60
+ next
61
+ end
62
+
63
+ begin
64
+ b = File.stat(fp.path)
65
+ next if orig_st.ino == b.ino && orig_st.dev == b.dev
66
+ rescue Errno::ENOENT
67
+ end
68
+
69
+ begin
70
+ File.open(fp.path, 'a') { |tmpfp| fp.reopen(tmpfp) }
71
+ fp.sync = true
72
+ nr += 1
73
+ rescue IOError, Errno::EBADF
74
+ # not much we can do...
75
+ end
76
+ end
77
+ nr
78
+ rescue RuntimeError => ex
79
+ # RuntimeError: ObjectSpace is disabled; each_object will only work with Class, pass -X+O to enable
80
+ puts "Unable to reopen logs: #{ex.message}"
81
+ end
82
+
83
+ def logger
84
+ Teplohod::Logging.logger
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,19 @@
1
+ require 'teplohod/stats'
2
+
3
+ module Teplohod
4
+ class Manager
5
+ class << self
6
+ def providers
7
+ @providers ||= []
8
+ end
9
+
10
+ def register_provider(provider)
11
+ providers << provider
12
+ end
13
+
14
+ def reload_providers!
15
+ @providers = []
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,89 @@
1
+ require 'teplohod/utils'
2
+ require 'teplohod/processor/equality'
3
+
4
+ module Teplohod
5
+ module Pipeline
6
+ class Base
7
+ include Utils
8
+ attr_reader :key, :timeout
9
+ attr_accessor :provider
10
+
11
+ def initialize(key: self.class.to_s.underscore, timeout: 5, loop: true)
12
+ @key = key
13
+ @timeout = timeout
14
+ @loop = loop
15
+ end
16
+
17
+ def parent
18
+ @parent ||= self.class.parent
19
+ end
20
+
21
+ def queue
22
+ @queue ||= self.class.queue
23
+ end
24
+
25
+ def processor
26
+ @processor = @processor || self.class.processor || Teplohod::Processor::Equality.new
27
+ end
28
+
29
+ def run
30
+ on_empty_load if queue.empty?
31
+
32
+ return if parent.nil?
33
+ threads thread_name do
34
+ log { logger.info { 'loop started' } }
35
+ loop do
36
+ while element = parent.queue.pop
37
+ queue.push process(element)
38
+ Teplohod::Stats.increment(:processed_jobs, self)
39
+ end
40
+ log { logger.warn { "parent queue is clear, sleep #{timeout} sec"} }
41
+ parent.on_finish
42
+ break unless @loop
43
+ sleep timeout
44
+ end
45
+ end
46
+ end
47
+
48
+ def name
49
+ "#{self.class}#name is not implemented"
50
+ end
51
+
52
+ def description
53
+ "#{self.class}#description is not implemented"
54
+ end
55
+
56
+ protected
57
+
58
+ def process(data)
59
+ result = processor.process data
60
+ log { logger.info { "processed #{data}" } }
61
+ result
62
+ end
63
+
64
+ def on_finish
65
+ log { logger.warn { '#on_finish does nothing' } }
66
+ end
67
+
68
+ def on_empty_load
69
+ log { logger.warn { '#on_empty_load does nothing' } }
70
+ end
71
+
72
+ private
73
+
74
+ def thread_name
75
+ [provider&.key, key].join('/')
76
+ end
77
+
78
+ class << self
79
+ attr_accessor :parent, :queue, :processor
80
+ end
81
+
82
+ def log
83
+ logger.tagged provider&.key, key do
84
+ yield
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,9 @@
1
+ module Teplohod
2
+ module Processor
3
+ class Base
4
+ def process(_input)
5
+ raise NotImplemented
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ require 'teplohod/processor/base'
2
+
3
+ module Teplohod
4
+ module Processor
5
+ class Equality < Base
6
+ def process(input)
7
+ input
8
+ end
9
+ end
10
+ end
11
+ end