Skip to content

Commit ce27d9e

Browse files
committed
Add concurrency via flay fork and concurrent-ruby
This PR swaps out flay with our own fork of flay that uses concurrency-ruby classes instead of stdlib `Hash`. This also introduces a `FileThreadPool` class for running blocks on an array of files concurrently.
1 parent 3c29ee1 commit ce27d9e

File tree

17 files changed

+191
-37
lines changed

17 files changed

+191
-37
lines changed

Diff for: Dockerfile

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
FROM alpine:edge
23

34
WORKDIR /usr/src/app/
@@ -8,8 +9,9 @@ COPY Gemfile.lock /usr/src/app/
89
COPY vendor/php-parser/composer.json /usr/src/app/vendor/php-parser/
910
COPY vendor/php-parser/composer.lock /usr/src/app/vendor/php-parser/
1011

11-
RUN apk --update add git python nodejs php-cli php-json php-phar php-openssl php-xml curl\
12-
ruby ruby-io-console ruby-dev ruby-bundler build-base && \
12+
RUN apk --update add openssh git python nodejs php-cli php-json php-phar php-openssl php-xml curl\
13+
ruby ruby-io-console ruby-dev build-base && \
14+
gem install bundler --no-ri --no-rdoc && \
1315
bundle install -j 4 && \
1416
apk del build-base && rm -fr /usr/share/ri && \
1517
curl -sS https://getcomposer.org/installer | php

Diff for: Gemfile

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
source 'https://rubygems.org'
22

3-
gem 'flay'
3+
gem 'flay', git: 'https://github.com/codeclimate/flay.git'
4+
gem 'concurrent-ruby', "~> 1.0.0.pre4"
5+
gem 'ruby_parser'
46
gem 'pry'
57
gem 'posix-spawn'
68
gem 'sexp_processor'

Diff for: Gemfile.lock

+12-6
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1+
GIT
2+
remote: https://github.com/codeclimate/flay.git
3+
revision: e6e472e064dc459277f8bd57167789f8fed77f56
4+
specs:
5+
flay (2.6.1)
6+
17
GEM
28
remote: https://rubygems.org/
39
specs:
410
coderay (1.1.0)
11+
concurrent-ruby (1.0.0.pre4)
512
diff-lcs (1.2.5)
6-
flay (2.6.1)
7-
ruby_parser (~> 3.0)
8-
sexp_processor (~> 4.0)
913
json (1.8.3)
1014
method_source (0.8.2)
1115
posix-spawn (0.3.11)
12-
pry (0.10.1)
16+
pry (0.10.3)
1317
coderay (~> 1.1.0)
1418
method_source (~> 0.8.1)
1519
slop (~> 3.4)
@@ -27,7 +31,7 @@ GEM
2731
diff-lcs (>= 1.2.0, < 2.0)
2832
rspec-support (~> 3.3.0)
2933
rspec-support (3.3.0)
30-
ruby_parser (3.7.1)
34+
ruby_parser (3.7.2)
3135
sexp_processor (~> 4.1)
3236
sexp_processor (4.6.0)
3337
slop (3.6.0)
@@ -36,12 +40,14 @@ PLATFORMS
3640
ruby
3741

3842
DEPENDENCIES
39-
flay
43+
concurrent-ruby (~> 1.0.0.pre4)
44+
flay!
4045
json
4146
posix-spawn
4247
pry
4348
rake
4449
rspec
50+
ruby_parser
4551
sexp_processor
4652

4753
BUNDLED WITH

Diff for: circle.yml

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ machine:
55
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
66
PRIVATE_REGISTRY: us.gcr.io/code_climate
77

8-
test:
8+
dependencies:
99
override:
10+
- docker info
1011
- docker build -t=$PRIVATE_REGISTRY/$CIRCLE_PROJECT_REPONAME:b$CIRCLE_BUILD_NUM .
12+
13+
test:
14+
override:
1115
- docker run $PRIVATE_REGISTRY/$CIRCLE_PROJECT_REPONAME:b$CIRCLE_BUILD_NUM bundle exec rake
1216

1317
deployment:

Diff for: lib/cc/engine/analyzers/analyzer_base.rb

+12-12
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ def initialize(engine_config:)
66
@engine_config = engine_config
77
end
88

9-
def run
10-
files.map do |file|
11-
begin
12-
process_file(file)
13-
rescue => ex
14-
$stderr.puts "Skipping file #{file} due to exception"
15-
$stderr.puts "(#{ex.class}) #{ex.message} #{ex.backtrace.join("\n")}"
16-
end
17-
end
9+
def run(file)
10+
process_file(file)
11+
rescue => ex
12+
$stderr.puts "Skipping file #{file} due to exception"
13+
$stderr.puts "(#{ex.class}) #{ex.message} #{ex.backtrace.join("\n")}"
14+
end
15+
16+
def files
17+
file_list.files
1818
end
1919

