akane 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0bd862e8bde263f78e295317635f8bb4eb0256cd
4
- data.tar.gz: 4b64ada85e74c5018bc7f013c3f37d850cccb7e5
3
+ metadata.gz: de30517cd16b9dd52bf5628602e20e22dfcb594a
4
+ data.tar.gz: 509875d93f4c1bcebfb950f289b206d69916f2ea
5
5
  SHA512:
6
- metadata.gz: 2015deae33ec6b5598a17743dc308f01ce8cf452819f1f2ca2334b1a37365897fb79c7f7d8676bb1618ebe5bc9ac98d3e224d39cd34738bad89cecb038029343
7
- data.tar.gz: fe3250c3bc69de4bafcad0f68ea3d488efdf0ee75d7718e04eabe27f3ce2a400a200fab02bb1fe124858dab17d430fa2555277278b38cf161ae38696e4f681da
6
+ metadata.gz: 98ec5786e6587eac236b868309b85fc9a8d1fc445b761f927e1085c04629fb7534105bee28bec99796af86b7aa2ba44ea9e87fb5f6a738370b5ca97e36a91038
7
+ data.tar.gz: 3c6ab75d276a089ce1a0486dfb608c3a09f029ece9096a94a98caf2927447ef275c9f64338e7f707ae96dd273da997b18e0d1b9cea35935e4d8e59c6720c4d2a
@@ -0,0 +1,21 @@
1
+ ---
2
+ consumer:
3
+ token: CONSUMER_TOKEN
4
+ secret: CONSUMER_SECRET
5
+ accounts:
6
+ your_account_name1:
7
+ token: ACCESS_TOKEN1
8
+ secret: SECRET_TOKEN1
9
+ your_account_name2:
10
+ token: ACCESS_TOKEN2
11
+ secret: SECRET_TOKEN2
12
+ storages:
13
+ - stdout
14
+ - file:
15
+ dir: /tmp/akane
16
+ sync_io: false
17
+ - elasticsearch:
18
+ host: localhost
19
+ index: akane
20
+ kuromoji: false
21
+ enable_es_log: false
@@ -21,6 +21,8 @@ Gem::Specification.new do |spec|
21
21
  spec.add_dependency "elasticsearch", "~> 0.4.1"
22
22
  spec.add_dependency "twitter", "~> 5.5.1"
23
23
  spec.add_dependency "oauth", ">= 0.4.7"
24
+ spec.add_dependency "sigdump"
25
+
24
26
 
25
27
  spec.add_development_dependency "bundler"
26
28
  spec.add_development_dependency "rake"
@@ -29,5 +29,15 @@ module Akane
29
29
  def logger
30
30
  Logger.new(@hash["log"] || $stdout)
31
31
  end
32
+
33
+ def log_direct(line)
34
+ if @hash["log"]
35
+ open(@hash["log"], 'a') do |io|
36
+ io.puts line
37
+ end
38
+ else
39
+ $stdout.puts line
40
+ end
41
+ end
32
42
  end
33
43
  end
@@ -35,7 +35,7 @@ module Akane
35
35
  [[definition, {}]]
36
36
  end
37
37
  end.map do |kind, config|
38
- @logger.info "Preparing... storgae - #{kind}"
38
+ @logger.info "Preparing... storage - #{kind}"
39
39
  require "akane/storages/#{kind}"
40
40
  Akane::Storages.const_get(kind.gsub(/(?:\A|_)(.)/) { $1.upcase }).new(
41
41
  config: config,
@@ -43,7 +43,11 @@ module Akane
43
43
  )
44
44
  end
45
45
 
46
- @recorder = Akane::Recorder.new(@storages, logger: @config.logger)
46
+ @recorder = Akane::Recorder.new(
47
+ @storages,
48
+ timeout: @config["timeout"] || 20,
49
+ logger: @config.logger
50
+ )
47
51
 
48
52
  @logger.info "Prepared with #{@storages.size} storage(s) and #{@receivers.size} receiver(s)"
49
53
  end
@@ -51,8 +55,38 @@ module Akane
51
55
  def start
52
56
  @logger.info "Starting receivers..."
53
57
  @receivers.each(&:start)
