processor 0.0.1 → 1.0.0.alpha
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.
- 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
|
[](http://travis-ci.org/AlexParamonov/processor)
|
4
4
|
[](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
|