2020
def mass_threshold
@@ -33,12 +33,12 @@ def process_file(path)
3333
raise NoMethodError.new("Subclass must implement `process_file`")
3434
end
3535

36-
def files
37-
::CC::Engine::Analyzers::FileList.new(
36+
def file_list
37+
@_file_list ||= ::CC::Engine::Analyzers::FileList.new(
3838
engine_config: engine_config,
3939
default_paths: self.class::DEFAULT_PATHS,
4040
language: self.class::LANGUAGE
41-
).files
41+
)
4242
end
4343
end
4444
end

Diff for: lib/cc/engine/analyzers/engine_config.rb

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ def languages
1414
config.fetch("languages", {})
1515
end
1616

17+
def concurrency
18+
config.fetch("concurrency", 2)
19+
end
20+
1721
def mass_threshold_for(language)
1822
threshold = fetch_language(language).fetch("mass_threshold", nil)
1923

Diff for: lib/cc/engine/analyzers/file_thread_pool.rb

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
require "thread"
2+
3+
module CC
4+
module Engine
5+
module Analyzers
6+
class FileThreadPool
7+
DEFAULT_CONCURRENCY = 2
8+
MAX_CONCURRENCY = 2
9+
10+
def initialize(files, concurrency: DEFAULT_CONCURRENCY)
11+
@files = files
12+
@concurrency = concurrency
13+
end
14+
15+
def run(&block)
16+
queue = build_queue
17+
18+
@workers = thread_count.times.map do
19+
Thread.new do
20+
begin
21+
while item = queue.pop(true)
22+
yield item
23+
end
24+
rescue ThreadError
25+
end
26+
end
27+
end
28+
end
29+
30+
def join
31+
workers.map(&:join)
32+
end
33+
34+
private
35+
36+
attr_reader :files, :concurrency, :workers
37+
38+
def build_queue
39+
Queue.new.tap do |queue|
40+
files.each do |file|
41+
queue.push(file)
42+
end
43+
end
44+
end
45+
46+
def thread_count
47+
if (1..MAX_CONCURRENCY) === concurrency
48+
concurrency
49+
elsif concurrency < 1
50+
DEFAULT_CONCURRENCY
51+
else
52+
DEFAULT_CONCURRENCY
53+
end
54+
end
55+
end
56+
end
57+
end
58+
end

Diff for: lib/cc/engine/analyzers/javascript/main.rb

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ class Main < CC::Engine::Analyzers::Base
1818
DEFAULT_MASS_THRESHOLD = 40
1919
BASE_POINTS = 3000
2020

21-
2221
private
2322

2423
def process_file(path)

Diff for: lib/cc/engine/analyzers/python/main.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class Main < CC::Engine::Analyzers::Base
1515
DEFAULT_MASS_THRESHOLD = 40
1616
BASE_POINTS = 1000
1717

18-
attr_reader :directory, :engine_config
18+
private
1919

2020
def process_file(path)
2121
Node.new(::CC::Engine::Analyzers::Python::Parser.new(File.binread(path), path).parse.syntax_tree, path).format

Diff for: lib/cc/engine/analyzers/reporter.rb

+18-5
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,36 @@
11
require 'cc/engine/analyzers/violation'
2+
require 'cc/engine/analyzers/file_thread_pool'
3+
require 'thread'
24

35
module CC
46
module Engine
57
module Analyzers
68
class Reporter
79
TIMEOUT = 10
8-
def initialize(language_strategy, io)
10+
11+
def initialize(engine_config, language_strategy, io)
12+
@engine_config = engine_config
913
@language_strategy = language_strategy
1014
@io = io
1115
end
1216

1317
def run
14-
sexps = language_strategy.run
18+
process_files
19+
report
20+
end
1521

16-
sexps.each do |sexp|
22+
def process_files
23+
pool = FileThreadPool.new(
24+
language_strategy.files,
25+
concurrency: engine_config.concurrency
26+
)
27+
28+
pool.run do |file|
29+
sexp = language_strategy.run(file)
1730
process_sexp(sexp)
1831
end
1932

20-
report
33+
pool.join
2134
end
2235

2336
def report
@@ -37,7 +50,7 @@ def flay
3750
@flay ||= Flay.new(flay_options)
3851
end
3952

40-
attr_reader :language_strategy, :io
53+
attr_reader :engine_config, :language_strategy, :io
4154

4255
def mass_threshold
4356
@mass_threshold ||= language_strategy.mass_threshold

Diff for: lib/cc/engine/analyzers/ruby/main.rb

-3
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ class Main < CC::Engine::Analyzers::Base
2323

2424
private
2525

26-
attr_reader :directory, :engine_config
27-
2826
def process_file(file)
2927
RubyParser.new.process(File.binread(file), file, TIMEOUT)
3028
end
@@ -33,4 +31,3 @@ def process_file(file)
3331
end
3432
end
3533
end
36-