58
+
59
+ @logger.info "Assigning signal handlers..."
60
+ handle_signals
61
+
54
62
  @logger.info "Starting recorder..."
55
63
  @recorder.run
64
+
65
+ @logger.info "Recorder stopped. Waiting for storages..."
66
+ stop_storages
67
+ end
68
+
69
+ def handle_signals
70
+ @terminating = false
71
+
72
+ begin
73
+ require 'sigdump/setup'
74
+ rescue LoadError
75
+ end
76
+
77
+ on_interrupt = proc do
78
+ if @terminating
79
+ @config.log_direct "Terminating forcely..."
80
+ exit
81
+ else
82
+ @terminating = true
83
+ @config.log_direct "Gracefully stopping..."
84
+ @recorder.stop!
85
+ end
86
+ end
87
+
88
+ trap(:INT, on_interrupt)
89
+ trap(:TERM, on_interrupt)
56
90
  end
57
91
 
58
92
  def run
@@ -62,6 +96,22 @@ module Akane
62
96
  start()
63
97
  end
64
98
 
99
+ def stop_storages
100
+ @storages.each(&:stop!)
101
+ loop do
102
+ not_exitable = @storages.any? do |storage|
103
+ if storage.exitable?
104
+ false
105
+ else
106
+ @logger.debug "[status] #{storage.name}: #{storage.status || 'not exitable'.freeze}"
107
+ true
108
+ end
109
+ end
110
+ break unless not_exitable
111
+ sleep 1
112
+ end
113
+ end
114
+
65
115
  private
66
116
 
67
117
  def on_tweet(account, tweet)
@@ -26,19 +26,26 @@ module Akane
26
26
  @logger.info "Stream : Starting"
27
27
 
28
28
  @thread = Thread.new do
29
- stream.user do |obj|
30
- case obj
31
- when Twitter::Tweet
32
- invoke(:tweet, obj)
33
- when Twitter::DirectMessage
34
- invoke(:message, obj)
35
- when Twitter::Streaming::DeletedTweet
36
- invoke(:delete, obj.user_id, obj.id)
37
- when Twitter::Streaming::Event
38
- invoke(:event,
39
- 'event' => obj.name, 'source' => obj.source,
40
- 'target' => obj.target, 'target_object' => obj.target_object)
29
+ begin
30
+ stream.user do |obj|
31
+ case obj
32
+ when Twitter::Tweet
33
+ invoke(:tweet, obj)
34
+ when Twitter::DirectMessage
35
+ invoke(:message, obj)
36
+ when Twitter::Streaming::DeletedTweet
37
+ invoke(:delete, obj.user_id, obj.id)
38
+ when Twitter::Streaming::Event
39
+ invoke(:event,
40
+ 'event' => obj.name, 'source' => obj.source,
41
+ 'target' => obj.target, 'target_object' => obj.target_object)
42
+ end
41
43
  end
44
+ rescue Exception => e
45
+ raise e if defined?(Twitter::Streaming::MockClient)
46
+ @logger.error 'Error on stream'
47
+ @logger.error e.inspect
48
+ @logger.error e.backtrace
42
49
  end
43
50
  end
44
51
 
@@ -1,12 +1,17 @@
1
1
  require 'thread'
2
+ require 'timeout'
2
3
 
3
4
  module Akane
4
5
  class Recorder
5
- def initialize(storages, logger: Logger.new(nil))
6
+ class Stop < Exception; end # :nodoc:
7
+
8
+ def initialize(storages, timeout: 20, logger: Logger.new(nil))
6
9
  @storages = storages
7
10
  @logger = logger
8
11
  @queue = Queue.new
9
12
  @recently_performed = RoundrobinFlags.new(1000)
13
+ @timeout = timeout
14
+ @stop = false
10
15
  end
11
16
 
12
17
  def queue_length
@@ -14,21 +19,25 @@ module Akane
14
19
  end
15
20
 
16
21
  def record_tweet(account, tweet)
22
+ return self if @stop
17
23
  @queue << [:record_tweet, account, tweet]
18
24
  self
19
25
  end
20
26
 
21
27
  def mark_as_deleted(account, user_id, tweet_id)
