Improved streaming handling (fetch parent tweets via REST). Improved auto reconnect to stream if channel config has changed. Improved max import for streaming (already reached max import on initial config - took 15 min. to import first tweet). Changed fallback REST tweet search from 30 to 20 minutes.

This commit is contained in:
Martin Edenhofer 2017-06-01 08:23:53 +02:00
parent d77e73bb12
commit 8cef58b4da
6 changed files with 181 additions and 69 deletions

View file

@ -132,45 +132,57 @@ stream all accounts
def self.stream def self.stream
Thread.abort_on_exception = true Thread.abort_on_exception = true
auto_reconnect_after = 25
last_channels = [] last_channels = []
loop do loop do
logger.debug 'stream controll loop' logger.debug 'stream controll loop'
current_channels = [] current_channels = []
channels = Channel.where('active = ? AND area LIKE ?', true, '%::Account') channels = Channel.where('active = ? AND area LIKE ?', true, '%::Account')
channels.each { |channel| channels.each { |channel|
next if channel.options[:adapter] != 'twitter' next if channel.options[:adapter] != 'twitter'
channel_id = channel.id.to_s
current_channels.push channel_id
current_channels.push channel.id # exit it channel has changed or connection is older then 25 min.
if @@channel_stream[channel_id]
# exit it channel has changed if @@channel_stream[channel_id][:updated_at] != channel.updated_at
if @@channel_stream[channel.id] && @@channel_stream[channel.id][:updated_at] != channel.updated_at logger.info "channel (#{channel.id}) has changed, stop thread"
logger.debug "channel (#{channel.id}) has changed, restart thread" @@channel_stream[channel_id][:thread].exit
@@channel_stream[channel.id][:thread].exit @@channel_stream[channel_id][:thread].join
@@channel_stream[channel.id][:thread].join @@channel_stream[channel_id][:stream_instance].disconnect
@@channel_stream[channel.id][:stream_instance].disconnect @@channel_stream[channel_id] = false
@@channel_stream[channel.id] = false elsif @@channel_stream[channel_id][:started_at] && @@channel_stream[channel_id][:started_at] < Time.zone.now - auto_reconnect_after.minutes
logger.info "channel (#{channel.id}) reconnect - thread is older then #{auto_reconnect_after} minutes, restart thread"
@@channel_stream[channel_id][:thread].exit
@@channel_stream[channel_id][:thread].join
@@channel_stream[channel_id][:stream_instance].disconnect
@@channel_stream[channel_id] = false
end
end end
#logger.debug "thread for channel (#{channel.id}) already running" if @@channel_stream[channel.id] #logger.debug "thread for channel (#{channel.id}) already running" if channel_stream
next if @@channel_stream[channel.id] next if @@channel_stream[channel_id]
@@channel_stream[channel.id] = { @@channel_stream[channel_id] = {
updated_at: channel.updated_at updated_at: channel.updated_at,
started_at: Time.zone.now,
} }
# start channels with delay # start channels with delay
sleep @@channel_stream.count sleep @@channel_stream.count
# start threads for each channel # start threads for each channel
@@channel_stream[channel.id][:thread] = Thread.new { @@channel_stream[channel_id][:thread] = Thread.new {
begin begin
logger.info "Started stream channel for '#{channel.id}' (#{channel.area})..." logger.info "Started stream channel for '#{channel.id}' (#{channel.area})..."
@@channel_stream[channel.id][:stream_instance] = channel.stream_instance @@channel_stream[channel_id] ||= {}
@@channel_stream[channel.id][:stream_instance].stream @@channel_stream[channel_id][:stream_instance] = channel.stream_instance
@@channel_stream[channel.id][:stream_instance].disconnect @@channel_stream[channel_id][:stream_instance].stream
@@channel_stream[channel.id] = false @@channel_stream[channel_id][:stream_instance].disconnect
logger.debug " ...stopped thread for '#{channel.id}'" @@channel_stream[channel_id] = false
logger.info " ...stopped thread for '#{channel.id}'"
rescue => e rescue => e
error = "Can't use channel (#{channel.id}): #{e.inspect}" error = "Can't use channel (#{channel.id}): #{e.inspect}"
logger.error error logger.error error
@ -178,24 +190,24 @@ stream all accounts
channel.status_in = 'error' channel.status_in = 'error'
channel.last_log_in = error channel.last_log_in = error
channel.save channel.save
@@channel_stream[channel.id] = false @@channel_stream[channel_id] = false
end end
} }
} }
# cleanup deleted channels # cleanup deleted channels
last_channels.each { |channel_id| last_channels.each { |channel_id|
next if !@@channel_stream[channel_id] next if !@@channel_stream[channel_id.to_s]
next if current_channels.include?(channel_id) next if current_channels.include?(channel_id)
logger.debug "channel (#{channel_id}) not longer active, stop thread" logger.info "channel (#{channel_id}) not longer active, stop thread"
@@channel_stream[channel_id][:thread].exit @@channel_stream[channel_id.to_s][:thread].exit
@@channel_stream[channel_id][:thread].join @@channel_stream[channel_id.to_s][:thread].join
@@channel_stream[channel_id][:stream_instance].disconnect @@channel_stream[channel_id.to_s][:stream_instance].disconnect
@@channel_stream[channel_id] = false @@channel_stream[channel_id.to_s] = false
} }
last_channels = current_channels last_channels = current_channels
sleep 30 sleep 20
end end
end end