Diff for: lib/cc/engine/duplication.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def initialize(directory:, engine_config:, io:)
2929
def run
3030
languages_to_analyze.each do |language|
3131
engine = LANGUAGES[language].new(engine_config: engine_config)
32-
reporter = CC::Engine::Analyzers::Reporter.new(engine, io)
32+
reporter = CC::Engine::Analyzers::Reporter.new(engine_config, engine, io)
3333
reporter.run
3434
end
3535
end

Diff for: spec/cc/engine/analyzers/file_thread_pool_spec.rb

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
require "spec_helper"
2+
require "cc/engine/analyzers/file_thread_pool"
3+
4+
RSpec.describe CC::Engine::Analyzers::FileThreadPool do
5+
describe "#run" do
6+
it "uses default count of threads when concurrency is not specified" do
7+
allow(Thread).to receive(:new)
8+
9+
pool = CC::Engine::Analyzers::FileThreadPool.new([])
10+
pool.run {}
11+
12+
expect(Thread).to have_received(:new).exactly(
13+
CC::Engine::Analyzers::FileThreadPool::DEFAULT_CONCURRENCY
14+
).times
15+
end
16+
17+
it "uses default concurrency when concurrency is over max" do
18+
allow(Thread).to receive(:new)
19+
20+
run_pool_with_concurrency(
21+
CC::Engine::Analyzers::FileThreadPool::DEFAULT_CONCURRENCY + 2
22+
)
23+
24+
expect(Thread).to have_received(:new).exactly(
25+
CC::Engine::Analyzers::FileThreadPool::DEFAULT_CONCURRENCY
26+
).times
27+
end
28+
29+
it "uses default concucurrency when concucurrency is under 1" do
30+
allow(Thread).to receive(:new)
31+
32+
run_pool_with_concurrency(-2)
33+
34+
expect(Thread).to have_received(:new).exactly(
35+
CC::Engine::Analyzers::FileThreadPool::DEFAULT_CONCURRENCY
36+
).times
37+
end
38+
39+
it "uses supplied concurrency when valid" do
40+
allow(Thread).to receive(:new)
41+
42+
run_pool_with_concurrency(1)
43+
44+
expect(Thread).to have_received(:new).exactly(1).times
45+
end
46+
47+
it "calls block for each file" do
48+
pool = CC::Engine::Analyzers::FileThreadPool.new(["abc", "123", "xyz"])
49+
50+
results = []
51+
pool.run do |f|
52+
results.push f.reverse
53+
end
54+
pool.join
55+
56+
expect(results).to include("cba")
57+
expect(results).to include("321")
58+
expect(results).to include("zyx")
59+
end
60+
end
61+
62+
def run_pool_with_concurrency(concurrency)
63+
pool = CC::Engine::Analyzers::FileThreadPool.new(
64+
[],
65+
concurrency: concurrency
66+
)
67+
pool.run {}
68+
end
69+
end

Diff for: spec/cc/engine/analyzers/javascript/main_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def run_engine(config = nil)
6666
io = StringIO.new
6767

6868
engine = ::CC::Engine::Analyzers::Javascript::Main.new(engine_config: config)
69-
reporter = ::CC::Engine::Analyzers::Reporter.new(engine, io)
69+
reporter = ::CC::Engine::Analyzers::Reporter.new(double(concurrency: 2), engine, io)
7070

7171
reporter.run
7272

Diff for: spec/cc/engine/analyzers/php/main_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def run_engine(config = nil)
6666
io = StringIO.new
6767

6868
engine = ::CC::Engine::Analyzers::Php::Main.new(engine_config: config)
69-
reporter = ::CC::Engine::Analyzers::Reporter.new(engine, io)
69+
reporter = ::CC::Engine::Analyzers::Reporter.new(double(concurrency: 2), engine, io)
7070

7171
reporter.run
7272

Diff for: spec/cc/engine/analyzers/python/main_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def run_engine(config = nil)
5454
io = StringIO.new
5555

5656
engine = ::CC::Engine::Analyzers::Python::Main.new(engine_config: config)
57-
reporter = ::CC::Engine::Analyzers::Reporter.new(engine, io)
57+
reporter = ::CC::Engine::Analyzers::Reporter.new(double(concurrency: 2), engine, io)
5858

5959
reporter.run
6060

Diff for: spec/cc/engine/analyzers/ruby/main_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def run_engine(config = {})
7474

7575
config = CC::Engine::Analyzers::EngineConfig.new(config)
7676
engine = ::CC::Engine::Analyzers::Ruby::Main.new(engine_config: config)
77-
reporter = ::CC::Engine::Analyzers::Reporter.new(engine, io)
77+
reporter = ::CC::Engine::Analyzers::Reporter.new(double(concurrency: 2), engine, io)
7878

7979
reporter.run
8080

0 commit comments

Comments
 (0)