28
+ return self if @stop
22
29
  @queue << [:mark_as_deleted, account, user_id, tweet_id]
23
30
  self
24
31
  end
25
32
 
26
33
  def record_message(account, message)
34
+ return self if @stop
27
35
  @queue << [:record_message, account, message]
28
36
  self
29
37
  end
30
38
 
31
39
  def record_event(account, event)
40
+ return self if @stop
32
41
  @queue << [:record_event, account, event]
33
42
  self
34
43
  end
@@ -50,9 +59,18 @@ module Akane
50
59
 
51
60
  @storages.each do |storage|
52
61
  begin
53
- storage.__send__(action, account, *payload)
62
+ timeout(@timeout) do
63
+ storage.__send__(action, account, *payload)
64
+ end
65
+
66
+ rescue Timeout::Error => e
67
+ raise e if raise_errors
68
+ @logger.warn "#{storage.name} (#{action}) timed out"
69
+
70
+ rescue Interrupt, SignalException, SystemExit => e
71
+ raise e
72
+
54
73
  rescue Exception => e
55
- raise e if e === Interrupt
56
74
  raise e if raise_errors
57
75
  @logger.error "Error while recorder performing to #{storage.inspect}: #{e.inspect}"
58
76
  @logger.error e.backtrace
@@ -61,16 +79,36 @@ module Akane
61
79
  end
62
80
 
63
81
  def run(raise_errors = false)
64
- loop do
65
- begin
66
- self.dequeue(raise_errors)
67
- rescue Exception => e
68
- raise e if Interrupt === e
69
- raise e if raise_errors
70
- @logger.error "Error while recorder dequing: #{e.inspect}"
71
- @logger.error e.backtrace
82
+ @running_thread = Thread.new do
83
+ loop do
84
+ begin
85
+ begin
86
+ self.dequeue(raise_errors)
87
+ rescue Interrupt, SignalException, Stop
88
+ end
89
+
90
+ if @stop
91
+ break if self.queue_length.zero?
92
+ @logger.info "processing queue: #{self.queue_length} remaining."
93
+ end
94
+ rescue Exception => e
95
+ raise e if raise_errors
96
+ @logger.error "Error while recorder dequing: #{e.inspect}"
97
+ @logger.error e.backtrace
98
+ end
72
99
  end
100
+
101
+ @logger.info "Recorder stopped."
102
+ @stop = false
73
103
  end
104
+
105
+ @running_thread.join
106
+ nil
107
+ end
108
+
109
+ def stop!
110
+ @stop = true
111
+ @running_thread.raise Stop
74
112
  end
75
113
 
76
114
  class RoundrobinFlags
@@ -4,6 +4,11 @@ module Akane
4
4
  def initialize(config: raise(ArgumentError, 'missing config'), logger: Logger.new($stdout))
5
5
  @config = config
6
6
  @logger = logger
7
+ @stop = false
8
+ end
9
+
10
+ def name
11
+ self.class.name
7
12
  end
8
13
 
9
14
  def record_tweet(account, tweet)
@@ -21,6 +26,18 @@ module Akane
21
26
  def record_message(account, message)
22
27
  raise NotImplementedError
23
28
  end
29
+
30
+ def stop!
31
+ @stop = true
32
+ end
33
+
34
+ def exitable?
35
+ true
36
+ end
37
+
38
+ def status
39
+ nil
40
+ end
24
41
  end
25
42
  end
26
43
  end
@@ -15,6 +15,10 @@ module Akane
15
15
  set_elasticsearch_up
16
16
  end
17
17
 
18
+ def name
19
+ @name ||= "#{self.class.name}:#{@config["host"]}/#{@index_name}"
20
+ end
21
+
18
22
  def record_tweet(account, tweet)
19
23
  tweet_hash = tweet.attrs
20
24
  tweet_hash[:deleted] = false
@@ -16,6 +16,10 @@ module Akane
16
16
  end
17
17
  end
18
18
 
19
+ def name
20
+ @name ||= "#{self.class.name}:#{@config["dir"]}"
21
+ end
22
+
19
23
  def record_tweet(account, tweet)
