akane 0.1.0 → 0.2.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 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: