ts-sidekiq-delta 0.1.0
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 +7 -0
- data/.gitignore +16 -0
- data/.rspec +1 -0
- data/Gemfile +10 -0
- data/Guardfile +17 -0
- data/LICENSE +20 -0
- data/README.markdown +36 -0
- data/Rakefile +16 -0
- data/config/redis-cucumber.conf +13 -0
- data/cucumber.yml +2 -0
- data/features/sidekiq_deltas.feature +62 -0
- data/features/smart_indexing.feature +43 -0
- data/features/step_definitions/common_steps.rb +76 -0
- data/features/step_definitions/sidekiq_delta_steps.rb +33 -0
- data/features/step_definitions/smart_indexing_steps.rb +3 -0
- data/features/support/env.rb +31 -0
- data/features/thinking_sphinx/database.example.yml +3 -0
- data/features/thinking_sphinx/db/migrations/create_delayed_betas.rb +4 -0
- data/features/thinking_sphinx/models/delayed_beta.rb +6 -0
- data/lib/thinking_sphinx/deltas/sidekiq_delta.rb +64 -0
- data/lib/thinking_sphinx/deltas/sidekiq_delta/delta_job.rb +51 -0
- data/lib/thinking_sphinx/deltas/sidekiq_delta/flag_as_deleted_job.rb +20 -0
- data/lib/thinking_sphinx/deltas/sidekiq_delta/railtie.rb +5 -0
- data/lib/thinking_sphinx/deltas/sidekiq_delta/tasks.rb +38 -0
- data/lib/ts-sidekiq-delta.rb +2 -0
- data/spec/acceptance/sidekiq_deltas_spec.rb +50 -0
- data/spec/acceptance/spec_helper.rb +5 -0
- data/spec/acceptance/support/database_cleaner.rb +11 -0
- data/spec/acceptance/support/sphinx_controller.rb +39 -0
- data/spec/acceptance/support/sphinx_helpers.rb +39 -0
- data/spec/internal/.gitignore +2 -0
- data/spec/internal/app/indices/book_index.rb +3 -0
- data/spec/internal/app/models/book.rb +3 -0
- data/spec/internal/config/database.yml +5 -0
- data/spec/internal/db/schema.rb +24 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/spec_helper.rb +17 -0
- data/tasks/rails.rake +1 -0
- data/ts-sidekiq-delta.gemspec +26 -0
- metadata +204 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 69d263abd6456994f89536fed3e8ab516e91d029
|
4
|
+
data.tar.gz: d8068a4e447382b7b8c95f08702ba8b7988a13f3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7ad79d13705125cdbbc5b7d171c7de7a3de5411189e36845adf9b5db7a4c9a9187c8eb0d91fcf32db56d6e5df377a8762541def04441c43b4c4f490a916653d5
|
7
|
+
data.tar.gz: 31e37ee5b4ae26931e4b0d18eeb275a5ea251c4dac16e6ee9deaebc0332250e3e73fa79431f4a1e254010c44e659341e7c6a8fb5e8341c3db3b9610e624aa5fe
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
guard 'bundler' do
|
2
|
+
watch('Gemfile')
|
3
|
+
watch(/^.+\.gemspec/)
|
4
|
+
end
|
5
|
+
|
6
|
+
guard 'rspec', :version => 2, :cli => "-c --format progress", :all_on_start => false do
|
7
|
+
watch(%r{^spec/.+_spec\.rb$})
|
8
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
9
|
+
watch('spec/spec_helper.rb') { "spec" }
|
10
|
+
end
|
11
|
+
|
12
|
+
guard 'cucumber', :all_on_start => false do
|
13
|
+
watch(%r{^features/.+\.feature$})
|
14
|
+
watch(%r{^features/support/.+$}) { 'features' }
|
15
|
+
watch(%r{^features/step_definitions/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'features' }
|
16
|
+
end
|
17
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Pat Allan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# Delayed Deltas for Thinking Sphinx with Sidekiq
|
2
|
+
|
3
|
+
This code was heavily based on Aaron Gibralter's [ts-resque-delta](https://github.com/agibralter/ts-resque-delta), and was initially adapted for Sidekiq by [Danny Hawkins](https://github.com/danhawkins). This release is maintained by [Pat Allan](https://github.com/pat).
|
4
|
+
|
5
|
+
This version of `ts-sidekiq-delta` works only with [Thinking Sphinx](https://github.com/pat/thinking-sphinx) v3 or newer. v1/v2 releases are not supported, and almost certainly will never be. It does work with the Flying Sphinx service, provided you're using 1.0.0 or newer of the `flying-sphinx` gem.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Get it into your Gemfile - and don't forget the version constraint!
|
10
|
+
|
11
|
+
gem 'ts-sidekiq-delta', '~> 0.1.0'
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
In your index definitions, you'll want to include the delta setting as an initial option:
|
16
|
+
|
17
|
+
ThinkingSphinx::Index.define(:article,
|
18
|
+
:with => :active_record,
|
19
|
+
:delta => ThinkingSphinx::Deltas::SidekiqDelta
|
20
|
+
) do
|
21
|
+
# fields and attributes and such
|
22
|
+
end
|
23
|
+
|
24
|
+
If you've never used delta indexes before, you'll want to add the boolean
|
25
|
+
column named `:delta` to each model's table and a corresponding database index:
|
26
|
+
|
27
|
+
def change
|
28
|
+
add_column :articles, :delta, :boolean, :default => true, :null => false
|
29
|
+
add_index :articles, :delta
|
30
|
+
end
|
31
|
+
|
32
|
+
From here on in, just use Thinking Sphinx and Sidekiq as you normally would, and you'll find your Sphinx indices are updated quite promptly by Sidekiq.
|
33
|
+
|
34
|
+
## Licence
|
35
|
+
|
36
|
+
Copyright (c) 2013, ts-sidekiq-delta was originally developed by Danny Hawkins, is currently maintained by Pat Allan, and is released under the open MIT Licence.
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
require 'cucumber'
|
5
|
+
require 'cucumber/rake/task'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
8
|
+
t.rspec_opts = ["-c", "--format progress"]
|
9
|
+
end
|
10
|
+
|
11
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
12
|
+
end
|
13
|
+
|
14
|
+
task :all_tests => [:spec, :features]
|
15
|
+
|
16
|
+
task :default => :all_tests
|
data/cucumber.yml
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
Feature: Resque Delta Indexing
|
2
|
+
In order to have delta indexing on frequently-updated sites
|
3
|
+
Developers
|
4
|
+
Should be able to use Resque to handle delta indices to lower system load
|
5
|
+
|
6
|
+
Background:
|
7
|
+
Given Sphinx is running
|
8
|
+
And I am searching on delayed betas
|
9
|
+
And I have data and it has been indexed
|
10
|
+
|
11
|
+
Scenario: Delta Index should not fire automatically
|
12
|
+
When I search for one
|
13
|
+
Then I should get 1 result
|
14
|
+
|
15
|
+
When I change the name of delayed beta one to eleven
|
16
|
+
And I wait for Sphinx to catch up
|
17
|
+
And I search for one
|
18
|
+
Then I should get 1 result
|
19
|
+
|
20
|
+
When I search for eleven
|
21
|
+
Then I should get 0 results
|
22
|
+
|
23
|
+
Scenario: Delta Index should fire when jobs are run
|
24
|
+
When I search for one
|
25
|
+
Then I should get 1 result
|
26
|
+
|
27
|
+
When I change the name of delayed beta two to twelve
|
28
|
+
And I wait for Sphinx to catch up
|
29
|
+
And I search for twelve
|
30
|
+
Then I should get 0 results
|
31
|
+
|
32
|
+
When I run the delayed jobs
|
33
|
+
And I wait for Sphinx to catch up
|
34
|
+
And I search for twelve
|
35
|
+
Then I should get 1 result
|
36
|
+
|
37
|
+
When I search for two
|
38
|
+
Then I should get 0 results
|
39
|
+
|
40
|
+
Scenario: ensuring that duplicate jobs are deleted
|
41
|
+
When I change the name of delayed beta two to fifty
|
42
|
+
And I change the name of delayed beta five to twelve
|
43
|
+
And I change the name of delayed beta one to fifteen
|
44
|
+
And I change the name of delayed beta six to twenty
|
45
|
+
And I run one delayed job
|
46
|
+
Then there should be no more DeltaJobs on the Resque queue
|
47
|
+
|
48
|
+
When I run the delayed jobs
|
49
|
+
And I wait for Sphinx to catch up
|
50
|
+
And I search for fifty
|
51
|
+
Then I should get 1 result
|
52
|
+
|
53
|
+
When I search for two
|
54
|
+
Then I should get 0 results
|
55
|
+
|
56
|
+
Scenario: canceling jobs
|
57
|
+
When I change the name of delayed beta two to fifty
|
58
|
+
And I cancel the jobs
|
59
|
+
And I run the delayed jobs
|
60
|
+
And I wait for Sphinx to catch up
|
61
|
+
And I search for fifty
|
62
|
+
Then I should get 0 results
|
@@ -0,0 +1,43 @@
|
|
1
|
+
Feature: Smart Indexing
|
2
|
+
In order to have core indexing that works well with resque delta indexing
|
3
|
+
Developers
|
4
|
+
Should be able to use smart index to update core indices
|
5
|
+
|
6
|
+
Background:
|
7
|
+
Given Sphinx is running
|
8
|
+
And I am searching on delayed betas
|
9
|
+
And I have data
|
10
|
+
|
11
|
+
Scenario: Smart indexing should update core indices
|
12
|
+
When I run the smart indexer
|
13
|
+
And I wait for Sphinx to catch up
|
14
|
+
And I search for one
|
15
|
+
Then I should get 1 result
|
16
|
+
|
17
|
+
Scenario: Smart indexing should reset the delta index
|
18
|
+
Given I have indexed
|
19
|
+
When I change the name of delayed beta one to eleven
|
20
|
+
And I run the delayed jobs
|
21
|
+
And I wait for Sphinx to catch up
|
22
|
+
|
23
|
+
When I change the name of delayed beta eleven to one
|
24
|
+
And I run the smart indexer
|
25
|
+
And I run the delayed jobs
|
26
|
+
And I wait for Sphinx to catch up
|
27
|
+
|
28
|
+
When I search for eleven
|
29
|
+
Then I should get 0 results
|
30
|
+
|
31
|
+
Scenario: Delta Index running after smart indexing should not hide records
|
32
|
+
When I run the smart indexer
|
33
|
+
And I run the delayed jobs
|
34
|
+
And I wait for Sphinx to catch up
|
35
|
+
|
36
|
+
When I search for two
|
37
|
+
Then I should get 1 result
|
38
|
+
|
39
|
+
Scenario: Smart index should remove existing delta jobs
|
40
|
+
When I run the smart indexer
|
41
|
+
And I run one delayed job
|
42
|
+
And I wait for Sphinx to catch up
|
43
|
+
Then there should be no more DeltaJobs on the Resque queue
|
@@ -0,0 +1,76 @@
|
|
1
|
+
Before do
|
2
|
+
$queries_executed = []
|
3
|
+
ThinkingSphinx::Deltas::SidekiqDelta.clear!
|
4
|
+
@model = nil
|
5
|
+
@method = :search
|
6
|
+
@query = ""
|
7
|
+
@conditions = {}
|
8
|
+
@with = {}
|
9
|
+
@without = {}
|
10
|
+
@with_all = {}
|
11
|
+
@options = {}
|
12
|
+
@results = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
Given "Sphinx is running" do
|
16
|
+
ThinkingSphinx::Configuration.instance.controller.should be_running
|
17
|
+
end
|
18
|
+
|
19
|
+
Given /^I am searching on (.+)$/ do |model|
|
20
|
+
@model = model.gsub(/\s/, '_').singularize.camelize.constantize
|
21
|
+
end
|
22
|
+
|
23
|
+
Given "I have data" do
|
24
|
+
DelayedBeta.create(:name => "one")
|
25
|
+
DelayedBeta.create(:name => "two")
|
26
|
+
DelayedBeta.create(:name => "three")
|
27
|
+
DelayedBeta.create(:name => "four")
|
28
|
+
DelayedBeta.create(:name => "five")
|
29
|
+
DelayedBeta.create(:name => "six")
|
30
|
+
DelayedBeta.create(:name => "seven")
|
31
|
+
DelayedBeta.create(:name => "eight")
|
32
|
+
DelayedBeta.create(:name => "nine")
|
33
|
+
DelayedBeta.create(:name => "ten")
|
34
|
+
end
|
35
|
+
|
36
|
+
Given "I have indexed" do
|
37
|
+
ThinkingSphinx::Deltas::SidekiqDelta.clear!
|
38
|
+
ThinkingSphinx::Configuration.instance.controller.index
|
39
|
+
sleep(1.5)
|
40
|
+
end
|
41
|
+
|
42
|
+
Given "I have data and it has been indexed" do
|
43
|
+
step "I have data"
|
44
|
+
step "I have indexed"
|
45
|
+
end
|
46
|
+
|
47
|
+
When "I wait for Sphinx to catch up" do
|
48
|
+
sleep(0.5)
|
49
|
+
end
|
50
|
+
|
51
|
+
When /^I search for (\w+)$/ do |query|
|
52
|
+
@results = nil
|
53
|
+
@query = query
|
54
|
+
end
|
55
|
+
|
56
|
+
Then /^I should get (\d+) results?$/ do |count|
|
57
|
+
results.length.should == count.to_i
|
58
|
+
end
|
59
|
+
|
60
|
+
Then /^I debug$/ do
|
61
|
+
debugger
|
62
|
+
0
|
63
|
+
end
|
64
|
+
|
65
|
+
def results
|
66
|
+
@results ||= (@model || ThinkingSphinx).send(
|
67
|
+
@method,
|
68
|
+
@query,
|
69
|
+
@options.merge(
|
70
|
+
:conditions => @conditions,
|
71
|
+
:with => @with,
|
72
|
+
:without => @without,
|
73
|
+
:with_all => @with_all
|
74
|
+
)
|
75
|
+
)
|
76
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
When /^I run the delayed jobs$/ do
|
2
|
+
unless @resque_worker
|
3
|
+
@resque_worker = Resque::Worker.new("ts_delta")
|
4
|
+
@resque_worker.register_worker
|
5
|
+
end
|
6
|
+
while job = @resque_worker.reserve
|
7
|
+
@resque_worker.perform(job)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
When /^I run one delayed job$/ do
|
12
|
+
unless @resque_worker
|
13
|
+
@resque_worker = Resque::Worker.new("ts_delta")
|
14
|
+
@resque_worker.register_worker
|
15
|
+
end
|
16
|
+
job = @resque_worker.reserve
|
17
|
+
@resque_worker.perform(job)
|
18
|
+
end
|
19
|
+
|
20
|
+
When /^I cancel the jobs$/ do
|
21
|
+
ThinkingSphinx::Deltas::SidekiqDelta.clear!
|
22
|
+
end
|
23
|
+
|
24
|
+
When /^I change the name of delayed beta (\w+) to (\w+)$/ do |current, replacement|
|
25
|
+
DelayedBeta.find_by_name(current).update_attributes(:name => replacement)
|
26
|
+
end
|
27
|
+
|
28
|
+
Then /^there should be no more DeltaJobs on the Resque queue$/ do
|
29
|
+
job_classes = Resque.redis.lrange("queue:ts_delta", 0, -1).collect do |j|
|
30
|
+
Resque.decode(j)["class"]
|
31
|
+
end
|
32
|
+
job_classes.should_not include("ThinkingSphinx::Deltas::SidekiqDelta::DeltaJob")
|
33
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'cucumber'
|
2
|
+
require 'rspec/expectations'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'active_record'
|
5
|
+
require 'mock_redis'
|
6
|
+
|
7
|
+
PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
|
8
|
+
|
9
|
+
$:.unshift(File.join(PROJECT_ROOT, 'lib'))
|
10
|
+
$:.unshift(File.dirname(__FILE__))
|
11
|
+
|
12
|
+
require 'cucumber/thinking_sphinx/internal_world'
|
13
|
+
|
14
|
+
ActiveRecord::Base.default_timezone = :utc
|
15
|
+
|
16
|
+
world = Cucumber::ThinkingSphinx::InternalWorld.new
|
17
|
+
world.configure_database
|
18
|
+
|
19
|
+
require 'thinking_sphinx'
|
20
|
+
require 'thinking_sphinx/deltas/resque_delta'
|
21
|
+
|
22
|
+
world.setup
|
23
|
+
|
24
|
+
Resque.redis = MockRedis.new
|
25
|
+
Before do
|
26
|
+
Sidekiq.redis{|r| r.flushall}
|
27
|
+
end
|
28
|
+
|
29
|
+
require 'database_cleaner'
|
30
|
+
require 'database_cleaner/cucumber'
|
31
|
+
DatabaseCleaner.strategy = :truncation
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
require 'thinking_sphinx'
|
3
|
+
|
4
|
+
class ThinkingSphinx::Deltas::SidekiqDelta < ThinkingSphinx::Deltas::DefaultDelta
|
5
|
+
JOB_TYPES = []
|
6
|
+
JOB_PREFIX = 'ts-delta'
|
7
|
+
|
8
|
+
# LTRIM + LPOP deletes all items from the Resque queue without loading it
|
9
|
+
# into client memory (unlike Resque.dequeue).
|
10
|
+
# WARNING: This will clear ALL jobs in any queue used by a ResqueDelta job.
|
11
|
+
# If you're sharing a queue with other jobs they'll be deleted!
|
12
|
+
def self.clear_thinking_sphinx_queues
|
13
|
+
JOB_TYPES.collect { |job|
|
14
|
+
job.sidekiq_options['queue']
|
15
|
+
}.uniq.each do |queue|
|
16
|
+
Sidekiq.redis { |redis| redis.srem "queues", queue }
|
17
|
+
Sidekiq.redis { |redis| redis.del "queue:#{queue}" }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Clear both the resque queues and any other state maintained in redis
|
22
|
+
def self.clear!
|
23
|
+
self.clear_thinking_sphinx_queues
|
24
|
+
|
25
|
+
FlagAsDeletedSet.clear_all!
|
26
|
+
end
|
27
|
+
|
28
|
+
# Use simplistic locking. We're assuming that the user won't run more than one
|
29
|
+
# `rake ts:si` or `rake ts:in` task at a time.
|
30
|
+
def self.lock(index_name)
|
31
|
+
Sidekiq.redis {|redis|
|
32
|
+
redis.set("#{JOB_PREFIX}:index:#{index_name}:locked", 'true')
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.unlock(index_name)
|
37
|
+
Sidekiq.redis {|redis|
|
38
|
+
redis.del("#{JOB_PREFIX}:index:#{index_name}:locked")
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.locked?(index_name)
|
43
|
+
Sidekiq.redis {|redis|
|
44
|
+
redis.get("#{JOB_PREFIX}:index:#{index_name}:locked") == 'true'
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def delete(index, instance)
|
49
|
+
return if self.class.locked?(index.reference)
|
50
|
+
|
51
|
+
ThinkingSphinx::Deltas::SidekiqDelta::FlagAsDeletedJob.perform_async(
|
52
|
+
index.name, index.document_id_for_key(instance.id)
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def index(index)
|
57
|
+
return if self.class.locked?(index.reference)
|
58
|
+
|
59
|
+
ThinkingSphinx::Deltas::SidekiqDelta::DeltaJob.perform_async(index.name)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
require 'thinking_sphinx/deltas/sidekiq_delta/delta_job'
|
64
|
+
require 'thinking_sphinx/deltas/sidekiq_delta/flag_as_deleted_job'
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# A simple job class that processes a given index.
|
2
|
+
#
|
3
|
+
class ThinkingSphinx::Deltas::SidekiqDelta::DeltaJob
|
4
|
+
include Sidekiq::Worker
|
5
|
+
|
6
|
+
sidekiq_options unique: true, retry: true, queue: 'ts_delta'
|
7
|
+
|
8
|
+
# Runs Sphinx's indexer tool to process the index.
|
9
|
+
#
|
10
|
+
# @param [String] index the name of the Sphinx index
|
11
|
+
#
|
12
|
+
def perform(index)
|
13
|
+
config = ThinkingSphinx::Configuration.instance
|
14
|
+
config.controller.index index, :verbose => !config.settings['quiet_deltas']
|
15
|
+
end
|
16
|
+
|
17
|
+
# Try again later if lock is in use.
|
18
|
+
def self.lock_failed(*arguments)
|
19
|
+
self.class.perform_async(*arguments)
|
20
|
+
end
|
21
|
+
|
22
|
+
# This allows us to have a concurrency safe version of ts-delayed-delta's
|
23
|
+
# duplicates_exist:
|
24
|
+
#
|
25
|
+
# http://github.com/freelancing-god/ts-delayed-delta/blob/master/lib/thinkin
|
26
|
+
# g_sphinx/deltas/delayed_delta/job.rb#L47
|
27
|
+
#
|
28
|
+
# The name of this method ensures that it runs within around_perform_lock.
|
29
|
+
#
|
30
|
+
# We've leveraged resque-lock-timeout to ensure that only one DeltaJob is
|
31
|
+
# running at a time. Now, this around filter essentially ensures that only
|
32
|
+
# one DeltaJob of each index type can sit at the queue at once. If the queue
|
33
|
+
# has more than one, lrem will clear the rest off.
|
34
|
+
#
|
35
|
+
def self.around_perform_lock1(*arguments)
|
36
|
+
# Remove all other instances of this job (with the same args) from the
|
37
|
+
# queue. Uses LREM (http://code.google.com/p/redis/wiki/LremCommand) which
|
38
|
+
# takes the form: "LREM key count value" and if count == 0 removes all
|
39
|
+
# instances of value from the list.
|
40
|
+
redis_job_value = {:class => self.to_s, :args => arguments}.to_json
|
41
|
+
|
42
|
+
Sidekiq.redis {|redis|
|
43
|
+
redis.lrem("queue:#{@queue}", 0, redis_job_value)
|
44
|
+
}
|
45
|
+
|
46
|
+
yield
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
ThinkingSphinx::Deltas::SidekiqDelta::JOB_TYPES <<
|
51
|
+
ThinkingSphinx::Deltas::SidekiqDelta::DeltaJob
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class ThinkingSphinx::Deltas::SidekiqDelta::FlagAsDeletedJob
|
2
|
+
include Sidekiq::Worker
|
3
|
+
|
4
|
+
# Runs Sphinx's indexer tool to process the index. Currently assumes Sphinx
|
5
|
+
# is running.
|
6
|
+
sidekiq_options unique: true, retry: true, queue: 'ts_delta'
|
7
|
+
|
8
|
+
def perform(index, document_id)
|
9
|
+
ThinkingSphinx::Connection.pool.take do |connection|
|
10
|
+
connection.query(
|
11
|
+
Riddle::Query.update(index, document_id, :sphinx_deleted => true)
|
12
|
+
)
|
13
|
+
end
|
14
|
+
rescue Mysql2::Error => error
|
15
|
+
# This isn't vital, so don't raise the error
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
ThinkingSphinx::Deltas::SidekiqDelta::JOB_TYPES <<
|
20
|
+
ThinkingSphinx::Deltas::SidekiqDelta::FlagAsDeletedJob
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'thinking_sphinx/deltas/sidekiq_delta'
|
2
|
+
|
3
|
+
namespace :thinking_sphinx do
|
4
|
+
desc 'Lock all delta indices (Resque will not run indexer or place new jobs on the :ts_delta queue).'
|
5
|
+
task :lock_deltas do
|
6
|
+
ThinkingSphinx::Deltas::SidekiqDelta::CoreIndex.new.lock_deltas
|
7
|
+
end
|
8
|
+
|
9
|
+
desc 'Unlock all delta indices.'
|
10
|
+
task :unlock_deltas do
|
11
|
+
ThinkingSphinx::Deltas::SidekiqDelta::CoreIndex.new.unlock_deltas
|
12
|
+
end
|
13
|
+
|
14
|
+
desc 'Like `rake thinking_sphinx:index`, but locks one index at a time.'
|
15
|
+
task :smart_index => :app_env do
|
16
|
+
ret = ThinkingSphinx::Deltas::SidekiqDelta::CoreIndex.new.smart_index
|
17
|
+
|
18
|
+
abort("Indexing failed.") if ret != true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
namespace :ts do
|
23
|
+
desc 'Like `rake thinking_sphinx:index`, but locks one index at a time.'
|
24
|
+
task :si => 'thinking_sphinx:smart_index'
|
25
|
+
end
|
26
|
+
|
27
|
+
unless Rake::Task.task_defined?('ts:index')
|
28
|
+
require 'thinking_sphinx/tasks'
|
29
|
+
end
|
30
|
+
|
31
|
+
# Ensure that indexing does not conflict with ts-resque-delta delta jobs.
|
32
|
+
# Rake::Task['ts:index'].enhance ['thinking_sphinx:lock_deltas'] do
|
33
|
+
# Rake::Task['thinking_sphinx:unlock_deltas'].invoke
|
34
|
+
# end
|
35
|
+
|
36
|
+
# Rake::Task['ts:reindex'].enhance ['thinking_sphinx:lock_deltas'] do
|
37
|
+
# Rake::Task['thinking_sphinx:unlock_deltas'].invoke
|
38
|
+
# end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'acceptance/spec_helper'
|
2
|
+
|
3
|
+
describe 'SQL delta indexing', :live => true do
|
4
|
+
before :each do
|
5
|
+
Sidekiq.redis = { url: "resque://localhost:6379/", namespace: 'test' }
|
6
|
+
end
|
7
|
+
|
8
|
+
it "automatically indexes new records" do
|
9
|
+
guards = Book.create(
|
10
|
+
:title => 'Guards! Guards!', :author => 'Terry Pratchett'
|
11
|
+
)
|
12
|
+
index
|
13
|
+
|
14
|
+
Book.search('Terry Pratchett').to_a.should == [guards]
|
15
|
+
|
16
|
+
men = Book.create(
|
17
|
+
:title => 'Men At Arms', :author => 'Terry Pratchett'
|
18
|
+
)
|
19
|
+
work
|
20
|
+
sleep 0.25
|
21
|
+
|
22
|
+
Book.search('Terry Pratchett').to_a.should == [guards, men]
|
23
|
+
end
|
24
|
+
|
25
|
+
it "automatically indexes updated records" do
|
26
|
+
book = Book.create :title => 'Night Watch', :author => 'Harry Pritchett'
|
27
|
+
index
|
28
|
+
|
29
|
+
Book.search('Harry').to_a.should == [book]
|
30
|
+
|
31
|
+
book.reload.update_attributes(:author => 'Terry Pratchett')
|
32
|
+
work
|
33
|
+
sleep 0.25
|
34
|
+
|
35
|
+
Book.search('Terry').to_a.should == [book]
|
36
|
+
end
|
37
|
+
|
38
|
+
it "does not match on old values" do
|
39
|
+
book = Book.create :title => 'Night Watch', :author => 'Harry Pritchett'
|
40
|
+
index
|
41
|
+
|
42
|
+
Book.search('Harry').to_a.should == [book]
|
43
|
+
|
44
|
+
book.reload.update_attributes(:author => 'Terry Pratchett')
|
45
|
+
work
|
46
|
+
sleep 0.25
|
47
|
+
|
48
|
+
Book.search('Harry').should be_empty
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class SphinxController
|
2
|
+
def initialize
|
3
|
+
config.searchd.mysql41 = 9307
|
4
|
+
end
|
5
|
+
|
6
|
+
def setup
|
7
|
+
FileUtils.mkdir_p config.indices_location
|
8
|
+
config.render_to_file && index
|
9
|
+
|
10
|
+
ThinkingSphinx::Configuration.reset
|
11
|
+
ActiveSupport::Dependencies.clear
|
12
|
+
|
13
|
+
config.index_paths.each do |path|
|
14
|
+
Dir["#{path}/**/*.rb"].each { |file| $LOADED_FEATURES.delete file }
|
15
|
+
end
|
16
|
+
|
17
|
+
config.searchd.mysql41 = 9307
|
18
|
+
config.settings['quiet_deltas'] = true
|
19
|
+
config.settings['attribute_updates'] = true
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
config.controller.start
|
24
|
+
end
|
25
|
+
|
26
|
+
def stop
|
27
|
+
config.controller.stop
|
28
|
+
end
|
29
|
+
|
30
|
+
def index(*indices)
|
31
|
+
config.controller.index *indices
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def config
|
37
|
+
ThinkingSphinx::Configuration.instance
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SphinxHelpers
|
2
|
+
include Sidekiq::Util
|
3
|
+
|
4
|
+
def sphinx
|
5
|
+
@sphinx ||= SphinxController.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def index(*indices)
|
9
|
+
yield if block_given?
|
10
|
+
|
11
|
+
ThinkingSphinx::Deltas::SidekiqDelta.clear_thinking_sphinx_queues
|
12
|
+
sphinx.index *indices
|
13
|
+
sleep 0.25
|
14
|
+
end
|
15
|
+
|
16
|
+
def work
|
17
|
+
client = Redis.connect(:url => "resque://localhost:6379/")
|
18
|
+
namespace = Redis::Namespace.new('test', :redis => client)
|
19
|
+
|
20
|
+
Sidekiq::Client.registered_queues.each do |queue_name|
|
21
|
+
while message = namespace.lpop("queue:#{queue_name}")
|
22
|
+
message = JSON.parse(message)
|
23
|
+
message['class'].constantize.new.perform(*message['args'])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
RSpec.configure do |config|
|
30
|
+
config.include SphinxHelpers
|
31
|
+
|
32
|
+
config.before :all do |group|
|
33
|
+
sphinx.setup && sphinx.start if group.class.metadata[:live]
|
34
|
+
end
|
35
|
+
|
36
|
+
config.after :all do |group|
|
37
|
+
sphinx.stop if group.class.metadata[:live]
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
create_table(:books, :force => true) do |t|
|
3
|
+
t.string :title
|
4
|
+
t.string :author
|
5
|
+
t.integer :year
|
6
|
+
t.string :blurb_file
|
7
|
+
t.boolean :delta, :default => true, :null => false
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
|
11
|
+
create_table(:delayed_jobs, :force => true) do |t|
|
12
|
+
t.column :priority, :integer, :default => 0
|
13
|
+
t.column :attempts, :integer, :default => 0
|
14
|
+
t.column :handler, :text
|
15
|
+
t.column :last_error, :text
|
16
|
+
t.column :run_at, :datetime
|
17
|
+
t.column :locked_at, :datetime
|
18
|
+
t.column :failed_at, :datetime
|
19
|
+
t.column :locked_by, :string
|
20
|
+
t.column :queue, :string
|
21
|
+
t.column :created_at, :datetime
|
22
|
+
t.column :updated_at, :datetime
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
*.log
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
|
4
|
+
Bundler.require :default, :development
|
5
|
+
|
6
|
+
require 'thinking_sphinx/railtie'
|
7
|
+
|
8
|
+
Combustion.initialize! :active_record
|
9
|
+
|
10
|
+
root = File.expand_path File.dirname(__FILE__)
|
11
|
+
Dir["#{root}/support/**/*.rb"].each { |file| require file }
|
12
|
+
|
13
|
+
RSpec.configure do |config|
|
14
|
+
# enable filtering for examples
|
15
|
+
config.filter_run :wip => nil
|
16
|
+
config.run_all_when_everything_filtered = true
|
17
|
+
end
|
data/tasks/rails.rake
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '/../lib/thinking_sphinx/deltas/resque_delta/tasks')
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.name = 'ts-sidekiq-delta'
|
4
|
+
s.version = '0.1.0'
|
5
|
+
s.platform = Gem::Platform::RUBY
|
6
|
+
s.authors = ['Pat Allan', 'Aaron Gibralter', 'Danny Hawkins']
|
7
|
+
s.email = ['pat@freelancing-gods.com', 'danny.hawkins@gmail.com']
|
8
|
+
s.homepage = 'https://github.com/pat/ts-sidekiq-delta'
|
9
|
+
s.summary = %q{Thinking Sphinx - Sidekiq Deltas}
|
10
|
+
s.description = %q{Manage delta indexes via Sidekiq for Thinking Sphinx}
|
11
|
+
s.license = 'MIT'
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
s.add_dependency 'thinking-sphinx', '>= 3.0.0'
|
19
|
+
s.add_dependency 'sidekiq', '>= 2.5.4'
|
20
|
+
|
21
|
+
s.add_development_dependency 'activerecord', '>= 3.1.0'
|
22
|
+
s.add_development_dependency 'database_cleaner', '>= 0.5.2'
|
23
|
+
s.add_development_dependency 'mysql2', '>= 0.3.12b4'
|
24
|
+
s.add_development_dependency 'rake', '>= 0.8.7'
|
25
|
+
s.add_development_dependency 'rspec', '>= 2.11.0'
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ts-sidekiq-delta
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Pat Allan
|
8
|
+
- Aaron Gibralter
|
9
|
+
- Danny Hawkins
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-12-02 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: thinking-sphinx
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - '>='
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: 3.0.0
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: sidekiq
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - '>='
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 2.5.4
|
36
|
+
type: :runtime
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - '>='
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 2.5.4
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: activerecord
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - '>='
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 3.1.0
|
50
|
+
type: :development
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: 3.1.0
|
57
|
+
- !ruby/object:Gem::Dependency
|
58
|
+
name: database_cleaner
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - '>='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: 0.5.2
|
64
|
+
type: :development
|
65
|
+
prerelease: false
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 0.5.2
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: mysql2
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 0.3.12b4
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: 0.3.12b4
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: rake
|
87
|
+
requirement: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: 0.8.7
|
92
|
+
type: :development
|
93
|
+
prerelease: false
|
94
|
+
version_requirements: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: 0.8.7
|
99
|
+
- !ruby/object:Gem::Dependency
|
100
|
+
name: rspec
|
101
|
+
requirement: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - '>='
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: 2.11.0
|
106
|
+
type: :development
|
107
|
+
prerelease: false
|
108
|
+
version_requirements: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: 2.11.0
|
113
|
+
description: Manage delta indexes via Sidekiq for Thinking Sphinx
|
114
|
+
email:
|
115
|
+
- pat@freelancing-gods.com
|
116
|
+
- danny.hawkins@gmail.com
|
117
|
+
executables: []
|
118
|
+
extensions: []
|
119
|
+
extra_rdoc_files: []
|
120
|
+
files:
|
121
|
+
- .gitignore
|
122
|
+
- .rspec
|
123
|
+
- Gemfile
|
124
|
+
- Guardfile
|
125
|
+
- LICENSE
|
126
|
+
- README.markdown
|
127
|
+
- Rakefile
|
128
|
+
- config/redis-cucumber.conf
|
129
|
+
- cucumber.yml
|
130
|
+
- features/sidekiq_deltas.feature
|
131
|
+
- features/smart_indexing.feature
|
132
|
+
- features/step_definitions/common_steps.rb
|
133
|
+
- features/step_definitions/sidekiq_delta_steps.rb
|
134
|
+
- features/step_definitions/smart_indexing_steps.rb
|
135
|
+
- features/support/env.rb
|
136
|
+
- features/thinking_sphinx/database.example.yml
|
137
|
+
- features/thinking_sphinx/db/migrations/create_delayed_betas.rb
|
138
|
+
- features/thinking_sphinx/models/delayed_beta.rb
|
139
|
+
- lib/thinking_sphinx/deltas/sidekiq_delta.rb
|
140
|
+
- lib/thinking_sphinx/deltas/sidekiq_delta/delta_job.rb
|
141
|
+
- lib/thinking_sphinx/deltas/sidekiq_delta/flag_as_deleted_job.rb
|
142
|
+
- lib/thinking_sphinx/deltas/sidekiq_delta/railtie.rb
|
143
|
+
- lib/thinking_sphinx/deltas/sidekiq_delta/tasks.rb
|
144
|
+
- lib/ts-sidekiq-delta.rb
|
145
|
+
- spec/acceptance/sidekiq_deltas_spec.rb
|
146
|
+
- spec/acceptance/spec_helper.rb
|
147
|
+
- spec/acceptance/support/database_cleaner.rb
|
148
|
+
- spec/acceptance/support/sphinx_controller.rb
|
149
|
+
- spec/acceptance/support/sphinx_helpers.rb
|
150
|
+
- spec/internal/.gitignore
|
151
|
+
- spec/internal/app/indices/book_index.rb
|
152
|
+
- spec/internal/app/models/book.rb
|
153
|
+
- spec/internal/config/database.yml
|
154
|
+
- spec/internal/db/schema.rb
|
155
|
+
- spec/internal/log/.gitignore
|
156
|
+
- spec/spec_helper.rb
|
157
|
+
- tasks/rails.rake
|
158
|
+
- ts-sidekiq-delta.gemspec
|
159
|
+
homepage: https://github.com/pat/ts-sidekiq-delta
|
160
|
+
licenses:
|
161
|
+
- MIT
|
162
|
+
metadata: {}
|
163
|
+
post_install_message:
|
164
|
+
rdoc_options: []
|
165
|
+
require_paths:
|
166
|
+
- lib
|
167
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
168
|
+
requirements:
|
169
|
+
- - '>='
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
version: '0'
|
172
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
173
|
+
requirements:
|
174
|
+
- - '>='
|
175
|
+
- !ruby/object:Gem::Version
|
176
|
+
version: '0'
|
177
|
+
requirements: []
|
178
|
+
rubyforge_project:
|
179
|
+
rubygems_version: 2.1.11
|
180
|
+
signing_key:
|
181
|
+
specification_version: 4
|
182
|
+
summary: Thinking Sphinx - Sidekiq Deltas
|
183
|
+
test_files:
|
184
|
+
- features/sidekiq_deltas.feature
|
185
|
+
- features/smart_indexing.feature
|
186
|
+
- features/step_definitions/common_steps.rb
|
187
|
+
- features/step_definitions/sidekiq_delta_steps.rb
|
188
|
+
- features/step_definitions/smart_indexing_steps.rb
|
189
|
+
- features/support/env.rb
|
190
|
+
- features/thinking_sphinx/database.example.yml
|
191
|
+
- features/thinking_sphinx/db/migrations/create_delayed_betas.rb
|
192
|
+
- features/thinking_sphinx/models/delayed_beta.rb
|
193
|
+
- spec/acceptance/sidekiq_deltas_spec.rb
|
194
|
+
- spec/acceptance/spec_helper.rb
|
195
|
+
- spec/acceptance/support/database_cleaner.rb
|
196
|
+
- spec/acceptance/support/sphinx_controller.rb
|
197
|
+
- spec/acceptance/support/sphinx_helpers.rb
|
198
|
+
- spec/internal/.gitignore
|
199
|
+
- spec/internal/app/indices/book_index.rb
|
200
|
+
- spec/internal/app/models/book.rb
|
201
|
+
- spec/internal/config/database.yml
|
202
|
+
- spec/internal/db/schema.rb
|
203
|
+
- spec/internal/log/.gitignore
|
204
|
+
- spec/spec_helper.rb
|