processor 0.0.1 → 1.0.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +4 -0
- data/README.md +46 -24
- data/example/migration.rb +3 -14
- data/lib/processor/data/array_processor.rb +11 -0
- data/lib/processor/data/batch_processor.rb +37 -0
- data/lib/processor/data/csv_processor.rb +39 -0
- data/lib/processor/data/null_processor.rb +26 -0
- data/lib/processor/data/solr_pages_processor.rb +30 -0
- data/lib/processor/data/solr_processor.rb +23 -0
- data/lib/processor/observer/logger.rb +3 -3
- data/lib/processor/process_runner/successive.rb +12 -0
- data/lib/processor/process_runner/threads.rb +51 -0
- data/lib/processor/runner.rb +4 -5
- data/lib/processor/thread.rb +7 -7
- data/lib/processor/version.rb +1 -1
- data/lib/processor.rb +13 -0
- data/processor.gemspec +3 -2
- data/spec/processor/data/array_processor_spec.rb +11 -0
- data/spec/processor/data/batch_processor_spec.rb +38 -0
- data/spec/processor/data/null_processor_spec.rb +9 -0
- data/spec/processor/process_runner/specs.rb +50 -0
- data/spec/processor/process_runner/successive_spec.rb +7 -0
- data/spec/processor/process_runner/threads_spec.rb +25 -0
- data/spec/processor/runner_spec.rb +10 -41
- data/spec/processor/thread_spec.rb +3 -3
- metadata +30 -24
- data/example/solr_migration.rb +0 -31
- data/example/solr_pages_migration.rb +0 -48
- data/lib/processor/data_processor.rb +0 -28
- data/lib/processor/records_processor/successive.rb +0 -20
- data/lib/processor/records_processor/threads.rb +0 -27
- data/spec/processor/data_processor_spec.rb +0 -20
- data/spec/processor/records_processor/specs.rb +0 -38
- data/spec/processor/records_processor/successive_spec.rb +0 -7
- data/spec/processor/records_processor/threads_spec.rb +0 -8
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -3,7 +3,10 @@ Processor
|
|
3
3
|
[![Build Status](https://travis-ci.org/AlexParamonov/processor.png?branch=master)](http://travis-ci.org/AlexParamonov/processor)
|
4
4
|
[![Gemnasium Build Status](https://gemnasium.com/AlexParamonov/processor.png)](http://gemnasium.com/AlexParamonov/processor)
|
5
5
|
|
6
|
-
|
6
|
+
Processor could execute any `DataProcessor` you specify and log entire process using any number of loggers you need.
|
7
|
+
You may add own observers for monitoring background tasks on even send an email to bussiness with generated report.
|
8
|
+
Processor provide customisation for almost every part of it.
|
9
|
+
|
7
10
|
|
8
11
|
Contents
|
9
12
|
---------
|
@@ -41,42 +44,61 @@ Requirements
|
|
41
44
|
|
42
45
|
Usage
|
43
46
|
------------
|
44
|
-
1. Implement a `DataProcessor`. See `Processor::Example::Migration
|
47
|
+
1. Implement a `DataProcessor`. See `Processor::Example::Migration`
|
48
|
+
and `processor/data` directory. CSV and Solr data processors are usabe
|
49
|
+
but not yet finished and tested.
|
45
50
|
1. Run your `DataProcessor`:
|
46
51
|
|
47
52
|
``` ruby
|
53
|
+
data_processor = UserLocationMigration.new
|
48
54
|
thread = Processor::Thread.new data_processor
|
49
55
|
thread.run_successive
|
50
56
|
```
|
51
|
-
See `spec/processor/thread_spec.rb` and `spec/example_spec.rb` and
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
Processor::Thread
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
should respond to total_records
|
66
|
-
should respond to process
|
67
|
-
|
68
|
-
Processor::Observer::Logger
|
69
|
-
accepts logger builder as parameter
|
70
|
-
accepts logger as parameter
|
71
|
-
use ruby Logger if no external logger provided
|
57
|
+
See `spec/processor/thread_spec.rb` and `spec/example_spec.rb` and
|
58
|
+
`example` directory for other usage examples.
|
59
|
+
|
60
|
+
|
61
|
+
It is recomended to wrap Processor::Thread in your classes like:
|
62
|
+
|
63
|
+
```
|
64
|
+
WeeklyReport
|
65
|
+
TaxonomyMigration
|
66
|
+
UserDataImport
|
67
|
+
```
|
68
|
+
to hide configuration of observers or use your own API to run
|
69
|
+
migrations:
|
70
|
+
|
72
71
|
```
|
72
|
+
weekly_report.create_and_deliver
|
73
|
+
user_data_import.import_from_csv(file)
|
74
|
+
etc.
|
75
|
+
```
|
76
|
+
|
77
|
+
Sure, it is possible to use it raw, but please dont fear to add a
|
78
|
+
wrapper class for it:
|
79
|
+
|
80
|
+
```
|
81
|
+
csv_data_processor = Processor::Data::CsvProcessor.new file
|
82
|
+
stdout_notifier = Processor::Observer::Logger.new(Logger.new(STDOUT))
|
83
|
+
logger_observer = Processor::Observer::Logger.new
|
84
|
+
Processor::Thread.new(
|
85
|
+
csv_data_processor,
|
86
|
+
stdout_notifier,
|
87
|
+
logger_observer,
|
88
|
+
email_notification_observer
|
89
|
+
).run_in_threads 5
|
90
|
+
```
|
91
|
+
|
92
|
+
### Observers
|
93
|
+
Observers should respond to `update` method but if you inherit from
|
94
|
+
`Processor::Observers::NullObserver` you'll get a bunch of methods to
|
95
|
+
use. See `Processor::Observers::Logger` for example.
|
73
96
|
|
74
97
|
Compatibility
|
75
98
|
-------------
|
76
99
|
tested with Ruby
|
77
100
|
|
78
101
|
* 1.9.3
|
79
|
-
* jruby-19mode
|
80
102
|
* rbx-19mode
|
81
103
|
* ruby-head
|
82
104
|
|
data/example/migration.rb
CHANGED
@@ -1,29 +1,18 @@
|
|
1
|
-
require 'processor/
|
1
|
+
require 'processor/data/array_processor'
|
2
2
|
|
3
3
|
module Processor
|
4
4
|
module Example
|
5
|
-
class Migration <
|
5
|
+
class Migration < Data::ArrayProcessor
|
6
6
|
attr_reader :records
|
7
|
+
|
7
8
|
def initialize(records)
|
8
9
|
@records = records
|
9
10
|
end
|
10
11
|
|
11
|
-
def done?(records)
|
12
|
-
records.count < 1
|
13
|
-
end
|
14
|
-
|
15
12
|
def process(record)
|
16
13
|
record.do_something
|
17
14
|
"OK"
|
18
15
|
end
|
19
|
-
|
20
|
-
def fetch_records
|
21
|
-
records.shift(2)
|
22
|
-
end
|
23
|
-
|
24
|
-
def total_records
|
25
|
-
records.count
|
26
|
-
end
|
27
16
|
end
|
28
17
|
end
|
29
18
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative 'null_processor'
|
2
|
+
|
3
|
+
module Processor
|
4
|
+
module Data
|
5
|
+
class BatchProcessor < NullProcessor
|
6
|
+
def initialize(batch_size = 10)
|
7
|
+
@batch_size = batch_size
|
8
|
+
end
|
9
|
+
|
10
|
+
def records
|
11
|
+
Enumerator.new do |result|
|
12
|
+
loop do
|
13
|
+
fetch_batch.each do |record|
|
14
|
+
result << record
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def fetch_batch
|
21
|
+
@fetcher ||= query.each_slice(batch_size)
|
22
|
+
@fetcher.next
|
23
|
+
end
|
24
|
+
|
25
|
+
def total_records
|
26
|
+
@total_records ||= query.count
|
27
|
+
end
|
28
|
+
|
29
|
+
def query
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
attr_reader :batch_size
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative 'batch_processor'
|
2
|
+
|
3
|
+
module Processor
|
4
|
+
module Data
|
5
|
+
class CsvProcessor < BatchProcessor
|
6
|
+
def initialize(file, csv_options = {})
|
7
|
+
@file = file
|
8
|
+
@separator = separator
|
9
|
+
@csv_options = {
|
10
|
+
col_sep: ";",
|
11
|
+
headers: true,
|
12
|
+
}.merge csv_options
|
13
|
+
end
|
14
|
+
|
15
|
+
def process(row)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def records
|
20
|
+
Enumerator.new do |result|
|
21
|
+
::CSV.foreach(file, csv_options) do |record|
|
22
|
+
result << record
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def total_records
|
28
|
+
@total_records ||= File.new(file).readlines.size
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
attr_reader :file, :separator, :csv_options
|
33
|
+
|
34
|
+
def fetch_field(field_name, row)
|
35
|
+
row[field_name].to_s.strip
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Processor
|
2
|
+
module Data
|
3
|
+
class NullProcessor
|
4
|
+
def process(record)
|
5
|
+
# do nothing
|
6
|
+
end
|
7
|
+
|
8
|
+
def records
|
9
|
+
[]
|
10
|
+
end
|
11
|
+
|
12
|
+
def total_records
|
13
|
+
@total_records ||= records.count
|
14
|
+
end
|
15
|
+
|
16
|
+
def name
|
17
|
+
# underscore a class name
|
18
|
+
self.class.name.to_s.
|
19
|
+
gsub(/::/, '_').
|
20
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
21
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
22
|
+
downcase
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative "batch_processor"
|
2
|
+
|
3
|
+
module Processor
|
4
|
+
module Data
|
5
|
+
class SolrPagesProcessor < BatchProcessor
|
6
|
+
def process(record)
|
7
|
+
raise NotImplementedError
|
8
|
+
end
|
9
|
+
|
10
|
+
def query(requested_page)
|
11
|
+
raise NotImplementedError
|
12
|
+
end
|
13
|
+
|
14
|
+
def fetch_batch
|
15
|
+
query(next_page).results
|
16
|
+
end
|
17
|
+
|
18
|
+
def total_records
|
19
|
+
@total_records ||= query(1).total
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def next_page
|
24
|
+
@page ||= 0
|
25
|
+
@page += 1
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require_relative "batch_processor"
|
2
|
+
|
3
|
+
module Processor
|
4
|
+
module Data
|
5
|
+
class SolrProcessor < BatchProcessor
|
6
|
+
def process(record)
|
7
|
+
raise NotImplementedError
|
8
|
+
end
|
9
|
+
|
10
|
+
def query
|
11
|
+
raise NotImplementedError
|
12
|
+
end
|
13
|
+
|
14
|
+
def fetch_batch
|
15
|
+
query.results
|
16
|
+
end
|
17
|
+
|
18
|
+
def total_records
|
19
|
+
@total_records ||= query.total
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -26,7 +26,7 @@ module Processor
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def after_record_processing(record, result)
|
29
|
-
logger.info "
|
29
|
+
logger.info "Processed #{id_for record}: #{result}"
|
30
30
|
end
|
31
31
|
|
32
32
|
def processing_finished(processor)
|
@@ -39,7 +39,7 @@ module Processor
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def processing_error(processor, exception)
|
42
|
-
logger.fatal "Processing #{processor.name}
|
42
|
+
logger.fatal "Processing #{processor.name} FAILED: #{exception.backtrace}"
|
43
43
|
end
|
44
44
|
|
45
45
|
private
|
@@ -79,7 +79,7 @@ module Processor
|
|
79
79
|
return record[method.to_s] if record.key? method.to_s
|
80
80
|
end if record.respond_to?(:key?) && record.respond_to?(:[])
|
81
81
|
|
82
|
-
record.to_s
|
82
|
+
record.to_s.strip
|
83
83
|
end
|
84
84
|
end
|
85
85
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Processor
|
2
|
+
module ProcessRunner
|
3
|
+
class Threads
|
4
|
+
def initialize(number_of_threads = 1)
|
5
|
+
@number_of_threads = number_of_threads
|
6
|
+
@threads = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(processor, events, recursion_preventer)
|
10
|
+
join_threads
|
11
|
+
|
12
|
+
begin
|
13
|
+
processor.records.each do |record|
|
14
|
+
recursion_preventer.call
|
15
|
+
if threads_created >= number_of_threads then join_threads end
|
16
|
+
|
17
|
+
new_thread(processor, record) do |thread_data_processor, thread_record|
|
18
|
+
begin
|
19
|
+
events.register :before_record_processing, thread_record
|
20
|
+
|
21
|
+
result = thread_data_processor.process(thread_record)
|
22
|
+
|
23
|
+
events.register :after_record_processing, thread_record, result
|
24
|
+
rescue StandardError => exception
|
25
|
+
events.register :record_processing_error, thread_record, exception
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
ensure # join already created threads
|
31
|
+
join_threads
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
attr_reader :threads_created, :number_of_threads
|
37
|
+
|
38
|
+
def new_thread(processor, record, &block)
|
39
|
+
@threads << ::Thread.new(processor, record, &block)
|
40
|
+
@threads_created += 1
|
41
|
+
end
|
42
|
+
|
43
|
+
def join_threads
|
44
|
+
@threads.each(&:join)
|
45
|
+
@threads_created = 0
|
46
|
+
@threads = []
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/processor/runner.rb
CHANGED
@@ -7,11 +7,10 @@ module Processor
|
|
7
7
|
@events = events_registrator
|
8
8
|
end
|
9
9
|
|
10
|
-
def run(
|
10
|
+
def run(process_runner)
|
11
11
|
events.register :processing_started, processor
|
12
|
-
|
13
|
-
|
14
|
-
end
|
12
|
+
|
13
|
+
process_runner.call processor, events, method(:recursion_preventer)
|
15
14
|
|
16
15
|
events.register :processing_finished, processor
|
17
16
|
rescue Exception => exception
|
@@ -33,7 +32,7 @@ module Processor
|
|
33
32
|
end
|
34
33
|
|
35
34
|
def max_records_to_process
|
36
|
-
(processor.total_records * 1.1).round + 10
|
35
|
+
@max_records_to_process ||= (processor.total_records * 1.1).round + 10
|
37
36
|
end
|
38
37
|
end
|
39
38
|
end
|
data/lib/processor/thread.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'processor/runner'
|
2
|
-
require 'processor/
|
3
|
-
require 'processor/
|
2
|
+
require 'processor/process_runner/successive'
|
3
|
+
require 'processor/process_runner/threads'
|
4
4
|
|
5
5
|
module Processor
|
6
6
|
class Thread
|
@@ -8,16 +8,16 @@ module Processor
|
|
8
8
|
@runner = Runner.new data_processor, EventsRegistrator.new(observers)
|
9
9
|
end
|
10
10
|
|
11
|
-
def run_as(&
|
12
|
-
runner.run
|
11
|
+
def run_as(&process_runner)
|
12
|
+
runner.run process_runner
|
13
13
|
end
|
14
14
|
|
15
15
|
def run_successive
|
16
|
-
runner.run
|
16
|
+
runner.run ProcessRunner::Successive.new
|
17
17
|
end
|
18
18
|
|
19
|
-
def run_in_threads
|
20
|
-
runner.run
|
19
|
+
def run_in_threads(number_of_threads = 2)
|
20
|
+
runner.run ProcessRunner::Threads.new number_of_threads
|
21
21
|
end
|
22
22
|
|
23
23
|
private
|
data/lib/processor/version.rb
CHANGED
data/lib/processor.rb
CHANGED
@@ -1,4 +1,17 @@
|
|
1
1
|
require "processor/version"
|
2
2
|
|
3
|
+
require "processor/data/array_processor"
|
4
|
+
require "processor/data/batch_processor"
|
5
|
+
require "processor/data/null_processor"
|
6
|
+
|
7
|
+
require "processor/observer/logger"
|
8
|
+
require "processor/observer/null_observer"
|
9
|
+
|
10
|
+
require "processor/process_runner/successive"
|
11
|
+
require "processor/process_runner/threads"
|
12
|
+
|
13
|
+
require "processor/runner"
|
14
|
+
require "processor/thread"
|
15
|
+
|
3
16
|
module Processor
|
4
17
|
end
|
data/processor.gemspec
CHANGED
@@ -8,8 +8,9 @@ Gem::Specification.new do |gem|
|
|
8
8
|
gem.version = Processor::VERSION
|
9
9
|
gem.authors = ["Alexander Paramonov"]
|
10
10
|
gem.email = ["alexander.n.paramonov@gmail.com"]
|
11
|
-
gem.summary = %q{
|
12
|
-
gem.description = %q{
|
11
|
+
gem.summary = %q{Universal processor for data migration and reports generation.}
|
12
|
+
gem.description = %q{Processor could execute any DataProcessor you specify and log entire process.
|
13
|
+
You may add own observers for monitoring background tasks on even send email to bussiness with generated report.}
|
13
14
|
gem.homepage = "http://github.com/AlexParamonov/processor"
|
14
15
|
gem.license = "MIT"
|
15
16
|
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'spec_helper_lite'
|
2
|
+
require 'processor/data/array_processor'
|
3
|
+
|
4
|
+
describe Processor::Data::ArrayProcessor do
|
5
|
+
it "should have total records count equals to count of records" do
|
6
|
+
records = *1..5
|
7
|
+
subject.stub(records: records)
|
8
|
+
subject.total_records.should eq 5
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "spec_helper_lite"
|
2
|
+
|
3
|
+
require "processor/data/batch_processor"
|
4
|
+
|
5
|
+
describe Processor::Data::BatchProcessor do
|
6
|
+
it "should create and process records by batch" do
|
7
|
+
processor = Processor::Data::BatchProcessor.new 2
|
8
|
+
|
9
|
+
watcher = mock
|
10
|
+
5.times do
|
11
|
+
watcher.should_receive(:created).ordered
|
12
|
+
watcher.should_receive(:created).ordered
|
13
|
+
watcher.should_receive(:processed).ordered
|
14
|
+
watcher.should_receive(:processed).ordered
|
15
|
+
end
|
16
|
+
|
17
|
+
query = Enumerator.new do |y|
|
18
|
+
a = 1
|
19
|
+
loop do
|
20
|
+
break if a > 10
|
21
|
+
watcher.created
|
22
|
+
y << a
|
23
|
+
a += 1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
processor.stub(query: query)
|
28
|
+
processor.records.each do |record|
|
29
|
+
watcher.processed
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should have total records count equals to count of query" do
|
34
|
+
query = 1..5
|
35
|
+
subject.stub(query: query)
|
36
|
+
subject.total_records.should eq 5
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
shared_examples_for "a records processor" do
|
2
|
+
let(:process_runner) { described_class.new }
|
3
|
+
let(:records) { 1..2 }
|
4
|
+
let(:processor) { stub.tap { |p| p.stub(records: records) } }
|
5
|
+
|
6
|
+
let(:no_recursion_preventer) { Proc.new{} }
|
7
|
+
let(:no_events) { stub.as_null_object }
|
8
|
+
|
9
|
+
it "should fetch records from processor" do
|
10
|
+
processor.should_receive(:records).and_return([])
|
11
|
+
process_runner.call processor, no_events, no_recursion_preventer
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should send each found record to processor" do
|
15
|
+
records.each { |record| processor.should_receive(:process).with(record) }
|
16
|
+
process_runner.call processor, no_events, no_recursion_preventer
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "exception handling" do
|
20
|
+
describe "processing a record raised StandardError" do
|
21
|
+
it "should continue processing" do
|
22
|
+
processor.should_receive(:process).twice.and_raise(StandardError)
|
23
|
+
expect { process_runner.call processor, no_events, no_recursion_preventer }.to_not raise_error
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should register a record_processing_error event" do
|
27
|
+
event = mock.tap { |event| event.should_receive(:trigger).any_number_of_times }
|
28
|
+
|
29
|
+
events_registrator = stub
|
30
|
+
events_registrator.should_receive(:register) do |event_name, failed_record, exception|
|
31
|
+
next if event_name != :record_processing_error
|
32
|
+
event_name.should eq :record_processing_error
|
33
|
+
exception.should be_a StandardError
|
34
|
+
event.trigger
|
35
|
+
end.any_number_of_times
|
36
|
+
|
37
|
+
processor.stub(:process).and_raise(StandardError)
|
38
|
+
|
39
|
+
process_runner.call processor, events_registrator, no_recursion_preventer rescue nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "processing a record raised Exception" do
|
44
|
+
it "should break processing" do
|
45
|
+
processor.should_receive(:process).once.and_raise(Exception)
|
46
|
+
expect { process_runner.call processor, no_events, no_recursion_preventer }.to raise_error(Exception)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper_lite'
|
2
|
+
require_relative 'specs'
|
3
|
+
require 'processor/process_runner/threads'
|
4
|
+
|
5
|
+
describe Processor::ProcessRunner::Threads do
|
6
|
+
let(:no_recursion_preventer) { Proc.new{} }
|
7
|
+
let(:no_events) { stub.as_null_object }
|
8
|
+
it_behaves_like "a records processor"
|
9
|
+
|
10
|
+
it "should run in defined number of threads" do
|
11
|
+
process_runner = Processor::ProcessRunner::Threads.new 5
|
12
|
+
|
13
|
+
process_runner.should_receive(:join_threads).once.ordered.and_call_original
|
14
|
+
process_runner.should_receive(:new_thread).exactly(5).times.ordered.and_call_original
|
15
|
+
process_runner.should_receive(:join_threads).once.ordered.and_call_original
|
16
|
+
process_runner.should_receive(:new_thread).exactly(4).times.ordered.and_call_original
|
17
|
+
process_runner.should_receive(:join_threads).once.ordered.and_call_original
|
18
|
+
|
19
|
+
processor = mock
|
20
|
+
processor.stub(records: 1..9)
|
21
|
+
processor.should_receive(:process).exactly(9).times
|
22
|
+
|
23
|
+
process_runner.call(processor, no_events, no_recursion_preventer)
|
24
|
+
end
|
25
|
+
end
|
@@ -5,25 +5,10 @@ describe Processor::Runner do
|
|
5
5
|
let(:runner) { Processor::Runner.new(processor, events_registrator) }
|
6
6
|
let(:processor) { stub }
|
7
7
|
let(:events_registrator) { stub.as_null_object }
|
8
|
-
let(:
|
9
|
-
|
10
|
-
it "should fetch records from processor till it'll be done" do
|
11
|
-
processor.stub(:done?).and_return(false, false, true)
|
12
|
-
processor.should_receive(:fetch_records).exactly(3).times.and_return([])
|
13
|
-
runner.run no_records_processor
|
14
|
-
end
|
8
|
+
let(:no_process_runner) { Proc.new{} }
|
15
9
|
|
16
10
|
describe "exception handling" do
|
17
|
-
let(:record) { stub }
|
18
|
-
before(:each) do
|
19
|
-
processor.stub(:fetch_records).and_return([record], [record], [])
|
20
|
-
end
|
21
|
-
|
22
11
|
describe "processing records raised" do
|
23
|
-
before(:each) do
|
24
|
-
processor.stub(done?: false)
|
25
|
-
end
|
26
|
-
|
27
12
|
it "should break processing and rerise" do
|
28
13
|
expect do
|
29
14
|
runner.run Proc.new { raise RuntimeError }
|
@@ -36,21 +21,6 @@ describe Processor::Runner do
|
|
36
21
|
end
|
37
22
|
end
|
38
23
|
|
39
|
-
describe "fetching records raised" do
|
40
|
-
it "should break processing and rerise Exception" do
|
41
|
-
processor.stub(:fetch_records).and_raise(RuntimeError)
|
42
|
-
processor.should_not_receive(:process)
|
43
|
-
expect { runner.run no_records_processor }.to raise_error(RuntimeError)
|
44
|
-
end
|
45
|
-
|
46
|
-
it "should register a processing_error" do
|
47
|
-
register_processing_error_event mock.tap { |event| event.should_receive :trigger }
|
48
|
-
|
49
|
-
processor.stub(:fetch_records).and_raise(RuntimeError)
|
50
|
-
runner.run no_records_processor rescue nil
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
24
|
private
|
55
25
|
def register_processing_error_event(event)
|
56
26
|
# Check that processing_error event was register
|
@@ -66,22 +36,21 @@ describe Processor::Runner do
|
|
66
36
|
|
67
37
|
describe "recursion" do
|
68
38
|
before(:each) do
|
69
|
-
processor.stub(done?: false)
|
70
39
|
processor.stub(total_records: 100)
|
71
|
-
processor.stub(
|
40
|
+
processor.stub(records: 1..Float::INFINITY)
|
72
41
|
end
|
73
42
|
|
74
43
|
it "should not fall into recursion" do
|
75
44
|
processor.should_receive(:process).at_most(1000).times
|
76
45
|
|
77
46
|
expect do
|
78
|
-
|
79
|
-
records.each do |record|
|
47
|
+
process_runner = Proc.new do |data_processor, events, recursion_preventer|
|
48
|
+
data_processor.records.each do |record|
|
80
49
|
recursion_preventer.call
|
81
|
-
|
50
|
+
data_processor.process record
|
82
51
|
end
|
83
52
|
end
|
84
|
-
runner.run
|
53
|
+
runner.run process_runner
|
85
54
|
end.to raise_error(Exception, /Processing fall into recursion/)
|
86
55
|
end
|
87
56
|
|
@@ -89,13 +58,13 @@ describe Processor::Runner do
|
|
89
58
|
processor.should_receive(:process).exactly(120).times
|
90
59
|
|
91
60
|
expect do
|
92
|
-
|
93
|
-
records.each do |record|
|
61
|
+
process_runner = Proc.new do |data_processor, events, recursion_preventer|
|
62
|
+
data_processor.records.each do |record|
|
94
63
|
recursion_preventer.call
|
95
|
-
|
64
|
+
data_processor.process record
|
96
65
|
end
|
97
66
|
end
|
98
|
-
runner.run
|
67
|
+
runner.run process_runner
|
99
68
|
end.to raise_error(Exception, /Processing fall into recursion/)
|
100
69
|
end
|
101
70
|
end
|
@@ -13,8 +13,8 @@ describe Processor::Thread do
|
|
13
13
|
|
14
14
|
it "should run a migration using provided block" do
|
15
15
|
thread = Processor::Thread.new @migration
|
16
|
-
thread.run_as do |
|
17
|
-
records.each do |record|
|
16
|
+
thread.run_as do |processor, *|
|
17
|
+
processor.records.each do |record|
|
18
18
|
processor.process record
|
19
19
|
end
|
20
20
|
end
|
@@ -30,7 +30,7 @@ describe Processor::Thread do
|
|
30
30
|
thread.run_in_threads
|
31
31
|
end
|
32
32
|
|
33
|
-
|
33
|
+
it "should run a migration in specifien number of threads" do
|
34
34
|
thread = Processor::Thread.new @migration
|
35
35
|
thread.run_in_threads 3
|
36
36
|
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: processor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
5
|
-
prerelease:
|
4
|
+
version: 1.0.0.alpha
|
5
|
+
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Alexander Paramonov
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-05-
|
12
|
+
date: 2013-05-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -59,7 +59,9 @@ dependencies:
|
|
59
59
|
- - ! '>='
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
|
-
description:
|
62
|
+
description: ! "Processor could execute any DataProcessor you specify and log entire
|
63
|
+
process.\n You may add own observers for monitoring background tasks on even send
|
64
|
+
email to bussiness with generated report."
|
63
65
|
email:
|
64
66
|
- alexander.n.paramonov@gmail.com
|
65
67
|
executables: []
|
@@ -78,26 +80,31 @@ files:
|
|
78
80
|
- example/migration.rb
|
79
81
|
- example/migrator.rb
|
80
82
|
- example/observer/progress_bar.rb
|
81
|
-
- example/solr_migration.rb
|
82
|
-
- example/solr_pages_migration.rb
|
83
83
|
- lib/processor.rb
|
84
|
-
- lib/processor/
|
84
|
+
- lib/processor/data/array_processor.rb
|
85
|
+
- lib/processor/data/batch_processor.rb
|
86
|
+
- lib/processor/data/csv_processor.rb
|
87
|
+
- lib/processor/data/null_processor.rb
|
88
|
+
- lib/processor/data/solr_pages_processor.rb
|
89
|
+
- lib/processor/data/solr_processor.rb
|
85
90
|
- lib/processor/events_registrator.rb
|
86
91
|
- lib/processor/observer/logger.rb
|
87
92
|
- lib/processor/observer/null_observer.rb
|
88
|
-
- lib/processor/
|
89
|
-
- lib/processor/
|
93
|
+
- lib/processor/process_runner/successive.rb
|
94
|
+
- lib/processor/process_runner/threads.rb
|
90
95
|
- lib/processor/runner.rb
|
91
96
|
- lib/processor/thread.rb
|
92
97
|
- lib/processor/version.rb
|
93
98
|
- processor.gemspec
|
94
99
|
- spec/example_spec.rb
|
95
|
-
- spec/processor/
|
100
|
+
- spec/processor/data/array_processor_spec.rb
|
101
|
+
- spec/processor/data/batch_processor_spec.rb
|
102
|
+
- spec/processor/data/null_processor_spec.rb
|
96
103
|
- spec/processor/events_registrator_spec.rb
|
97
104
|
- spec/processor/observer/logger_spec.rb
|
98
|
-
- spec/processor/
|
99
|
-
- spec/processor/
|
100
|
-
- spec/processor/
|
105
|
+
- spec/processor/process_runner/specs.rb
|
106
|
+
- spec/processor/process_runner/successive_spec.rb
|
107
|
+
- spec/processor/process_runner/threads_spec.rb
|
101
108
|
- spec/processor/runner_spec.rb
|
102
109
|
- spec/processor/thread_spec.rb
|
103
110
|
- spec/spec_helper_lite.rb
|
@@ -116,30 +123,29 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
116
123
|
version: '0'
|
117
124
|
segments:
|
118
125
|
- 0
|
119
|
-
hash:
|
126
|
+
hash: -1752503773457190967
|
120
127
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
128
|
none: false
|
122
129
|
requirements:
|
123
|
-
- - ! '
|
130
|
+
- - ! '>'
|
124
131
|
- !ruby/object:Gem::Version
|
125
|
-
version:
|
126
|
-
segments:
|
127
|
-
- 0
|
128
|
-
hash: 4470760754938067473
|
132
|
+
version: 1.3.1
|
129
133
|
requirements: []
|
130
134
|
rubyforge_project:
|
131
135
|
rubygems_version: 1.8.25
|
132
136
|
signing_key:
|
133
137
|
specification_version: 3
|
134
|
-
summary:
|
138
|
+
summary: Universal processor for data migration and reports generation.
|
135
139
|
test_files:
|
136
140
|
- spec/example_spec.rb
|
137
|
-
- spec/processor/
|
141
|
+
- spec/processor/data/array_processor_spec.rb
|
142
|
+
- spec/processor/data/batch_processor_spec.rb
|
143
|
+
- spec/processor/data/null_processor_spec.rb
|
138
144
|
- spec/processor/events_registrator_spec.rb
|
139
145
|
- spec/processor/observer/logger_spec.rb
|
140
|
-
- spec/processor/
|
141
|
-
- spec/processor/
|
142
|
-
- spec/processor/
|
146
|
+
- spec/processor/process_runner/specs.rb
|
147
|
+
- spec/processor/process_runner/successive_spec.rb
|
148
|
+
- spec/processor/process_runner/threads_spec.rb
|
143
149
|
- spec/processor/runner_spec.rb
|
144
150
|
- spec/processor/thread_spec.rb
|
145
151
|
- spec/spec_helper_lite.rb
|
data/example/solr_migration.rb
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
require 'processor/data_processor'
|
2
|
-
|
3
|
-
module Processor
|
4
|
-
module Example
|
5
|
-
class SolrMigration < DataProcessor
|
6
|
-
def process(user)
|
7
|
-
user.set_contact_method "Address book"
|
8
|
-
user.save!
|
9
|
-
end
|
10
|
-
|
11
|
-
def fetch_records
|
12
|
-
query.results
|
13
|
-
end
|
14
|
-
|
15
|
-
def total_records
|
16
|
-
@total_records ||= query.total
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
# query will return 0 records when all users'll be processed successfully
|
21
|
-
def query
|
22
|
-
User.search {
|
23
|
-
fulltext "My company"
|
24
|
-
with :title, "Manager"
|
25
|
-
with :contact_method, "Direct contact"
|
26
|
-
paginate page: 1, per_page: 10
|
27
|
-
}
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
@@ -1,48 +0,0 @@
|
|
1
|
-
require 'processor/data_processor'
|
2
|
-
|
3
|
-
module Processor
|
4
|
-
module Example
|
5
|
-
class SolrPagesMigration < DataProcessor
|
6
|
-
def done?(records)
|
7
|
-
# Optional custom check for migration to be done.
|
8
|
-
|
9
|
-
# your custom check
|
10
|
-
|
11
|
-
# use a default check if you like
|
12
|
-
super
|
13
|
-
end
|
14
|
-
|
15
|
-
def process(user)
|
16
|
-
user.set_contact_method "Address book"
|
17
|
-
user.save!
|
18
|
-
end
|
19
|
-
|
20
|
-
def fetch_records
|
21
|
-
query(next_page).results
|
22
|
-
end
|
23
|
-
|
24
|
-
def total_records
|
25
|
-
@total_records ||= query(1).total
|
26
|
-
end
|
27
|
-
|
28
|
-
# optional name to use in observers.
|
29
|
-
def name
|
30
|
-
"my_company_users_contact_method_migration"
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
def query(requested_page)
|
35
|
-
User.search {
|
36
|
-
fulltext "My company"
|
37
|
-
paginate page: requested_page, per_page: 10
|
38
|
-
}
|
39
|
-
end
|
40
|
-
|
41
|
-
def next_page
|
42
|
-
@page ||= 0
|
43
|
-
@page += 1
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
@@ -1,28 +0,0 @@
|
|
1
|
-
module Processor
|
2
|
-
class DataProcessor
|
3
|
-
def done?(records)
|
4
|
-
records.count < 1
|
5
|
-
end
|
6
|
-
|
7
|
-
def process(record)
|
8
|
-
raise NotImplementedError
|
9
|
-
end
|
10
|
-
|
11
|
-
def fetch_records
|
12
|
-
raise NotImplementedError
|
13
|
-
end
|
14
|
-
|
15
|
-
def total_records
|
16
|
-
raise NotImplementedError
|
17
|
-
end
|
18
|
-
|
19
|
-
def name
|
20
|
-
# underscore a class name
|
21
|
-
self.class.name.to_s.
|
22
|
-
gsub(/::/, '_').
|
23
|
-
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
24
|
-
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
25
|
-
downcase
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
module Processor
|
2
|
-
module RecordsProcessor
|
3
|
-
class Successive
|
4
|
-
def call(records, processor, events, recursion_preventer)
|
5
|
-
records.each do |record|
|
6
|
-
recursion_preventer.call
|
7
|
-
begin
|
8
|
-
events.register :before_record_processing, record
|
9
|
-
|
10
|
-
result = processor.process(record)
|
11
|
-
|
12
|
-
events.register :after_record_processing, record, result
|
13
|
-
rescue RuntimeError => exception
|
14
|
-
events.register :record_processing_error, record, exception
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
@@ -1,27 +0,0 @@
|
|
1
|
-
module Processor
|
2
|
-
module RecordsProcessor
|
3
|
-
class Threads
|
4
|
-
def call(records, processor, events, recursion_preventer)
|
5
|
-
threads = []
|
6
|
-
begin
|
7
|
-
records.each do |record|
|
8
|
-
recursion_preventer.call
|
9
|
-
threads << ::Thread.new(processor, record) do |thread_data_processor, thread_record|
|
10
|
-
begin
|
11
|
-
events.register :before_record_processing, thread_record
|
12
|
-
|
13
|
-
result = thread_data_processor.process(thread_record)
|
14
|
-
|
15
|
-
events.register :after_record_processing, thread_record, result
|
16
|
-
rescue RuntimeError => exception
|
17
|
-
events.register :record_processing_error, thread_record, exception
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
ensure # join already created threads even if recursion was detected
|
22
|
-
threads.each(&:join)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
require 'spec_helper_lite'
|
2
|
-
require 'processor/data_processor'
|
3
|
-
|
4
|
-
describe Processor::DataProcessor do
|
5
|
-
it "should have a name equals to underscored class name" do
|
6
|
-
subject.name.should eq "processor_data_processor"
|
7
|
-
end
|
8
|
-
|
9
|
-
it "should be done when there are 0 records to process" do
|
10
|
-
records = []
|
11
|
-
subject.done?(records).should be true
|
12
|
-
end
|
13
|
-
|
14
|
-
%w[done? fetch_records total_records process].each do |method_name|
|
15
|
-
it "should respond to #{method_name}" do
|
16
|
-
subject.should respond_to method_name
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
@@ -1,38 +0,0 @@
|
|
1
|
-
shared_examples_for "a records processor" do
|
2
|
-
let(:records_processor) { described_class.new }
|
3
|
-
let(:records) { 1..2 }
|
4
|
-
let(:processor) { stub }
|
5
|
-
|
6
|
-
let(:no_recursion_preventer) { Proc.new{} }
|
7
|
-
let(:no_events) { stub.as_null_object }
|
8
|
-
|
9
|
-
it "should send each found record to processor" do
|
10
|
-
records.each { |record| processor.should_receive(:process).with(record) }
|
11
|
-
records_processor.call records, processor, no_events, no_recursion_preventer
|
12
|
-
end
|
13
|
-
|
14
|
-
describe "exception handling" do
|
15
|
-
describe "processing a record raised RuntimeError" do
|
16
|
-
it "should continue processing" do
|
17
|
-
processor.should_receive(:process).twice.and_raise(RuntimeError)
|
18
|
-
expect { records_processor.call records, processor, no_events, no_recursion_preventer }.to_not raise_error
|
19
|
-
end
|
20
|
-
|
21
|
-
it "should register a record_processing_error event" do
|
22
|
-
event = mock.tap { |event| event.should_receive(:trigger).any_number_of_times }
|
23
|
-
|
24
|
-
events_registrator = stub
|
25
|
-
events_registrator.should_receive(:register) do |event_name, failed_record, exception|
|
26
|
-
next if event_name != :record_processing_error
|
27
|
-
event_name.should eq :record_processing_error
|
28
|
-
exception.should be_a RuntimeError
|
29
|
-
event.trigger
|
30
|
-
end.any_number_of_times
|
31
|
-
|
32
|
-
processor.stub(:process).and_raise(RuntimeError)
|
33
|
-
|
34
|
-
records_processor.call records, processor, events_registrator, no_recursion_preventer rescue nil
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|