teplohod 0.0.1.alpha1

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 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