View file

@ -82,7 +82,7 @@ returns
# only fetch once in 30 minutes # only fetch once in 30 minutes
return true if !channel.preferences return true if !channel.preferences
return true if !channel.preferences[:last_fetch] return true if !channel.preferences[:last_fetch]
return false if channel.preferences[:last_fetch] > Time.zone.now - 30.minutes return false if channel.preferences[:last_fetch] > Time.zone.now - 20.minutes
true true
end end
@ -183,6 +183,24 @@ returns
=end =end
def stream def stream
sleep_on_unauthorized = 61
2.times { |loop_count|
begin
stream_start
rescue Twitter::Error::Unauthorized => e
Rails.logger.info "Unable to stream, try #{loop_count}, error #{e.inspect}"
if loop_count < 2
Rails.logger.info "wait for #{sleep_on_unauthorized} sec. and try it again"
sleep sleep_on_unauthorized
else
raise "Unable to stream, try #{loop_count}, error #{e.inspect}"
end
end
}
end
def stream_start
sync = @channel.options['sync'] sync = @channel.options['sync']
raise 'Need channel.options[\'sync\'] for account, but no params found' if !sync raise 'Need channel.options[\'sync\'] for account, but no params found' if !sync
@ -204,20 +222,21 @@ returns
next if tweet.class != Twitter::Tweet && tweet.class != Twitter::DirectMessage next if tweet.class != Twitter::Tweet && tweet.class != Twitter::DirectMessage
# wait until own posts are stored in local database to prevent importing own tweets # wait until own posts are stored in local database to prevent importing own tweets
sleep 4 next if @stream_client.locale_sender?(tweet) && own_tweet_already_imported?(tweet)
next if Ticket::Article.find_by(message_id: tweet.id) next if Ticket::Article.find_by(message_id: tweet.id)
# check direct message # check direct message
if tweet.class == Twitter::DirectMessage if tweet.class == Twitter::DirectMessage
if sync['direct_messages'] && sync['direct_messages']['group_id'] != '' if sync['direct_messages'] && sync['direct_messages']['group_id'] != ''
next if @stream_client.direct_message_limit_reached(tweet) next if @stream_client.direct_message_limit_reached(tweet, 2)
@stream_client.to_group(tweet, sync['direct_messages']['group_id'], @channel) @stream_client.to_group(tweet, sync['direct_messages']['group_id'], @channel)
end end
next next
end end
next if !track_retweets? && tweet.retweet? next if !track_retweets? && tweet.retweet?
next if @stream_client.tweet_limit_reached(tweet) next if @stream_client.tweet_limit_reached(tweet, 2)
# check if it's mention # check if it's mention
if sync['mentions'] && sync['mentions']['group_id'] != '' if sync['mentions'] && sync['mentions']['group_id'] != ''
@ -290,6 +309,9 @@ returns
Rails.logger.debug "tweet to old: #{tweet.id}/#{tweet.created_at}" Rails.logger.debug "tweet to old: #{tweet.id}/#{tweet.created_at}"
next next
end end
next if @rest_client.locale_sender?(tweet) && own_tweet_already_imported?(tweet)
next if Ticket::Article.find_by(message_id: tweet.id) next if Ticket::Article.find_by(message_id: tweet.id)
break if @rest_client.tweet_limit_reached(tweet) break if @rest_client.tweet_limit_reached(tweet)
@rest_client.to_group(tweet, search[:group_id], @channel) @rest_client.to_group(tweet, search[:group_id], @channel)
@ -351,4 +373,28 @@ returns
def track_retweets? def track_retweets?
@channel.options && @channel.options['sync'] && @channel.options['sync']['track_retweets'] @channel.options && @channel.options['sync'] && @channel.options['sync']['track_retweets']
end end
def own_tweet_already_imported?(tweet)
event_time = Time.zone.now
sleep 4
12.times { |loop_count|
if Ticket::Article.find_by(message_id: tweet.id)
Rails.logger.debug "Own tweet already imported, skipping tweet #{tweet.id}"
return true
end
count = Delayed::Job.where('created_at < ?', event_time).count
break if count.zero?
sleep_time = 2 * count
sleep_time = 5 if sleep_time > 5
Rails.logger.debug "Delay importing own tweets - sleep #{sleep_time} (loop #{loop_count})"
sleep sleep_time
}
if Ticket::Article.find_by(message_id: tweet.id)
Rails.logger.debug "Own tweet already imported, skipping tweet #{tweet.id}"
return true
end
false
end
end end