20
24
  timeline_io.puts "[#{tweet[:created_at].xmlschema}][#{account}] #{tweet[:user][:screen_name]}: " \
21
25
  "#{tweet[:text].gsub(/\r?\n/,' ')} (#{tweet[:user][:id]},#{tweet[:id]})"
@@ -1,3 +1,3 @@
1
1
  module Akane
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -22,7 +22,8 @@ describe Akane::Manager do
22
22
  "token" => "consumer-token", "secret" => "consumer-secret"
23
23
  },
24
24
  "accounts" => conf_accounts,
25
- "storages" => conf_storages
25
+ "storages" => conf_storages,
26
+ "timeout" => 72,
26
27
  ).tap { |_| _.stub(logger: Logger.new(nil)) }
27
28
  end
28
29
 
@@ -49,7 +50,7 @@ describe Akane::Manager do
49
50
  it "creates recorder with storages" do
50
51
  storage = double("storage")
51
52
  Akane::Storages::Mock.stub(new: storage)
52
- Akane::Recorder.should_receive(:new).with([storage], logger: config.logger).and_call_original
53
+ Akane::Recorder.should_receive(:new).with([storage], timeout: 72, logger: config.logger).and_call_original
53
54
 
54
55
  subject.prepare
55
56
  end
@@ -66,21 +66,39 @@ describe Akane::Recorder do
66
66
  end
67
67
 
68
68
  describe "#run" do
69
- before do
70
- @th = Thread.new { subject.run(true) }
71
- @th.abort_on_exception = true
72
- end
73
-
74
69
  it "continues dequeuing the queue" do
75
70
  storages[0].should_receive(:record_tweet).with('a', {:id => 42})
76
71
  storages[0].should_receive(:record_tweet).with('b', {:id => 43})
72
+
73
+ @th = Thread.new { subject.run(true) }
74
+ @th.abort_on_exception = true
75
+
76
+ 15.times { break if @th.status == "sleep"; sleep 0.1 }
77
+
77
78
  subject.record_tweet('a', :id => 42)
78
79
  subject.record_tweet('b', :id => 43)
79
- 10.times { break if subject.queue_length.zero?; sleep 0.1 }
80
- end
81
80
 
82
- after do
81
+ 15.times { break if subject.queue_length.zero?; sleep 0.1 }
83
82
  @th.kill if @th && @th.alive?
84
83
  end
85
84
  end
85
+
86
+ describe "#stop!" do
87
+ it "stops gracefully" do
88
+ storages[0].should_receive(:record_tweet).with('a', {:id => 42})
89
+ allow(storages[0]).to receive(:exitable?).and_return(true)
90
+
91
+ @th = Thread.new { subject.run(true) }
92
+ @th.abort_on_exception = true
93
+ 15.times { break if @th.status == "sleep"; sleep 0.1 }
94
+
95
+ subject.record_tweet('a', :id => 42)
96
+ subject.stop!
97
+ subject.record_tweet('b', :id => 42)
98
+
99
+ 15.times { break unless @th.alive?; sleep 0.1 }
100
+
101
+ expect(@th).not_to be_alive
102
+ end
103
+ end
86
104
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: akane
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shota Fukumori (sora_h)
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-01-15 00:00:00.000000000 Z
11
+ date: 2014-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: elasticsearch
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.4.7
55
+ - !ruby/object:Gem::Dependency
56
+ name: sigdump
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: bundler
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +136,7 @@ files:
122
136
  - LICENSE.txt
123
137
  - README.md
124
138
  - Rakefile
139
+ - akane.example.yml
125
140
  - akane.gemspec
126
141
  - bin/akane
127
142
  - lib/akane.rb
@@ -169,7 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
169
184
  version: '0'
170
185
  requirements: []
171
186
  rubyforge_project:
172
- rubygems_version: 2.2.0
187
+ rubygems_version: 2.2.2
173
188
  signing_key:
174
189
  specification_version: 4
175
190
  summary: Log your timeline to something
@@ -185,4 +200,3 @@ test_files:
185
200
  - spec/support/mock_tweetstream.rb
186
201
  - spec/support/mock_twitter_streaming.rb
187
202
  - spec/util_spec.rb
188
- has_rdoc: