From 34a300238eabf01123b21920b0c45c689929ff9b Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Fri, 27 Jun 2014 08:43:37 +0200 Subject: [PATCH] Init version of session unit tests. --- lib/sessions.rb | 454 ++++++++++++++++++++++++++----------- script/websocket-server.rb | 12 +- test/unit/session_test.rb | 160 +++++++++++++ 3 files changed, 482 insertions(+), 144 deletions(-) create mode 100644 test/unit/session_test.rb diff --git a/lib/sessions.rb b/lib/sessions.rb index 1368ad971..49e323182 100644 --- a/lib/sessions.rb +++ b/lib/sessions.rb @@ -18,6 +18,18 @@ module Sessions @@user_threads = {} @@client_threads = {} +=begin + +start new session + + Sessions.create( client_id, session_data, { :type => 'websocket' } ) + +returns + + true|false + +=end + def self.create( client_id, session, meta ) path = @path + '/' + client_id.to_s FileUtils.mkpath path @@ -29,7 +41,6 @@ module Sessions }, :meta => meta, } -# puts 'CREATE' + Marshal.dump(data) file.write Marshal.dump(data) } @@ -42,6 +53,307 @@ module Sessions end end +=begin + +list of all session + + client_ids = Sessions.sessions + +returns + + ['4711', '4712'] + +=end + + def self.sessions + path = @path + '/' + + # just make sure that spool path exists + if !File::exists?( path ) + FileUtils.mkpath path + end + + data = [] + Dir.foreach( path ) do |entry| + next if entry == '.' || entry == '..' || entry == 'spool' + data.push entry.to_s + end + data + end + +=begin + +list of all session + + Sessions.session_exists?(client_id) + +returns + + true|false + +=end + + def self.session_exists?(client_id) + client_ids = self.sessions + client_ids.include? client_id.to_s + end + +=begin + +list of all session with data + + client_ids_with_data = Sessions.list + +returns + + { + '4711' => { + :user => { + :id => 123, + }, + :meta => { + :type => 'websocket', + :last_ping => time_of_last_ping, + } + }, + '4712' => { + :user => { + :id => 124, + }, + :meta => { + :type => 'ajax', + :last_ping => time_of_last_ping, + } + }, + } + +=end + + def self.list + client_ids = self.sessions + session_list = {} + client_ids.each { |client_id| + data = self.get(client_id) + next if !data + session_list[client_id] = data + } + session_list + end + +=begin + +destroy session + + Sessions.destory?(client_id) + +returns + + true|false + +=end + + def self.destory( client_id ) + path = @path + '/' + client_id.to_s + FileUtils.rm_rf path + end + +=begin + +destroy idle session + + list_of_client_ids = Sessions.destory_idle_sessions + +returns + + ['4711', '4712'] + +=end + + def self.destory_idle_sessions(idle_time_in_min = 4) + list_of_closed_sessions = [] + clients = Sessions.list + clients.each { |client_id, client| + if ( client[:meta][:last_ping].to_i + ( 60 * idle_time_in_min ) ) < Time.now.to_i + list_of_closed_sessions.push client_id + Sessions.destory( client_id ) + end + } + list_of_closed_sessions + end + +=begin + +touch session + + Sessions.touch(client_id) + +returns + + true|false + +=end + + def self.touch( client_id ) + data = self.get(client_id) + return false if !data + path = @path + '/' + client_id.to_s + data[:meta][:last_ping] = Time.new.to_i.to_s + File.open( path + '/session', 'wb' ) { |file| + file.write Marshal.dump(data) + } + true + end + +=begin + +get session data + + data = Sessions.get(client_id) + +returns + + { + :user => { + :id => 123, + }, + :meta => { + :type => 'websocket', + :last_ping => time_of_last_ping, + } + } + +=end + + def self.get( client_id ) + session_file = @path + '/' + client_id.to_s + '/session' + data = nil + return if !File.exist? session_file + begin + File.open( session_file, 'rb' ) { |file| + file.flock( File::LOCK_EX ) + all = file.read + file.flock( File::LOCK_UN ) + data = Marshal.load( all ) + } + rescue Exception => e + File.delete(session_file) + puts "Error reading '#{session_file}':" + puts e.inspect + return + end + data + end + +=begin + +send message to client + + Sessions.send(client_id_of_recipient, data) + +returns + + true|false + +=end + + def self.send( client_id, data ) + path = @path + '/' + client_id.to_s + '/' + filename = 'send-' + Time.new().to_f.to_s# + '-' + rand(99999999).to_s + check = true + count = 0 + while check + if File::exists?( path + filename ) + count += 1 + filename = filename + '-' + count + else + check = false + end + end + return false if !File.directory? path + File.open( path + 'a-' + filename, 'wb' ) { |file| + file.flock( File::LOCK_EX ) + file.write data.to_json + file.flock( File::LOCK_UN ) + file.close + } + return false if !File.exists?( path + 'a-' + filename ) + FileUtils.mv( path + 'a-' + filename, path + filename ) + true + end + +=begin + +send message to all client + + Sessions.broadcast(data) + +returns + + true|false + +=end + + def self.broadcast( data ) + + # list all current clients + client_list = self.sessions + client_list.each {|client_id| + Sessions.send( client_id, data ) + } + true + end + +=begin + +get messages for client + + messages = Sessions.queue(client_id_of_recipient) + +returns + + [ + { + key1 => 'some data of message 1', + key2 => 'some data of message 1', + }, + { + key1 => 'some data of message 2', + key2 => 'some data of message 2', + }, + ] + +=end + + def self.queue( client_id ) + path = @path + '/' + client_id.to_s + '/' + data = [] + files = [] + Dir.foreach( path ) {|entry| + next if entry == '.' || entry == '..' + files.push entry + } + files.sort.each {|entry| + filename = path + '/' + entry + if /^send/.match( entry ) + data.push Sessions.queue_file_read( path, entry ) + end + } + data + end + + def self.queue_file_read( path, filename ) + file_old = path + filename + file_new = path + 'a-' + filename + FileUtils.mv( file_old, file_new ) + data = nil + all = '' + File.open( file_new, 'rb' ) { |file| + all = file.read + } + File.delete( file_new ) + JSON.parse( all ) + end + def self.spool_create( msg ) path = @path + '/spool/' FileUtils.mkpath path @@ -51,7 +363,6 @@ module Sessions :msg => msg, :timestamp => Time.now.to_i, } -# puts 'CREATE' + Marshal.dump(data) file.write data.to_json } end @@ -120,75 +431,6 @@ module Sessions return data end - def self.list - client_ids = self.sessions - session_list = {} - client_ids.each { |client_id| - data = self.get(client_id) - next if !data - session_list[client_id] = data - } - return session_list - end - - def self.touch( client_id ) - data = self.get(client_id) - return if !data - path = @path + '/' + client_id.to_s - data[:meta][:last_ping] = Time.new.to_i.to_s - File.open( path + '/session', 'wb' ) { |file| - file.write Marshal.dump(data) - } - return true - end - - def self.get( client_id ) - session_file = @path + '/' + client_id.to_s + '/session' - data = nil - return if !File.exist? session_file - begin - File.open( session_file, 'rb' ) { |file| - file.flock( File::LOCK_EX ) - all = file.read - file.flock( File::LOCK_UN ) - data = Marshal.load( all ) - } - rescue Exception => e - File.delete(session_file) - puts "Error reading '#{session_file}':" - puts e.inspect - return - end - return data - end - - def self.send( client_id, data ) - path = @path + '/' + client_id.to_s + '/' - filename = 'send-' + Time.new().to_f.to_s# + '-' + rand(99999999).to_s - check = true - count = 0 - while check - if File::exists?( path + filename ) - count += 1 - filename = filename + '-' + count -# filename = filename + '-' + rand(99999).to_s -# filename = filename + '-' + rand(99999).to_s - else - check = false - end - end - return false if !File.directory? path - File.open( path + 'a-' + filename, 'wb' ) { |file| - file.flock( File::LOCK_EX ) - file.write data.to_json - file.flock( File::LOCK_UN ) - file.close - } - return false if !File.exists?( path + 'a-' + filename ) - FileUtils.mv( path + 'a-' + filename, path + filename ) - return true - end - def self.jobs # just make sure that spool path exists @@ -248,12 +490,13 @@ module Sessions def self.thread_worker(user_id, try_count = 0, try_run_time = Time.now) puts "LOOP WORKER #{user_id} - #{try_count}" begin - Sessions::Worker.new(user_id) + Sessions::Worker.new(user_id) rescue => e puts "thread_worker exited with error #{ e.inspect }" sleep 10 begin - ActiveRecord::Base.connection.reconnect! +# ActiveRecord::Base.remove_connection + ActiveRecord::Base.connection_pool.reap rescue => e puts "Can't reconnect to database #{ e.inspect }" end @@ -285,7 +528,8 @@ module Sessions puts "thread_client exited with error #{ e.inspect }" sleep 10 begin - ActiveRecord::Base.connection.reconnect! +# ActiveRecord::Base.remove_connection + ActiveRecord::Base.connection_pool.reap rescue => e puts "Can't reconnect to database #{ e.inspect }" end @@ -309,66 +553,4 @@ module Sessions puts "/LOOP #{client_id} - #{try_count}" end - def self.sessions - path = @path + '/' - - # just make sure that spool path exists - if !File::exists?( path ) - FileUtils.mkpath path - end - - data = [] - Dir.foreach( path ) do |entry| - next if entry == '.' || entry == '..' || entry == 'spool' - data.push entry.to_s - end - return data - end - - def self.queue( client_id ) - path = @path + '/' + client_id.to_s + '/' - data = [] - files = [] - Dir.foreach( path ) {|entry| - next if entry == '.' || entry == '..' - files.push entry - } - files.sort.each {|entry| - filename = path + '/' + entry - if /^send/.match( entry ) - data.push Sessions.queue_file( path, entry ) - end - } - return data - end - - def self.queue_file( path, filename ) - file_old = path + filename - file_new = path + 'a-' + filename - FileUtils.mv( file_old, file_new ) - data = nil - all = '' - File.open( file_new, 'rb' ) { |file| - all = file.read - } - File.delete( file_new ) - data = JSON.parse( all ) - return data - end - - def self.broadcast( data ) - - # list all current clients - client_list = self.list - client_list.each {|local_client_id, local_client| - Sessions.send( local_client_id, data ) - } - return true - end - - def self.destory( client_id ) - path = @path + '/' + client_id.to_s - FileUtils.rm_rf path - end - end \ No newline at end of file diff --git a/script/websocket-server.rb b/script/websocket-server.rb index 2b487b54c..059c03b3d 100755 --- a/script/websocket-server.rb +++ b/script/websocket-server.rb @@ -311,14 +311,10 @@ EventMachine.run { end } - # ajax - clients = Sessions.list - clients.each { |client_id, client| - next if client[:meta][:type] == 'websocket' - if ( client[:meta][:last_ping].to_i + ( 60 * idle_time_in_min ) ) < Time.now.to_i - log 'notice', "closing idle ajax connection", client_id - Sessions.destory( client_id ) - end + # close unused sessions + clients = Sessions.destory_idle_sessions(idle_time_in_min) + clients.each { |client_id| + log 'notice', "closing idle connection", client_id } end diff --git a/test/unit/session_test.rb b/test/unit/session_test.rb new file mode 100644 index 000000000..24cc7d600 --- /dev/null +++ b/test/unit/session_test.rb @@ -0,0 +1,160 @@ +# encoding: utf-8 +require 'test_helper' + +class SessionTest < ActiveSupport::TestCase + test 'base' do + + # create users + roles = Role.where( :name => [ 'Agent'] ) + groups = Group.all + + UserInfo.current_user_id = 1 + agent1 = User.create_or_update( + :login => 'session-agent-1', + :firstname => 'Session', + :lastname => 'Agent 1', + :email => 'session-agent1@example.com', + :password => 'agentpw', + :active => true, + :roles => roles, + :groups => groups, + ) + agent2 = User.create_or_update( + :login => 'session-agent-2', + :firstname => 'Session', + :lastname => 'Agent 2', + :email => 'session-agent2@example.com', + :password => 'agentpw', + :active => true, + :roles => roles, + :groups => groups, + ) + agent3 = User.create_or_update( + :login => 'session-agent-3', + :firstname => 'Session', + :lastname => 'Agent 3', + :email => 'session-agent3@example.com', + :password => 'agentpw', + :active => true, + :roles => roles, + :groups => groups, + ) + + # create sessions + client_id1 = '1234' + client_id2 = '123456' + client_id3 = 'abc' + Sessions.destory(client_id1) + Sessions.destory(client_id2) + Sessions.destory(client_id3) + Sessions.create( client_id1, agent1.attributes, { :type => 'websocket' } ) + Sessions.create( client_id2, agent2.attributes, { :type => 'ajax' } ) + Sessions.create( client_id3, agent3.attributes, { :type => 'ajax' } ) + + # check if session exists + assert( Sessions.session_exists?(client_id1), "check if session exists" ) + assert( Sessions.session_exists?(client_id2), "check if session exists" ) + assert( Sessions.session_exists?(client_id3), "check if session exists" ) + + # check if session still exists after idle cleanup + sleep 1 + Sessions.destory_idle_sessions(1) + assert( Sessions.session_exists?(client_id1), "check if session exists after 1 sec" ) + assert( Sessions.session_exists?(client_id2), "check if session exists after 1 sec" ) + assert( Sessions.session_exists?(client_id3), "check if session exists after 1 sec" ) + + + + # check client threads + + + # check worker threads + + + + # check if session still exists after idle cleanup with touched sessions + sleep 62 + Sessions.touch(client_id1) + Sessions.touch(client_id2) + Sessions.touch(client_id3) + Sessions.destory_idle_sessions(1) + assert( Sessions.session_exists?(client_id1), "check if session exists after touch" ) + assert( Sessions.session_exists?(client_id2), "check if session exists after touch" ) + assert( Sessions.session_exists?(client_id3), "check if session exists after touch" ) + + # check session data + data = Sessions.get(client_id1) + assert( data[:meta], "check if meta exists" ) + assert( data[:user], "check if user exists" ) + assert_equal( data[:user][:id], agent1.id, "check if user id is correct" ) + + data = Sessions.get(client_id2) + assert( data[:meta], "check if meta exists" ) + assert( data[:user], "check if user exists" ) + assert_equal( data[:user][:id], agent2.id, "check if user id is correct" ) + + data = Sessions.get(client_id3) + assert( data[:meta], "check if meta exists" ) + assert( data[:user], "check if user exists" ) + assert_equal( data[:user][:id], agent3.id, "check if user id is correct" ) + + # send data to one client + Sessions.send( client_id1, { :msg => 'äöüß123' } ) + Sessions.send( client_id1, { :msg => 'äöüß1234' } ) + messages = Sessions.queue(client_id1) + assert_equal( 3, messages.count, 'messages count') + assert_equal( 'ws:login', messages[0]['event'], 'messages 1') + assert_equal( true, messages[0]['data']['success'], 'messages 1') + assert_equal( 'äöüß123', messages[1]['msg'], 'messages 2') + assert_equal( 'äöüß1234', messages[2]['msg'], 'messages 3') + + messages = Sessions.queue(client_id2) + assert_equal( messages.count, 1, 'messages count') + assert_equal( 'ws:login', messages[0]['event'], 'messages 1') + assert_equal( true, messages[0]['data']['success'], 'messages 1') + + messages = Sessions.queue(client_id3) + assert_equal( messages.count, 1, 'messages count') + assert_equal( 'ws:login', messages[0]['event'], 'messages 1') + assert_equal( true, messages[0]['data']['success'], 'messages 1') + + + # broadcast to all clients + Sessions.broadcast( { :msg => 'ooo123123123123123123'} ) + messages = Sessions.queue(client_id1) + assert_equal( messages.count, 1, 'messages count') + assert_equal( 'ooo123123123123123123', messages[0]['msg'], 'messages broadcast 1') + + messages = Sessions.queue(client_id2) + assert_equal( messages.count, 1, 'messages count') + assert_equal( 'ooo123123123123123123', messages[0]['msg'], 'messages broadcast 1') + + messages = Sessions.queue(client_id3) + assert_equal( messages.count, 1, 'messages count') + assert_equal( 'ooo123123123123123123', messages[0]['msg'], 'messages broadcast 1') + + + # check client threads + + + # check worker threads + + + # check if session still exists after idle cleanup + sleep 62 + client_ids = Sessions.destory_idle_sessions(1) + + # check client sessions + assert( !Sessions.session_exists?(client_id1), "check if session is removed" ) + assert( !Sessions.session_exists?(client_id2), "check if session is removed" ) + assert( !Sessions.session_exists?(client_id3), "check if session is removed" ) + + # check client threads + + + # check worker threads + + + + end +end