View file

@ -62,7 +62,7 @@ class TweetBase
user_data[:active] = true user_data[:active] = true
user_data[:role_ids] = Role.signup_role_ids user_data[:role_ids] = Role.signup_role_ids
user = User.create(user_data) user = User.create!(user_data)
end end
if user_data[:image_source] if user_data[:image_source]
@ -93,7 +93,7 @@ class TweetBase
if auth if auth
auth.update_attributes(auth_data) auth.update_attributes(auth_data)
else else
Authorization.create(auth_data) Authorization.create!(auth_data)
end end
user user
@ -128,10 +128,10 @@ class TweetBase
state = get_state(channel, tweet) state = get_state(channel, tweet)
Ticket.create( Ticket.create!(
customer_id: user.id, customer_id: user.id,
title: title, title: title,
group_id: group_id, group_id: group_id || Group.first.id,
state: state, state: state,
priority: Ticket::Priority.find_by(name: '2 normal'), priority: Ticket::Priority.find_by(name: '2 normal'),
preferences: { preferences: {
@ -235,29 +235,12 @@ class TweetBase
Rails.logger.debug 'import tweet' Rails.logger.debug 'import tweet'
ticket = nil
# use transaction # use transaction
if @connection_type == 'stream' if @connection_type == 'stream'
ActiveRecord::Base.connection.reconnect! ActiveRecord::Base.connection.reconnect!
# if sender is a system account, wait until twitter message id is stored
# on article to prevent two (own created & twitter created) articles
tweet_user = user(tweet)
Channel.where(area: 'Twitter::Account').each { |local_channel|
next if !local_channel.options
next if !local_channel.options[:user]
next if !local_channel.options[:user][:id]
next if local_channel.options[:user][:id].to_s != tweet_user.id.to_s
sleep 5
# return if tweet already exists (send via system)
if Ticket::Article.find_by(message_id: tweet.id)
Rails.logger.debug "Do not import tweet.id #{tweet.id}, article already exists"
return nil
end
}
end end
ticket = nil
Transaction.execute(reset_user_id: true) do Transaction.execute(reset_user_id: true) do
# check if parent exists # check if parent exists
@ -272,6 +255,11 @@ class TweetBase
ticket = existing_article.ticket ticket = existing_article.ticket
else else
begin begin
# in case of streaming mode, get parent tweet via REST client
if !@client && @auth
@client = TweetRest.new(@auth)
end
parent_tweet = @client.status(tweet.in_reply_to_status_id) parent_tweet = @client.status(tweet.in_reply_to_status_id)
ticket = to_group(parent_tweet, group_id, channel) ticket = to_group(parent_tweet, group_id, channel)
rescue Twitter::Error::NotFound, Twitter::Error::Forbidden => e rescue Twitter::Error::NotFound, Twitter::Error::Forbidden => e
@ -343,11 +331,12 @@ class TweetBase
Ticket::State.find_by(default_follow_up: true) Ticket::State.find_by(default_follow_up: true)
end end
def tweet_limit_reached(tweet) def tweet_limit_reached(tweet, factor = 1)
max_count = 120 max_count = 120
if @connection_type == 'stream' if @connection_type == 'stream'
max_count = 30 max_count = 30
end end
max_count = max_count * factor
type_id = Ticket::Article::Type.lookup(name: 'twitter status').id type_id = Ticket::Article::Type.lookup(name: 'twitter status').id
created_at = Time.zone.now - 15.minutes created_at = Time.zone.now - 15.minutes
created_count = Ticket::Article.where('created_at > ? AND type_id = ?', created_at, type_id).count created_count = Ticket::Article.where('created_at > ? AND type_id = ?', created_at, type_id).count
@ -358,11 +347,12 @@ class TweetBase
false false
end end
def direct_message_limit_reached(tweet) def direct_message_limit_reached(tweet, factor = 1)
max_count = 100 max_count = 100
if @connection_type == 'stream' if @connection_type == 'stream'
max_count = 40 max_count = 40
end end
max_count = max_count * factor
type_id = Ticket::Article::Type.lookup(name: 'twitter direct-message').id type_id = Ticket::Article::Type.lookup(name: 'twitter direct-message').id
created_at = Time.zone.now - 15.minutes created_at = Time.zone.now - 15.minutes
created_count = Ticket::Article.where('created_at > ? AND type_id = ?', created_at, type_id).count created_count = Ticket::Article.where('created_at > ? AND type_id = ?', created_at, type_id).count
@ -390,4 +380,17 @@ class TweetBase
preferences preferences
end end
def locale_sender?(tweet)
tweet_user = user(tweet)
Channel.where(area: 'Twitter::Account').each { |local_channel|
next if !local_channel.options
next if !local_channel.options[:user]
next if !local_channel.options[:user][:id]
next if local_channel.options[:user][:id].to_s != tweet_user.id.to_s
Rails.logger.debug "Tweet is sent by local account with user id #{tweet_user.id} and tweet.id #{tweet.id}"
return true
}
false
end
end end

View file

@ -6,6 +6,7 @@ class TweetStream < TweetBase
def initialize(auth) def initialize(auth)
@connection_type = 'stream' @connection_type = 'stream'
@auth = auth
@client = Twitter::Streaming::ClientCustom.new do |config| @client = Twitter::Streaming::ClientCustom.new do |config|
config.consumer_key = auth[:consumer_key] config.consumer_key = auth[:consumer_key]
config.consumer_secret = auth[:consumer_secret] config.consumer_secret = auth[:consumer_secret]

View file

@ -187,7 +187,7 @@ class TwitterBrowserTest < TestCase
) )
# wait till new streaming of channel is active # wait till new streaming of channel is active
sleep 60 sleep 80
# start tweet from customer # start tweet from customer
client = Twitter::REST::Client.new do |config| client = Twitter::REST::Client.new do |config|
@ -211,7 +211,6 @@ class TwitterBrowserTest < TestCase
) )
click(text: 'Unassigned & Open') click(text: 'Unassigned & Open')
sleep 6 # till overview is rendered
watch_for( watch_for(
css: '.content.active', css: '.content.active',

View file

@ -526,14 +526,15 @@ class TwitterTest < ActiveSupport::TestCase
tweet = client.update( tweet = client.update(
text, text,
) )
sleep 10
article = nil article = nil
2.times { 5.times {
Scheduler.worker(true)
article = Ticket::Article.find_by(message_id: tweet.id) article = Ticket::Article.find_by(message_id: tweet.id)
break if article break if article
ActiveRecord::Base.clear_all_connections! ActiveRecord::Base.clear_all_connections!
ActiveRecord::Base.connection.query_cache.clear ActiveRecord::Base.connection.query_cache.clear
sleep 15 sleep 10
} }
assert(article, "article from customer with text '#{text}' message_id '#{tweet.id}' created") assert(article, "article from customer with text '#{text}' message_id '#{tweet.id}' created")
assert_equal(customer_login, article.from, 'ticket article from') assert_equal(customer_login, article.from, 'ticket article from')
@ -551,9 +552,10 @@ class TwitterTest < ActiveSupport::TestCase
tweet = client.update( tweet = client.update(
text, text,
) )
sleep 10
article = nil article = nil
2.times { 5.times {
Scheduler.worker(true)
article = Ticket::Article.find_by(message_id: tweet.id) article = Ticket::Article.find_by(message_id: tweet.id)
break if article break if article
ActiveRecord::Base.clear_all_connections! ActiveRecord::Base.clear_all_connections!
@ -594,7 +596,7 @@ class TwitterTest < ActiveSupport::TestCase
assert(tweet_found, "found outbound '#{reply_text}' tweet '#{article.message_id}'") assert(tweet_found, "found outbound '#{reply_text}' tweet '#{article.message_id}'")
count = Ticket::Article.where(message_id: article.message_id).count count = Ticket::Article.where(message_id: article.message_id).count
assert_equal(1, count) assert_equal(1, count, "tweet #{article.message_id}")
channel_id = article.ticket.preferences[:channel_id] channel_id = article.ticket.preferences[:channel_id]
assert(channel_id) assert(channel_id)
@ -616,13 +618,12 @@ class TwitterTest < ActiveSupport::TestCase
text, text,
) )
assert(dm, "dm with ##{hash} created") assert(dm, "dm with ##{hash} created")
sleep 10
article = nil article = nil
2.times { 5.times {
Scheduler.worker(true)
article = Ticket::Article.find_by(message_id: dm.id) article = Ticket::Article.find_by(message_id: dm.id)
break if article break if article
ActiveRecord::Base.clear_all_connections!
ActiveRecord::Base.connection.query_cache.clear
sleep 10 sleep 10
} }
assert(article, "inbound article '#{text}' message_id '#{dm.id}' created") assert(article, "inbound article '#{text}' message_id '#{dm.id}' created")
@ -719,9 +720,8 @@ class TwitterTest < ActiveSupport::TestCase
retweet = client.retweet(tweet).first retweet = client.retweet(tweet).first
# fetch check system account # fetch check system account
sleep 15
article = nil article = nil
2.times { 4.times {
# check if ticket and article has been created # check if ticket and article has been created
article = Ticket::Article.find_by(message_id: retweet.id) article = Ticket::Article.find_by(message_id: retweet.id)
break if article break if article
@ -734,6 +734,57 @@ class TwitterTest < ActiveSupport::TestCase
thread.join thread.join
end end
test 'i restart stream after config of channel has changed' do
hash = "#citheo#{rand(999)}"
thread = Thread.new {
Channel.stream
sleep 10
item = {
term: hash,
group_id: group.id,
}
channel_thread = Channel.find(channel.id)
channel_thread[:options]['sync']['search'].push item
channel_thread.save!
}
sleep 60
# new tweet - by me_bauer
client = Twitter::REST::Client.new do |config|
config.consumer_key = consumer_key
config.consumer_secret = consumer_secret
config.access_token = customer_token
config.access_token_secret = customer_token_secret
end
hash = "#{hash_tag1} ##{hash_gen}"
text = "Today... #{rand_word} #{hash}"
tweet = client.update(
text,
)
article = nil
5.times {
Scheduler.worker(true)
article = Ticket::Article.find_by(message_id: tweet.id)
break if article
ActiveRecord::Base.clear_all_connections!
ActiveRecord::Base.connection.query_cache.clear
sleep 10
}
assert(article, "article from customer with text '#{text}' message_id '#{tweet.id}' created")
assert_equal(customer_login, article.from, 'ticket article from')
assert_nil(article.to, 'ticket article to')
thread.exit
thread.join
channel_thread = Channel.find(channel.id)
channel_thread[:options]['sync']['search'].pop
channel_thread.save!
end
def hash_gen def hash_gen
rand(999).to_s + (0...10).map { ('a'..'z').to_a[rand(26)] }.join rand(999).to_s + (0...10).map { ('a'..'z').to_a[rand(26)] }.join
end end