From ee80a7971a228ef51f6556d040f62fdf1f7dfa58 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 14 Oct 2011 15:07:01 -0400 Subject: [PATCH 001/136] AhnAcd -> AhnQueue --- ahn_acd.gemspec => ahn_queue.gemspec | 16 ++++++++-------- lib/ahn_acd.rb | 2 +- lib/ahn_acd/queue_strategy.rb | 2 +- lib/ahn_acd/round_robin.rb | 2 +- lib/ahn_acd/round_robin_meetme.rb | 2 +- spec/ahn_acd_spec.rb | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) rename ahn_acd.gemspec => ahn_queue.gemspec (72%) diff --git a/ahn_acd.gemspec b/ahn_queue.gemspec similarity index 72% rename from ahn_acd.gemspec rename to ahn_queue.gemspec index e275858..71020e5 100644 --- a/ahn_acd.gemspec +++ b/ahn_queue.gemspec @@ -1,14 +1,14 @@ GEM_FILES = %w{ - ahn_acd.gemspec - lib/ahn_acd.rb - lib/ahn_acd/queue_strategy.rb - lib/ahn_acd/round_robin.rb - lib/ahn_acd/round_robin_meetme.rb - config/ahn_acd.yml + ahn_queue.gemspec + lib/ahn_queue.rb + lib/ahn_queue/queue_strategy.rb + lib/ahn_queue/round_robin.rb + lib/ahn_queue/round_robin_meetme.rb + config/ahn_queue.yml } Gem::Specification.new do |s| - s.name = "ahn_acd" + s.name = "ahn_queue" s.version = "0.0.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= @@ -21,7 +21,7 @@ Gem::Specification.new do |s| s.files = GEM_FILES s.has_rdoc = true - s.homepage = "http://github.com/adhearsion/ahn_acd" + s.homepage = "http://github.com/adhearsion/ahn_queue" s.require_paths = ["lib"] s.rubygems_version = "1.2.0" s.summary = "Automatic Call Distributor for Adhearsion" diff --git a/lib/ahn_acd.rb b/lib/ahn_acd.rb index 3fa396d..d1034f0 100644 --- a/lib/ahn_acd.rb +++ b/lib/ahn_acd.rb @@ -2,7 +2,7 @@ #end # #initialization do -# COMPONENTS.ahn_acd[:queues].each do |q| +# COMPONENTS.ahn_queue[:queues].each do |q| # AhnQueue.create q[:name], q[:queue_type], q[:agent_type] # end #end diff --git a/lib/ahn_acd/queue_strategy.rb b/lib/ahn_acd/queue_strategy.rb index d466fa8..c50ead4 100644 --- a/lib/ahn_acd/queue_strategy.rb +++ b/lib/ahn_acd/queue_strategy.rb @@ -1,6 +1,6 @@ require 'countdownlatch' -class AhnAcd +class AhnQueue module QueueStrategy def wrap_call(call) QueuedCall.new(call) unless call.respond_to?(:queued_time) diff --git a/lib/ahn_acd/round_robin.rb b/lib/ahn_acd/round_robin.rb index 006d781..6e5c57b 100644 --- a/lib/ahn_acd/round_robin.rb +++ b/lib/ahn_acd/round_robin.rb @@ -1,6 +1,6 @@ require 'thread' -class AhnAcd +class AhnQueue class RoundRobin include QueueStrategy diff --git a/lib/ahn_acd/round_robin_meetme.rb b/lib/ahn_acd/round_robin_meetme.rb index 4436686..2b6460a 100644 --- a/lib/ahn_acd/round_robin_meetme.rb +++ b/lib/ahn_acd/round_robin_meetme.rb @@ -1,4 +1,4 @@ -class AhnAcd +class AhnQueue class RoundRobinMeetme include QueueStrategy diff --git a/spec/ahn_acd_spec.rb b/spec/ahn_acd_spec.rb index a92c4eb..da23cd7 100644 --- a/spec/ahn_acd_spec.rb +++ b/spec/ahn_acd_spec.rb @@ -5,4 +5,4 @@ require 'adhearsion/component_manager/spec_framework' -component_name.upcase = ComponentTester.new("ahn_acd", File.dirname(__FILE__) + "/../..") +component_name.upcase = ComponentTester.new("ahn_queue", File.dirname(__FILE__) + "/../..") From e1c55d7d5c12af274b94d5035f4716d84b20144f Mon Sep 17 00:00:00 2001 From: Taylor Carpenter Date: Fri, 14 Oct 2011 14:37:55 -0500 Subject: [PATCH 002/136] renaming acd stuff to _queue --- lib/{ahn_acd.rb => ahn_queue.rb} | 0 lib/{ahn_acd => ahn_queue}/queue_strategy.rb | 0 lib/{ahn_acd => ahn_queue}/round_robin.rb | 0 lib/{ahn_acd => ahn_queue}/round_robin_meetme.rb | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename lib/{ahn_acd.rb => ahn_queue.rb} (100%) rename lib/{ahn_acd => ahn_queue}/queue_strategy.rb (100%) rename lib/{ahn_acd => ahn_queue}/round_robin.rb (100%) rename lib/{ahn_acd => ahn_queue}/round_robin_meetme.rb (100%) diff --git a/lib/ahn_acd.rb b/lib/ahn_queue.rb similarity index 100% rename from lib/ahn_acd.rb rename to lib/ahn_queue.rb diff --git a/lib/ahn_acd/queue_strategy.rb b/lib/ahn_queue/queue_strategy.rb similarity index 100% rename from lib/ahn_acd/queue_strategy.rb rename to lib/ahn_queue/queue_strategy.rb diff --git a/lib/ahn_acd/round_robin.rb b/lib/ahn_queue/round_robin.rb similarity index 100% rename from lib/ahn_acd/round_robin.rb rename to lib/ahn_queue/round_robin.rb diff --git a/lib/ahn_acd/round_robin_meetme.rb b/lib/ahn_queue/round_robin_meetme.rb similarity index 100% rename from lib/ahn_acd/round_robin_meetme.rb rename to lib/ahn_queue/round_robin_meetme.rb From a34d3c835a2aa5055e51fb310f59afdce7a8871f Mon Sep 17 00:00:00 2001 From: Taylor Carpenter Date: Fri, 14 Oct 2011 14:44:24 -0500 Subject: [PATCH 003/136] ahn_queue spec dir --- spec/{ahn_acd_spec.rb => ahn_queue/ahn_queue_spec.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/{ahn_acd_spec.rb => ahn_queue/ahn_queue_spec.rb} (100%) diff --git a/spec/ahn_acd_spec.rb b/spec/ahn_queue/ahn_queue_spec.rb similarity index 100% rename from spec/ahn_acd_spec.rb rename to spec/ahn_queue/ahn_queue_spec.rb From f8b6262784d2e435b63a70b887fd9e0a2e90e18f Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 14 Oct 2011 15:55:41 -0400 Subject: [PATCH 004/136] We are an Inc. --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 053116e..0f9aa4a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011 The Adhearsion Foundation +Copyright (c) 2011 The Adhearsion Foundation, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: From 37c69370d61ad77cb14c3badf08f886e6bca8253 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 14 Oct 2011 15:57:15 -0400 Subject: [PATCH 005/136] README --- README.markdown | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 README.markdown diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..284686d --- /dev/null +++ b/README.markdown @@ -0,0 +1,3 @@ +AhnQueue - Automatic Call Distribution (ACD) Services for Adhearsion +==================================================================== + From 83a7f55b9c1854df450f4a9f6260886c67437e58 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 12:37:42 -0400 Subject: [PATCH 006/136] Rename for convention --- spec/{ahn_queue => }/ahn_queue_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename spec/{ahn_queue => }/ahn_queue_spec.rb (56%) diff --git a/spec/ahn_queue/ahn_queue_spec.rb b/spec/ahn_queue_spec.rb similarity index 56% rename from spec/ahn_queue/ahn_queue_spec.rb rename to spec/ahn_queue_spec.rb index da23cd7..79cbe9c 100644 --- a/spec/ahn_queue/ahn_queue_spec.rb +++ b/spec/ahn_queue_spec.rb @@ -5,4 +5,4 @@ require 'adhearsion/component_manager/spec_framework' -component_name.upcase = ComponentTester.new("ahn_queue", File.dirname(__FILE__) + "/../..") +#component_name.upcase = ComponentTester.new("ahn_queue", File.dirname(__FILE__) + "/../..") From 5c82abf4e947232c1d5ef783380c94604d8786ef Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 12:37:54 -0400 Subject: [PATCH 007/136] Spec framework --- ahn_queue.gemspec | 2 ++ spec/spec_helper.rb | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 spec/spec_helper.rb diff --git a/ahn_queue.gemspec b/ahn_queue.gemspec index 71020e5..7ee5d7e 100644 --- a/ahn_queue.gemspec +++ b/ahn_queue.gemspec @@ -27,6 +27,8 @@ Gem::Specification.new do |s| s.summary = "Automatic Call Distributor for Adhearsion" s.add_runtime_dependency 'adhearsion', ['~> 1.2.0'] + s.add_development_dependency 'rspec', ['>= 2.5.0'] + s.add_development_dependency 'flexmock', ['>= 0.9.0'] s.specification_version = 2 end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..8bfbd6d --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,23 @@ +$:.push File.join(File.dirname(__FILE__), '..', 'lib') + +%w{ + ahn_queue + ahn_queue/queued_call + ahn_queue/queue_strategy + ahn_queue/round_robin + rspec/core + flexmock + flexmock/rspec +}.each { |r| require r } + +RSpec.configure do |config| + config.mock_framework = :flexmock + config.filter_run_excluding :ignore => true + config.filter_run :focus => true + config.run_all_when_everything_filtered = true + config.color_enabled = true +end + +def dummy_call + Object.new +end From c691900e348cd7f832d58b8973497559a09a614a Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 12:38:16 -0400 Subject: [PATCH 008/136] Move QueuedCall to its own file --- lib/ahn_queue.rb | 78 ++++++++++++++---------------------- lib/ahn_queue/queued_call.rb | 21 ++++++++++ 2 files changed, 50 insertions(+), 49 deletions(-) create mode 100644 lib/ahn_queue/queued_call.rb diff --git a/lib/ahn_queue.rb b/lib/ahn_queue.rb index d1034f0..d1617bc 100644 --- a/lib/ahn_queue.rb +++ b/lib/ahn_queue.rb @@ -31,53 +31,33 @@ def self.method_missing(method, *args, &block) instance.send method, *args, &block end - class QueuedCall - attr_accessor :call, :queued_time - - def initialize(call) - @call = call - @queued_time = Time.now - end - - def hold - call.execute 'StartMusicOnHold' - @latch = CountDownLatch.new 1 - @latch.wait - call.execute 'StopMusicOnHold' - end - - def make_ready! - @latch.countdown! - end - end - - class Agent - def work(agent_call) - loop do - agent_call.execute 'Bridge', @queue.next_call - end - end - end - - class CalloutAgent - def work(agent_channel) - @queue.next_call.each do |next_call| - next_call.dial agent_channel - end - end - end - - class MeetMeAgent - include Agent - - def work(agent_call) - loop do - agent_call.join agent_conf, @queue.next_call - end - end - end - - class BridgeAgent - include Agent - end +# module Agent +# def work(agent_call) +# loop do +# agent_call.execute 'Bridge', @queue.next_call +# end +# end +# end +# +# class CalloutAgent +# def work(agent_channel) +# @queue.next_call.each do |next_call| +# next_call.dial agent_channel +# end +# end +# end +# +# class MeetMeAgent +# include Agent +# +# def work(agent_call) +# loop do +# agent_call.join agent_conf, @queue.next_call +# end +# end +# end +# +# class BridgeAgent +# include Agent +# end end diff --git a/lib/ahn_queue/queued_call.rb b/lib/ahn_queue/queued_call.rb new file mode 100644 index 0000000..9b9d076 --- /dev/null +++ b/lib/ahn_queue/queued_call.rb @@ -0,0 +1,21 @@ +class AhnQueue + class QueuedCall + attr_accessor :call, :queued_time + + def initialize(call) + @call = call + @queued_time = Time.now + end + + def hold + call.execute 'StartMusicOnHold' + @latch = CountDownLatch.new 1 + @latch.wait + call.execute 'StopMusicOnHold' + end + + def make_ready! + @latch.countdown! + end + end +end From 8fb6fbe0112799be600fec6a9acdad6c5b034967 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 12:38:41 -0400 Subject: [PATCH 009/136] Initial specs for queue_strategy and round_robin --- spec/ahn_queue/queue_strategy_spec.rb | 16 ++++++++++++++++ spec/ahn_queue/round_robin_spec.rb | 14 ++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 spec/ahn_queue/queue_strategy_spec.rb create mode 100644 spec/ahn_queue/round_robin_spec.rb diff --git a/spec/ahn_queue/queue_strategy_spec.rb b/spec/ahn_queue/queue_strategy_spec.rb new file mode 100644 index 0000000..5a3386e --- /dev/null +++ b/spec/ahn_queue/queue_strategy_spec.rb @@ -0,0 +1,16 @@ +require File.join(File.dirname(__FILE__), '..', 'spec_helper') + +describe AhnQueue::QueueStrategy do + include AhnQueue::QueueStrategy + + describe '#wrap_call' do + it 'should pass through a QueuedCall object' do + obj = AhnQueue::QueuedCall.new dummy_call + wrap_call(obj).should be obj + end + + it 'should wrap any object that does not respond to #queued_time' do + wrap_call(dummy_call).should be_a AhnQueue::QueuedCall + end + end +end diff --git a/spec/ahn_queue/round_robin_spec.rb b/spec/ahn_queue/round_robin_spec.rb new file mode 100644 index 0000000..25bb998 --- /dev/null +++ b/spec/ahn_queue/round_robin_spec.rb @@ -0,0 +1,14 @@ +require File.join(File.dirname(__FILE__), '..', 'spec_helper') + +describe AhnQueue::RoundRobin do + before :each do + @queue = AhnQueue::RoundRobin.new + @call = AhnQueue::QueuedCall.new dummy_call + end + + it 'should properly enqueue a call' do + flexmock(@call).should_receive(:hold).once + @queue.enqueue @call + @queue.instance_variable_get(:@queue).first.should be @call + end +end From ab5ed120091cf2f9bedf5ec405939ee645683c34 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 12:38:48 -0400 Subject: [PATCH 010/136] Make specs pass --- lib/ahn_queue.rb | 5 ++++- lib/ahn_queue/queue_strategy.rb | 4 +++- lib/ahn_queue/round_robin.rb | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/ahn_queue.rb b/lib/ahn_queue.rb index d1617bc..e192676 100644 --- a/lib/ahn_queue.rb +++ b/lib/ahn_queue.rb @@ -1,3 +1,6 @@ +require 'singleton' +require 'adhearsion/foundation/thread_safety' + #methods_for :dialplan do #end # @@ -8,7 +11,7 @@ #end class AhnQueue - include Singleton + include ::Singleton def initialize @queues = {} diff --git a/lib/ahn_queue/queue_strategy.rb b/lib/ahn_queue/queue_strategy.rb index c50ead4..33a61ff 100644 --- a/lib/ahn_queue/queue_strategy.rb +++ b/lib/ahn_queue/queue_strategy.rb @@ -3,10 +3,12 @@ class AhnQueue module QueueStrategy def wrap_call(call) - QueuedCall.new(call) unless call.respond_to?(:queued_time) + call = QueuedCall.new(call) unless call.respond_to?(:queued_time) + call end def priority_enqueue(call) + # TODO: Add this call to the front of the line enqueue call end diff --git a/lib/ahn_queue/round_robin.rb b/lib/ahn_queue/round_robin.rb index 6e5c57b..c8e50ad 100644 --- a/lib/ahn_queue/round_robin.rb +++ b/lib/ahn_queue/round_robin.rb @@ -1,4 +1,5 @@ require 'thread' +require 'ahn_queue/queue_strategy' class AhnQueue class RoundRobin From a1c70221b4bf25565b50e2a623aec832e1c45ac2 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 13:08:26 -0400 Subject: [PATCH 011/136] Move require to correct file --- lib/ahn_queue/queue_strategy.rb | 2 -- lib/ahn_queue/queued_call.rb | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ahn_queue/queue_strategy.rb b/lib/ahn_queue/queue_strategy.rb index 33a61ff..5843699 100644 --- a/lib/ahn_queue/queue_strategy.rb +++ b/lib/ahn_queue/queue_strategy.rb @@ -1,5 +1,3 @@ -require 'countdownlatch' - class AhnQueue module QueueStrategy def wrap_call(call) diff --git a/lib/ahn_queue/queued_call.rb b/lib/ahn_queue/queued_call.rb index 9b9d076..200b6e8 100644 --- a/lib/ahn_queue/queued_call.rb +++ b/lib/ahn_queue/queued_call.rb @@ -1,3 +1,5 @@ +require 'countdownlatch' + class AhnQueue class QueuedCall attr_accessor :call, :queued_time From 8547bfe1aee3d80f23746c4935347d8ba9e7c1cd Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 13:08:50 -0400 Subject: [PATCH 012/136] More queue specs --- spec/ahn_queue/round_robin_spec.rb | 47 +++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/spec/ahn_queue/round_robin_spec.rb b/spec/ahn_queue/round_robin_spec.rb index 25bb998..5cca7ae 100644 --- a/spec/ahn_queue/round_robin_spec.rb +++ b/spec/ahn_queue/round_robin_spec.rb @@ -4,11 +4,56 @@ before :each do @queue = AhnQueue::RoundRobin.new @call = AhnQueue::QueuedCall.new dummy_call + flexmock(@call).should_receive(:hold).once end it 'should properly enqueue a call' do - flexmock(@call).should_receive(:hold).once @queue.enqueue @call @queue.instance_variable_get(:@queue).first.should be @call end + + it 'should return the call object that is passed in' do + @queue.enqueue @call + flexmock(@call).should_receive(:make_ready!).once + @queue.next_call.should be @call + end + + it 'should block an agent requesting a call until a call becomes available' do + agent_thread = Thread.new { @queue.next_call } + + # Give the agent thread a chance to block... + sleep 0.5 + + condvar = @queue.instance_variable_get(:@conditional) + waiters = condvar.instance_variable_get(:@waiters) + waiters.count.should == 1 + + @queue.enqueue @call + + # Give the agent thread a chance to retrieve the call... + sleep 0.5 + waiters.count.should == 0 + agent_thread.kill + end + + it 'should unblock only one agent per call entering the queue' do + agent1_thread = Thread.new { @queue.next_call } + agent2_thread = Thread.new { @queue.next_call } + + # Give the agent threads a chance to block... + sleep 0.5 + + condvar = @queue.instance_variable_get(:@conditional) + waiters = condvar.instance_variable_get(:@waiters) + waiters.count.should == 2 + + flexmock(@call).should_receive(:make_ready!).once + @queue.enqueue @call + + # Give the agent thread a chance to retrieve the call... + sleep 0.5 + waiters.count.should == 1 + agent1_thread.kill + agent2_thread.kill + end end From 482efc6c5228c922a40f9fda18097925e621a934 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 13:31:10 -0400 Subject: [PATCH 013/136] Catch hidden exceptions in threads --- spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8bfbd6d..94a3881 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,5 @@ $:.push File.join(File.dirname(__FILE__), '..', 'lib') +Thread.abort_on_exception = true %w{ ahn_queue @@ -12,7 +13,6 @@ RSpec.configure do |config| config.mock_framework = :flexmock - config.filter_run_excluding :ignore => true config.filter_run :focus => true config.run_all_when_everything_filtered = true config.color_enabled = true From 2603934d8c86d163b34c122d7fe6a6a1d803b4ab Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 13:31:18 -0400 Subject: [PATCH 014/136] Fix now-broken test --- spec/ahn_queue/round_robin_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/ahn_queue/round_robin_spec.rb b/spec/ahn_queue/round_robin_spec.rb index 5cca7ae..5d853ee 100644 --- a/spec/ahn_queue/round_robin_spec.rb +++ b/spec/ahn_queue/round_robin_spec.rb @@ -19,6 +19,7 @@ end it 'should block an agent requesting a call until a call becomes available' do + flexmock(@call).should_receive(:make_ready!).once agent_thread = Thread.new { @queue.next_call } # Give the agent thread a chance to block... From 60de66488692b80da1a5a74ba6015c6b6bdc4784 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 13:31:27 -0400 Subject: [PATCH 015/136] Add specs for QueuedCall --- spec/ahn_queue/queued_call_spec.rb | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 spec/ahn_queue/queued_call_spec.rb diff --git a/spec/ahn_queue/queued_call_spec.rb b/spec/ahn_queue/queued_call_spec.rb new file mode 100644 index 0000000..be503ef --- /dev/null +++ b/spec/ahn_queue/queued_call_spec.rb @@ -0,0 +1,42 @@ +require File.join(File.dirname(__FILE__), '..', 'spec_helper') + +describe AhnQueue::QueuedCall do + it 'should initialize the queued_time to the current time' do + now = Time.now + flexmock(Time).should_receive(:now).once.and_return now + qcall = AhnQueue::QueuedCall.new dummy_call + qcall.instance_variable_get(:@queued_time).should == now + end + + it 'should start and stop music on hold when put on hold and released' do + # Both tests are combined here so we do not leave too many suspended threads lying about + queued_caller = dummy_call + flexmock(queued_caller).should_receive(:execute).once.with('StartMusicOnHold') + flexmock(queued_caller).should_receive(:execute).once.with('StopMusicOnHold') + qcall = AhnQueue::QueuedCall.new queued_caller + + # Place the call on hold and wait for it to enqueue + Thread.new { qcall.hold } + sleep 0.5 + + # Release the call from being on hold and sleep to ensure we get the Stop MOH signal + qcall.make_ready! + sleep 0.5 + end + + it 'should block the call when put on hold' do + queued_caller = dummy_call + flexmock(queued_caller).should_receive(:execute).once.with('StartMusicOnHold') + flexmock(queued_caller).should_receive(:execute).once.with('StopMusicOnHold') + qcall = AhnQueue::QueuedCall.new queued_caller + + hold_thread = Thread.new { qcall.hold } + + # Give the holding thread a chance to block... + sleep 0.5 + hold_thread.status.should == "sleep" + qcall.make_ready! + sleep 0.5 + hold_thread.status.should be false + end +end From 40b0ecdf8ce7a967dfdd43849b96aec804c72d86 Mon Sep 17 00:00:00 2001 From: Taylor Carpenter Date: Sun, 16 Oct 2011 13:18:06 -0500 Subject: [PATCH 016/136] Merged with bk. added activesupport autoload. added component name back, etc. enabled Agent as module again --- ahn_queue.gemspec | 13 ++--- lib/ahn_queue.rb | 87 +++++++++++++++++++----------- lib/ahn_queue/round_robin.rb | 4 +- spec/ahn_queue/round_robin_spec.rb | 12 ++++- spec/ahn_queue_spec.rb | 9 +--- spec/spec_helper.rb | 6 +++ 6 files changed, 81 insertions(+), 50 deletions(-) diff --git a/ahn_queue.gemspec b/ahn_queue.gemspec index 7ee5d7e..bdbe9e5 100644 --- a/ahn_queue.gemspec +++ b/ahn_queue.gemspec @@ -1,12 +1,3 @@ -GEM_FILES = %w{ - ahn_queue.gemspec - lib/ahn_queue.rb - lib/ahn_queue/queue_strategy.rb - lib/ahn_queue/round_robin.rb - lib/ahn_queue/round_robin_meetme.rb - config/ahn_queue.yml -} - Gem::Specification.new do |s| s.name = "ahn_queue" s.version = "0.0.1" @@ -18,7 +9,7 @@ Gem::Specification.new do |s| s.description = "Automatic Call Distributor (ACD) for Adhearsion. Currently implements only Round Robin distribution strategies." s.email = "dev&adhearsion.com" - s.files = GEM_FILES + s.files = `git ls-files`.split("\n") s.has_rdoc = true s.homepage = "http://github.com/adhearsion/ahn_queue" @@ -27,6 +18,8 @@ Gem::Specification.new do |s| s.summary = "Automatic Call Distributor for Adhearsion" s.add_runtime_dependency 'adhearsion', ['~> 1.2.0'] + s.add_runtime_dependency 'countdownlatch' + s.add_runtime_dependency 'activesupport' s.add_development_dependency 'rspec', ['>= 2.5.0'] s.add_development_dependency 'flexmock', ['>= 0.9.0'] diff --git a/lib/ahn_queue.rb b/lib/ahn_queue.rb index e192676..ff135d3 100644 --- a/lib/ahn_queue.rb +++ b/lib/ahn_queue.rb @@ -1,4 +1,5 @@ require 'singleton' +require 'active_support/dependencies/autoload' require 'adhearsion/foundation/thread_safety' #methods_for :dialplan do @@ -11,7 +12,13 @@ #end class AhnQueue - include ::Singleton + extend ActiveSupport::Autoload + + autoload :QueueStrategy + autoload :RoundRobin + autoload :RoundRobinMeetme + + include Singleton def initialize @queues = {} @@ -34,33 +41,53 @@ def self.method_missing(method, *args, &block) instance.send method, *args, &block end -# module Agent -# def work(agent_call) -# loop do -# agent_call.execute 'Bridge', @queue.next_call -# end -# end -# end -# -# class CalloutAgent -# def work(agent_channel) -# @queue.next_call.each do |next_call| -# next_call.dial agent_channel -# end -# end -# end -# -# class MeetMeAgent -# include Agent -# -# def work(agent_call) -# loop do -# agent_call.join agent_conf, @queue.next_call -# end -# end -# end -# -# class BridgeAgent -# include Agent -# end + class QueuedCall + attr_accessor :call, :queued_time + + def initialize(call) + @call = call + @queued_time = Time.now + end + + def hold + call.execute 'StartMusicOnHold' + @latch = CountDownLatch.new 1 + @latch.wait + call.execute 'StopMusicOnHold' + end + + def make_ready! + @latch.countdown! + end + end + + module Agent + def work(agent_call) + loop do + agent_call.execute 'Bridge', @queue.next_call + end + end + end + + class CalloutAgent + def work(agent_channel) + @queue.next_call.each do |next_call| + next_call.dial agent_channel + end + end + end + + class MeetMeAgent + include Agent + + def work(agent_call) + loop do + agent_call.join agent_conf, @queue.next_call + end + end + end + + class BridgeAgent + include Agent + end end diff --git a/lib/ahn_queue/round_robin.rb b/lib/ahn_queue/round_robin.rb index c8e50ad..7df88ef 100644 --- a/lib/ahn_queue/round_robin.rb +++ b/lib/ahn_queue/round_robin.rb @@ -1,9 +1,11 @@ require 'thread' -require 'ahn_queue/queue_strategy' +#require 'ahn_queue/queue_strategy' +require 'ahn_queue' class AhnQueue class RoundRobin include QueueStrategy + attr_reader :queue, :conditional def initialize @queue = [] diff --git a/spec/ahn_queue/round_robin_spec.rb b/spec/ahn_queue/round_robin_spec.rb index 5d853ee..84eba56 100644 --- a/spec/ahn_queue/round_robin_spec.rb +++ b/spec/ahn_queue/round_robin_spec.rb @@ -1,4 +1,8 @@ -require File.join(File.dirname(__FILE__), '..', 'spec_helper') +require 'spec_helper' +#require File.join(File.dirname(__FILE__), '..', 'spec_helper') + +#class AhnQueue +# describe RoundRobin do describe AhnQueue::RoundRobin do before :each do @@ -7,6 +11,12 @@ flexmock(@call).should_receive(:hold).once end + describe "Queue is empty at start" do + pending + # subject { AhnQueue::RoundRobin.new } + # its(:queue) {should have(0).items } + end + it 'should properly enqueue a call' do @queue.enqueue @call @queue.instance_variable_get(:@queue).first.should be @call diff --git a/spec/ahn_queue_spec.rb b/spec/ahn_queue_spec.rb index 79cbe9c..f8ec369 100644 --- a/spec/ahn_queue_spec.rb +++ b/spec/ahn_queue_spec.rb @@ -1,8 +1 @@ -require 'rubygems' -require 'bundler' -Bundler.setup -Bundler.require - -require 'adhearsion/component_manager/spec_framework' - -#component_name.upcase = ComponentTester.new("ahn_queue", File.dirname(__FILE__) + "/../..") +require 'spec_helper' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 94a3881..c6f7a71 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,6 +2,8 @@ Thread.abort_on_exception = true %w{ + adhearsion + adhearsion/component_manager/spec_framework ahn_queue ahn_queue/queued_call ahn_queue/queue_strategy @@ -21,3 +23,7 @@ def dummy_call Object.new end + +component_name = "ahn_queue" + +AHN_QUEUE = ComponentTester.new(component_name, File.dirname(__FILE__) + "/../..", "/#{component_name}/lib/#{component_name}.rb") From 284467520e311addc137bd7100bdb13257489dbe Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 13:52:13 -0400 Subject: [PATCH 017/136] DRY --- spec/ahn_queue/round_robin_spec.rb | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/spec/ahn_queue/round_robin_spec.rb b/spec/ahn_queue/round_robin_spec.rb index 84eba56..2cd9bfd 100644 --- a/spec/ahn_queue/round_robin_spec.rb +++ b/spec/ahn_queue/round_robin_spec.rb @@ -5,10 +5,15 @@ # describe RoundRobin do describe AhnQueue::RoundRobin do + def dummy_queued_call + dqc = AhnQueue::QueuedCall.new dummy_call + flexmock(dqc).should_receive(:hold).once + flexmock(dqc).should_receive(:make_ready!).once + dqc + end + before :each do @queue = AhnQueue::RoundRobin.new - @call = AhnQueue::QueuedCall.new dummy_call - flexmock(@call).should_receive(:hold).once end describe "Queue is empty at start" do @@ -18,18 +23,20 @@ end it 'should properly enqueue a call' do - @queue.enqueue @call - @queue.instance_variable_get(:@queue).first.should be @call + call = AhnQueue::QueuedCall.new dummy_call + flexmock(call).should_receive(:hold).once + @queue.enqueue call + @queue.instance_variable_get(:@queue).first.should be call end it 'should return the call object that is passed in' do - @queue.enqueue @call - flexmock(@call).should_receive(:make_ready!).once - @queue.next_call.should be @call + call = dummy_queued_call + @queue.enqueue call + @queue.next_call.should be call end it 'should block an agent requesting a call until a call becomes available' do - flexmock(@call).should_receive(:make_ready!).once + call = dummy_queued_call agent_thread = Thread.new { @queue.next_call } # Give the agent thread a chance to block... @@ -39,7 +46,7 @@ waiters = condvar.instance_variable_get(:@waiters) waiters.count.should == 1 - @queue.enqueue @call + @queue.enqueue call # Give the agent thread a chance to retrieve the call... sleep 0.5 @@ -48,6 +55,7 @@ end it 'should unblock only one agent per call entering the queue' do + call = dummy_queued_call agent1_thread = Thread.new { @queue.next_call } agent2_thread = Thread.new { @queue.next_call } @@ -58,8 +66,7 @@ waiters = condvar.instance_variable_get(:@waiters) waiters.count.should == 2 - flexmock(@call).should_receive(:make_ready!).once - @queue.enqueue @call + @queue.enqueue call # Give the agent thread a chance to retrieve the call... sleep 0.5 From a5a8fc87bd28348ac8c3672cbeed1feaa82dd3b6 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 13:52:22 -0400 Subject: [PATCH 018/136] Test call retrieval order --- spec/ahn_queue/round_robin_spec.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/spec/ahn_queue/round_robin_spec.rb b/spec/ahn_queue/round_robin_spec.rb index 2cd9bfd..bb990a0 100644 --- a/spec/ahn_queue/round_robin_spec.rb +++ b/spec/ahn_queue/round_robin_spec.rb @@ -74,4 +74,23 @@ def dummy_queued_call agent1_thread.kill agent2_thread.kill end + + it 'should properly enqueue calls and return them in the same order' do + call1 = dummy_queued_call + call2 = dummy_queued_call + call3 = dummy_queued_call + threads = {} + + threads[:call1] = Thread.new { @queue.enqueue call1 } + sleep 0.5 + threads[:call2] = Thread.new { @queue.enqueue call2 } + sleep 0.5 + threads[:call3] = Thread.new { @queue.enqueue call3 } + sleep 0.5 + + + @queue.next_call.should be call1 + @queue.next_call.should be call2 + @queue.next_call.should be call3 + end end From 0ee25dc782d820a5c802bcf7c97d645de1b60afa Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 13:52:32 -0400 Subject: [PATCH 019/136] Fix call retrieval order --- lib/ahn_queue/round_robin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ahn_queue/round_robin.rb b/lib/ahn_queue/round_robin.rb index 7df88ef..079a435 100644 --- a/lib/ahn_queue/round_robin.rb +++ b/lib/ahn_queue/round_robin.rb @@ -16,7 +16,7 @@ def next_call call = nil synchronize do @conditional.wait(@mutex) if @queue.length == 0 - call = @queue.pop + call = @queue.shift end call.make_ready! From 28359635bf696211125362e8d4e7aff6f1b4108f Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 14:22:07 -0400 Subject: [PATCH 020/136] Remove accidentally duplicated code --- lib/ahn_queue.rb | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/lib/ahn_queue.rb b/lib/ahn_queue.rb index ff135d3..1b25a08 100644 --- a/lib/ahn_queue.rb +++ b/lib/ahn_queue.rb @@ -41,26 +41,6 @@ def self.method_missing(method, *args, &block) instance.send method, *args, &block end - class QueuedCall - attr_accessor :call, :queued_time - - def initialize(call) - @call = call - @queued_time = Time.now - end - - def hold - call.execute 'StartMusicOnHold' - @latch = CountDownLatch.new 1 - @latch.wait - call.execute 'StopMusicOnHold' - end - - def make_ready! - @latch.countdown! - end - end - module Agent def work(agent_call) loop do From 7b6560973ef42a9f595335c9946712e6bb669625 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 14:28:12 -0400 Subject: [PATCH 021/136] Fix requires, prune dead code --- lib/ahn_queue/round_robin.rb | 1 - spec/ahn_queue/queue_strategy_spec.rb | 2 +- spec/ahn_queue/queued_call_spec.rb | 2 +- spec/ahn_queue/round_robin_spec.rb | 4 ---- 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/ahn_queue/round_robin.rb b/lib/ahn_queue/round_robin.rb index 079a435..e9c3174 100644 --- a/lib/ahn_queue/round_robin.rb +++ b/lib/ahn_queue/round_robin.rb @@ -1,5 +1,4 @@ require 'thread' -#require 'ahn_queue/queue_strategy' require 'ahn_queue' class AhnQueue diff --git a/spec/ahn_queue/queue_strategy_spec.rb b/spec/ahn_queue/queue_strategy_spec.rb index 5a3386e..f88b67b 100644 --- a/spec/ahn_queue/queue_strategy_spec.rb +++ b/spec/ahn_queue/queue_strategy_spec.rb @@ -1,4 +1,4 @@ -require File.join(File.dirname(__FILE__), '..', 'spec_helper') +require 'spec_helper' describe AhnQueue::QueueStrategy do include AhnQueue::QueueStrategy diff --git a/spec/ahn_queue/queued_call_spec.rb b/spec/ahn_queue/queued_call_spec.rb index be503ef..fc39a23 100644 --- a/spec/ahn_queue/queued_call_spec.rb +++ b/spec/ahn_queue/queued_call_spec.rb @@ -1,4 +1,4 @@ -require File.join(File.dirname(__FILE__), '..', 'spec_helper') +require 'spec_helper' describe AhnQueue::QueuedCall do it 'should initialize the queued_time to the current time' do diff --git a/spec/ahn_queue/round_robin_spec.rb b/spec/ahn_queue/round_robin_spec.rb index bb990a0..c091a35 100644 --- a/spec/ahn_queue/round_robin_spec.rb +++ b/spec/ahn_queue/round_robin_spec.rb @@ -1,8 +1,4 @@ require 'spec_helper' -#require File.join(File.dirname(__FILE__), '..', 'spec_helper') - -#class AhnQueue -# describe RoundRobin do describe AhnQueue::RoundRobin do def dummy_queued_call From 1900718c1da6c4c37200a04c784f9016220b59bd Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 14:36:32 -0400 Subject: [PATCH 022/136] This test is passing --- spec/ahn_queue/round_robin_spec.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/ahn_queue/round_robin_spec.rb b/spec/ahn_queue/round_robin_spec.rb index c091a35..36f5b92 100644 --- a/spec/ahn_queue/round_robin_spec.rb +++ b/spec/ahn_queue/round_robin_spec.rb @@ -13,9 +13,8 @@ def dummy_queued_call end describe "Queue is empty at start" do - pending - # subject { AhnQueue::RoundRobin.new } - # its(:queue) {should have(0).items } + subject { AhnQueue::RoundRobin.new } + its(:queue) { should have(0).items } end it 'should properly enqueue a call' do From 6f7ef244da6cf05c4c741d0034317248e47c8085 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 16 Oct 2011 14:55:15 -0400 Subject: [PATCH 023/136] Fix path to tested component --- spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c6f7a71..35a05fb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -26,4 +26,4 @@ def dummy_call component_name = "ahn_queue" -AHN_QUEUE = ComponentTester.new(component_name, File.dirname(__FILE__) + "/../..", "/#{component_name}/lib/#{component_name}.rb") +AHN_QUEUE = ComponentTester.new(component_name, File.dirname(__FILE__) + "/..", "/lib/#{component_name}.rb") From f4180228b3072b24aeffccb32774f6b209c35bb5 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 4 Dec 2011 13:26:39 -0500 Subject: [PATCH 024/136] Migrate to Adhearsion 2.0 Plugin layout --- Gemfile | 2 + ahn_queue.gemspec => adhearsion-queue.gemspec | 6 +- lib/adhearsion/plugin/queue.rb | 68 +++++++++++++++++ lib/adhearsion/plugin/queue/queue_strategy.rb | 22 ++++++ lib/adhearsion/plugin/queue/queued_call.rb | 28 +++++++ lib/adhearsion/plugin/queue/round_robin.rb | 41 +++++++++++ .../plugin/queue/round_robin_meetme.rb | 38 ++++++++++ lib/ahn_queue.rb | 73 ------------------- lib/ahn_queue/queue_strategy.rb | 17 ----- lib/ahn_queue/queued_call.rb | 23 ------ lib/ahn_queue/round_robin.rb | 37 ---------- lib/ahn_queue/round_robin_meetme.rb | 35 --------- .../plugin/queue/queue_strategy_spec.rb | 16 ++++ .../plugin/queue}/queued_call_spec.rb | 8 +- .../plugin/queue}/round_robin_spec.rb | 10 +-- .../plugin/queue_spec.rb} | 0 spec/ahn_queue/queue_strategy_spec.rb | 16 ---- spec/spec_helper.rb | 12 +-- 18 files changed, 231 insertions(+), 221 deletions(-) rename ahn_queue.gemspec => adhearsion-queue.gemspec (85%) create mode 100644 lib/adhearsion/plugin/queue.rb create mode 100644 lib/adhearsion/plugin/queue/queue_strategy.rb create mode 100644 lib/adhearsion/plugin/queue/queued_call.rb create mode 100644 lib/adhearsion/plugin/queue/round_robin.rb create mode 100644 lib/adhearsion/plugin/queue/round_robin_meetme.rb delete mode 100644 lib/ahn_queue.rb delete mode 100644 lib/ahn_queue/queue_strategy.rb delete mode 100644 lib/ahn_queue/queued_call.rb delete mode 100644 lib/ahn_queue/round_robin.rb delete mode 100644 lib/ahn_queue/round_robin_meetme.rb create mode 100644 spec/adhearsion/plugin/queue/queue_strategy_spec.rb rename spec/{ahn_queue => adhearsion/plugin/queue}/queued_call_spec.rb (83%) rename spec/{ahn_queue => adhearsion/plugin/queue}/round_robin_spec.rb (88%) rename spec/{ahn_queue_spec.rb => adhearsion/plugin/queue_spec.rb} (100%) delete mode 100644 spec/ahn_queue/queue_strategy_spec.rb diff --git a/Gemfile b/Gemfile index e45e65f..99cdc59 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,4 @@ source :rubygems gemspec + +gem 'adhearsion', :git => 'https://github.com/adhearsion/adhearsion.git', :branch => :develop diff --git a/ahn_queue.gemspec b/adhearsion-queue.gemspec similarity index 85% rename from ahn_queue.gemspec rename to adhearsion-queue.gemspec index bdbe9e5..f1f4525 100644 --- a/ahn_queue.gemspec +++ b/adhearsion-queue.gemspec @@ -1,5 +1,5 @@ Gem::Specification.new do |s| - s.name = "ahn_queue" + s.name = "adhearsion-queue" s.version = "0.0.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= @@ -12,12 +12,12 @@ Gem::Specification.new do |s| s.files = `git ls-files`.split("\n") s.has_rdoc = true - s.homepage = "http://github.com/adhearsion/ahn_queue" + s.homepage = "http://github.com/adhearsion/adhearsion-queue" s.require_paths = ["lib"] s.rubygems_version = "1.2.0" s.summary = "Automatic Call Distributor for Adhearsion" - s.add_runtime_dependency 'adhearsion', ['~> 1.2.0'] + s.add_runtime_dependency 'adhearsion' s.add_runtime_dependency 'countdownlatch' s.add_runtime_dependency 'activesupport' s.add_development_dependency 'rspec', ['>= 2.5.0'] diff --git a/lib/adhearsion/plugin/queue.rb b/lib/adhearsion/plugin/queue.rb new file mode 100644 index 0000000..b516824 --- /dev/null +++ b/lib/adhearsion/plugin/queue.rb @@ -0,0 +1,68 @@ +require 'singleton' +require 'active_support/dependencies/autoload' +require 'adhearsion/foundation/thread_safety' + +module Adhearsion + class Plugin + class Queue < Adhearsion::Plugin + extend ActiveSupport::Autoload + + autoload :QueueStrategy + autoload :RoundRobin + autoload :RoundRobinMeetme + + include Singleton + + def initialize + @queues = {} + end + + def create(name, queue_type, agent_type = Agent) + synchronize do + @queues[name] = const_get(queue_type).new unless @queues.has_key?(name) + @queues[name].extend agent_type + end + end + + def get_queue(name) + synchronize do + @queues[name] + end + end + + def self.method_missing(method, *args, &block) + instance.send method, *args, &block + end + + module Agent + def work(agent_call) + loop do + agent_call.execute 'Bridge', @queue.next_call + end + end + end + + class CalloutAgent + def work(agent_channel) + @queue.next_call.each do |next_call| + next_call.dial agent_channel + end + end + end + + class MeetMeAgent + include Agent + + def work(agent_call) + loop do + agent_call.join agent_conf, @queue.next_call + end + end + end + + class BridgeAgent + include Agent + end + end + end +end diff --git a/lib/adhearsion/plugin/queue/queue_strategy.rb b/lib/adhearsion/plugin/queue/queue_strategy.rb new file mode 100644 index 0000000..0906141 --- /dev/null +++ b/lib/adhearsion/plugin/queue/queue_strategy.rb @@ -0,0 +1,22 @@ +module Adhearsion + class Plugin + class Queue + module QueueStrategy + def wrap_call(call) + call = QueuedCall.new(call) unless call.respond_to?(:queued_time) + call + end + + def priority_enqueue(call) + # TODO: Add this call to the front of the line + enqueue call + end + + def enqueue(call) + call.hold + end + end + end + end +end + diff --git a/lib/adhearsion/plugin/queue/queued_call.rb b/lib/adhearsion/plugin/queue/queued_call.rb new file mode 100644 index 0000000..d12a560 --- /dev/null +++ b/lib/adhearsion/plugin/queue/queued_call.rb @@ -0,0 +1,28 @@ +require 'countdownlatch' + +module Adhearsion + class Plugin + class Queue + class QueuedCall + attr_accessor :call, :queued_time + + def initialize(call) + @call = call + @queued_time = Time.now + end + + def hold + call.execute 'StartMusicOnHold' + @latch = CountDownLatch.new 1 + @latch.wait + call.execute 'StopMusicOnHold' + end + + def make_ready! + @latch.countdown! + end + end + end + end +end + diff --git a/lib/adhearsion/plugin/queue/round_robin.rb b/lib/adhearsion/plugin/queue/round_robin.rb new file mode 100644 index 0000000..3cb4dbb --- /dev/null +++ b/lib/adhearsion/plugin/queue/round_robin.rb @@ -0,0 +1,41 @@ +require 'thread' +require 'adhearsion/plugin/queue/queue_strategy' + +module Adhearsion + class Plugin + class Queue + class RoundRobin + include QueueStrategy + attr_reader :queue, :conditional + + def initialize + @queue = [] + @conditional = ConditionVariable.new + end + + def next_call + call = nil + synchronize do + @conditional.wait(@mutex) if @queue.length == 0 + call = @queue.shift + end + + call.make_ready! + call + end + + # TODO: Add mechanism to add calls with higher priority to the front of the queue. + + def enqueue(call) + call = wrap_call(call) + synchronize do + @queue << call + @conditional.signal if @queue.length == 1 + end + super + end + end + end + end +end + diff --git a/lib/adhearsion/plugin/queue/round_robin_meetme.rb b/lib/adhearsion/plugin/queue/round_robin_meetme.rb new file mode 100644 index 0000000..a5ab7f4 --- /dev/null +++ b/lib/adhearsion/plugin/queue/round_robin_meetme.rb @@ -0,0 +1,38 @@ +module Adhearsion + class Plugin + class Queue + class RoundRobinMeetme + include QueueStrategy + + def initialize(call) + @queue = [] + end + + def next_call + call = synchronize do + @queue.pop + end + + call.make_ready! + call + end + + def priority_enqueue(call) + call = wrap_call(call) + + synchronize do + @queue.unshift call + end + super + end + + def enqueue(call) + synchronize do + @queue << call + end + end + end + end + end +end + diff --git a/lib/ahn_queue.rb b/lib/ahn_queue.rb deleted file mode 100644 index 1b25a08..0000000 --- a/lib/ahn_queue.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'singleton' -require 'active_support/dependencies/autoload' -require 'adhearsion/foundation/thread_safety' - -#methods_for :dialplan do -#end -# -#initialization do -# COMPONENTS.ahn_queue[:queues].each do |q| -# AhnQueue.create q[:name], q[:queue_type], q[:agent_type] -# end -#end - -class AhnQueue - extend ActiveSupport::Autoload - - autoload :QueueStrategy - autoload :RoundRobin - autoload :RoundRobinMeetme - - include Singleton - - def initialize - @queues = {} - end - - def create(name, queue_type, agent_type = Agent) - synchronize do - @queues[name] = const_get(queue_type).new unless @queues.has_key?(name) - @queues[name].extend agent_type - end - end - - def get_queue(name) - synchronize do - @queues[name] - end - end - - def self.method_missing(method, *args, &block) - instance.send method, *args, &block - end - - module Agent - def work(agent_call) - loop do - agent_call.execute 'Bridge', @queue.next_call - end - end - end - - class CalloutAgent - def work(agent_channel) - @queue.next_call.each do |next_call| - next_call.dial agent_channel - end - end - end - - class MeetMeAgent - include Agent - - def work(agent_call) - loop do - agent_call.join agent_conf, @queue.next_call - end - end - end - - class BridgeAgent - include Agent - end -end diff --git a/lib/ahn_queue/queue_strategy.rb b/lib/ahn_queue/queue_strategy.rb deleted file mode 100644 index 5843699..0000000 --- a/lib/ahn_queue/queue_strategy.rb +++ /dev/null @@ -1,17 +0,0 @@ -class AhnQueue - module QueueStrategy - def wrap_call(call) - call = QueuedCall.new(call) unless call.respond_to?(:queued_time) - call - end - - def priority_enqueue(call) - # TODO: Add this call to the front of the line - enqueue call - end - - def enqueue(call) - call.hold - end - end -end diff --git a/lib/ahn_queue/queued_call.rb b/lib/ahn_queue/queued_call.rb deleted file mode 100644 index 200b6e8..0000000 --- a/lib/ahn_queue/queued_call.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'countdownlatch' - -class AhnQueue - class QueuedCall - attr_accessor :call, :queued_time - - def initialize(call) - @call = call - @queued_time = Time.now - end - - def hold - call.execute 'StartMusicOnHold' - @latch = CountDownLatch.new 1 - @latch.wait - call.execute 'StopMusicOnHold' - end - - def make_ready! - @latch.countdown! - end - end -end diff --git a/lib/ahn_queue/round_robin.rb b/lib/ahn_queue/round_robin.rb deleted file mode 100644 index e9c3174..0000000 --- a/lib/ahn_queue/round_robin.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'thread' -require 'ahn_queue' - -class AhnQueue - class RoundRobin - include QueueStrategy - attr_reader :queue, :conditional - - def initialize - @queue = [] - @conditional = ConditionVariable.new - end - - def next_call - call = nil - synchronize do - @conditional.wait(@mutex) if @queue.length == 0 - call = @queue.shift - end - - call.make_ready! - call - end - - # TODO: Add mechanism to add calls with higher priority to the front of the queue. - - def enqueue(call) - call = wrap_call(call) - synchronize do - @queue << call - @conditional.signal if @queue.length == 1 - end - super - end - end -end - diff --git a/lib/ahn_queue/round_robin_meetme.rb b/lib/ahn_queue/round_robin_meetme.rb deleted file mode 100644 index 2b6460a..0000000 --- a/lib/ahn_queue/round_robin_meetme.rb +++ /dev/null @@ -1,35 +0,0 @@ -class AhnQueue - class RoundRobinMeetme - include QueueStrategy - - def initialize(call) - @queue = [] - end - - def next_call - call = synchronize do - @queue.pop - end - - call.make_ready! - call - end - - def priority_enqueue(call) - call = wrap_call(call) - - synchronize do - @queue.unshift call - end - super - end - - def enqueue(call) - synchronize do - @queue << call - end - end - end -end - - diff --git a/spec/adhearsion/plugin/queue/queue_strategy_spec.rb b/spec/adhearsion/plugin/queue/queue_strategy_spec.rb new file mode 100644 index 0000000..1e9d0cf --- /dev/null +++ b/spec/adhearsion/plugin/queue/queue_strategy_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe Adhearsion::Plugin::Queue::QueueStrategy do + include Adhearsion::Plugin::Queue::QueueStrategy + + describe '#wrap_call' do + it 'should pass through a QueuedCall object' do + obj = Adhearsion::Plugin::Queue::QueuedCall.new dummy_call + wrap_call(obj).should be obj + end + + it 'should wrap any object that does not respond to #queued_time' do + wrap_call(dummy_call).should be_a Adhearsion::Plugin::Queue::QueuedCall + end + end +end diff --git a/spec/ahn_queue/queued_call_spec.rb b/spec/adhearsion/plugin/queue/queued_call_spec.rb similarity index 83% rename from spec/ahn_queue/queued_call_spec.rb rename to spec/adhearsion/plugin/queue/queued_call_spec.rb index fc39a23..003c6f7 100644 --- a/spec/ahn_queue/queued_call_spec.rb +++ b/spec/adhearsion/plugin/queue/queued_call_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' -describe AhnQueue::QueuedCall do +describe Adhearsion::Plugin::Queue::QueuedCall do it 'should initialize the queued_time to the current time' do now = Time.now flexmock(Time).should_receive(:now).once.and_return now - qcall = AhnQueue::QueuedCall.new dummy_call + qcall = Adhearsion::Plugin::Queue::QueuedCall.new dummy_call qcall.instance_variable_get(:@queued_time).should == now end @@ -13,7 +13,7 @@ queued_caller = dummy_call flexmock(queued_caller).should_receive(:execute).once.with('StartMusicOnHold') flexmock(queued_caller).should_receive(:execute).once.with('StopMusicOnHold') - qcall = AhnQueue::QueuedCall.new queued_caller + qcall = Adhearsion::Plugin::Queue::QueuedCall.new queued_caller # Place the call on hold and wait for it to enqueue Thread.new { qcall.hold } @@ -28,7 +28,7 @@ queued_caller = dummy_call flexmock(queued_caller).should_receive(:execute).once.with('StartMusicOnHold') flexmock(queued_caller).should_receive(:execute).once.with('StopMusicOnHold') - qcall = AhnQueue::QueuedCall.new queued_caller + qcall = Adhearsion::Plugin::Queue::QueuedCall.new queued_caller hold_thread = Thread.new { qcall.hold } diff --git a/spec/ahn_queue/round_robin_spec.rb b/spec/adhearsion/plugin/queue/round_robin_spec.rb similarity index 88% rename from spec/ahn_queue/round_robin_spec.rb rename to spec/adhearsion/plugin/queue/round_robin_spec.rb index 36f5b92..0d897c0 100644 --- a/spec/ahn_queue/round_robin_spec.rb +++ b/spec/adhearsion/plugin/queue/round_robin_spec.rb @@ -1,24 +1,24 @@ require 'spec_helper' -describe AhnQueue::RoundRobin do +describe Adhearsion::Plugin::Queue::RoundRobin do def dummy_queued_call - dqc = AhnQueue::QueuedCall.new dummy_call + dqc = Adhearsion::Plugin::Queue::QueuedCall.new dummy_call flexmock(dqc).should_receive(:hold).once flexmock(dqc).should_receive(:make_ready!).once dqc end before :each do - @queue = AhnQueue::RoundRobin.new + @queue = Adhearsion::Plugin::Queue::RoundRobin.new end describe "Queue is empty at start" do - subject { AhnQueue::RoundRobin.new } + subject { Adhearsion::Plugin::Queue::RoundRobin.new } its(:queue) { should have(0).items } end it 'should properly enqueue a call' do - call = AhnQueue::QueuedCall.new dummy_call + call = Adhearsion::Plugin::Queue::QueuedCall.new dummy_call flexmock(call).should_receive(:hold).once @queue.enqueue call @queue.instance_variable_get(:@queue).first.should be call diff --git a/spec/ahn_queue_spec.rb b/spec/adhearsion/plugin/queue_spec.rb similarity index 100% rename from spec/ahn_queue_spec.rb rename to spec/adhearsion/plugin/queue_spec.rb diff --git a/spec/ahn_queue/queue_strategy_spec.rb b/spec/ahn_queue/queue_strategy_spec.rb deleted file mode 100644 index f88b67b..0000000 --- a/spec/ahn_queue/queue_strategy_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe AhnQueue::QueueStrategy do - include AhnQueue::QueueStrategy - - describe '#wrap_call' do - it 'should pass through a QueuedCall object' do - obj = AhnQueue::QueuedCall.new dummy_call - wrap_call(obj).should be obj - end - - it 'should wrap any object that does not respond to #queued_time' do - wrap_call(dummy_call).should be_a AhnQueue::QueuedCall - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 35a05fb..b846faf 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,11 +3,10 @@ %w{ adhearsion - adhearsion/component_manager/spec_framework - ahn_queue - ahn_queue/queued_call - ahn_queue/queue_strategy - ahn_queue/round_robin + adhearsion/plugin/queue + adhearsion/plugin/queue/queued_call + adhearsion/plugin/queue/queue_strategy + adhearsion/plugin/queue/round_robin rspec/core flexmock flexmock/rspec @@ -24,6 +23,3 @@ def dummy_call Object.new end -component_name = "ahn_queue" - -AHN_QUEUE = ComponentTester.new(component_name, File.dirname(__FILE__) + "/..", "/lib/#{component_name}.rb") From 56121ebbc3e2320636304a15b7cc1717fa4dc4db Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sun, 4 Dec 2011 13:37:57 -0500 Subject: [PATCH 025/136] Add Rakefile --- Rakefile | 16 ++++++++++++++++ adhearsion-queue.gemspec | 3 +++ 2 files changed, 19 insertions(+) create mode 100644 Rakefile diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..f4d1532 --- /dev/null +++ b/Rakefile @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# encoding: utf-8 +# -*- ruby -*- +ENV['RUBY_FLAGS'] = "-I#{%w(lib ext bin spec).join(File::PATH_SEPARATOR)}" + +require 'rubygems' +require 'bundler/gem_tasks' +require 'bundler/setup' + +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new + +require 'ci/reporter/rake/rspec' +task :ci => ['ci:setup:rspec', :spec] +task :default => :spec + diff --git a/adhearsion-queue.gemspec b/adhearsion-queue.gemspec index f1f4525..aa00d95 100644 --- a/adhearsion-queue.gemspec +++ b/adhearsion-queue.gemspec @@ -22,6 +22,9 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'activesupport' s.add_development_dependency 'rspec', ['>= 2.5.0'] s.add_development_dependency 'flexmock', ['>= 0.9.0'] + s.add_development_dependency 'ci_reporter' + s.add_development_dependency 'simplecov' + s.add_development_dependency 'simplecov-rcov' s.specification_version = 2 end From 27da03ba3c49c74aa9da2f659e65c759d68d364d Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 5 Dec 2011 08:48:21 -0500 Subject: [PATCH 026/136] Rename to ElectricSlide --- ...on-queue.gemspec => electric_slide.gemspec | 4 +- lib/adhearsion/plugin/queue.rb | 68 ------------------- lib/adhearsion/plugin/queue/queue_strategy.rb | 22 ------ lib/adhearsion/plugin/queue/queued_call.rb | 28 -------- lib/adhearsion/plugin/queue/round_robin.rb | 41 ----------- .../plugin/queue/round_robin_meetme.rb | 38 ----------- lib/electric_slide.rb | 65 ++++++++++++++++++ lib/electric_slide/queue_strategy.rb | 18 +++++ lib/electric_slide/queued_call.rb | 24 +++++++ lib/electric_slide/round_robin.rb | 37 ++++++++++ lib/electric_slide/round_robin_meetme.rb | 34 ++++++++++ .../plugin/queue/queue_strategy_spec.rb | 16 ----- spec/electric_slide/queue_strategy_spec.rb | 16 +++++ .../queued_call_spec.rb | 8 +-- .../round_robin_spec.rb | 10 +-- .../queue_spec.rb => electric_slide_spec.rb} | 0 16 files changed, 205 insertions(+), 224 deletions(-) rename adhearsion-queue.gemspec => electric_slide.gemspec (91%) delete mode 100644 lib/adhearsion/plugin/queue.rb delete mode 100644 lib/adhearsion/plugin/queue/queue_strategy.rb delete mode 100644 lib/adhearsion/plugin/queue/queued_call.rb delete mode 100644 lib/adhearsion/plugin/queue/round_robin.rb delete mode 100644 lib/adhearsion/plugin/queue/round_robin_meetme.rb create mode 100644 lib/electric_slide.rb create mode 100644 lib/electric_slide/queue_strategy.rb create mode 100644 lib/electric_slide/queued_call.rb create mode 100644 lib/electric_slide/round_robin.rb create mode 100644 lib/electric_slide/round_robin_meetme.rb delete mode 100644 spec/adhearsion/plugin/queue/queue_strategy_spec.rb create mode 100644 spec/electric_slide/queue_strategy_spec.rb rename spec/{adhearsion/plugin/queue => electric_slide}/queued_call_spec.rb (83%) rename spec/{adhearsion/plugin/queue => electric_slide}/round_robin_spec.rb (88%) rename spec/{adhearsion/plugin/queue_spec.rb => electric_slide_spec.rb} (100%) diff --git a/adhearsion-queue.gemspec b/electric_slide.gemspec similarity index 91% rename from adhearsion-queue.gemspec rename to electric_slide.gemspec index aa00d95..0b910a5 100644 --- a/adhearsion-queue.gemspec +++ b/electric_slide.gemspec @@ -1,5 +1,5 @@ Gem::Specification.new do |s| - s.name = "adhearsion-queue" + s.name = "electric_slide" s.version = "0.0.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= @@ -12,7 +12,7 @@ Gem::Specification.new do |s| s.files = `git ls-files`.split("\n") s.has_rdoc = true - s.homepage = "http://github.com/adhearsion/adhearsion-queue" + s.homepage = "http://github.com/adhearsion/electric_slide" s.require_paths = ["lib"] s.rubygems_version = "1.2.0" s.summary = "Automatic Call Distributor for Adhearsion" diff --git a/lib/adhearsion/plugin/queue.rb b/lib/adhearsion/plugin/queue.rb deleted file mode 100644 index b516824..0000000 --- a/lib/adhearsion/plugin/queue.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'singleton' -require 'active_support/dependencies/autoload' -require 'adhearsion/foundation/thread_safety' - -module Adhearsion - class Plugin - class Queue < Adhearsion::Plugin - extend ActiveSupport::Autoload - - autoload :QueueStrategy - autoload :RoundRobin - autoload :RoundRobinMeetme - - include Singleton - - def initialize - @queues = {} - end - - def create(name, queue_type, agent_type = Agent) - synchronize do - @queues[name] = const_get(queue_type).new unless @queues.has_key?(name) - @queues[name].extend agent_type - end - end - - def get_queue(name) - synchronize do - @queues[name] - end - end - - def self.method_missing(method, *args, &block) - instance.send method, *args, &block - end - - module Agent - def work(agent_call) - loop do - agent_call.execute 'Bridge', @queue.next_call - end - end - end - - class CalloutAgent - def work(agent_channel) - @queue.next_call.each do |next_call| - next_call.dial agent_channel - end - end - end - - class MeetMeAgent - include Agent - - def work(agent_call) - loop do - agent_call.join agent_conf, @queue.next_call - end - end - end - - class BridgeAgent - include Agent - end - end - end -end diff --git a/lib/adhearsion/plugin/queue/queue_strategy.rb b/lib/adhearsion/plugin/queue/queue_strategy.rb deleted file mode 100644 index 0906141..0000000 --- a/lib/adhearsion/plugin/queue/queue_strategy.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Adhearsion - class Plugin - class Queue - module QueueStrategy - def wrap_call(call) - call = QueuedCall.new(call) unless call.respond_to?(:queued_time) - call - end - - def priority_enqueue(call) - # TODO: Add this call to the front of the line - enqueue call - end - - def enqueue(call) - call.hold - end - end - end - end -end - diff --git a/lib/adhearsion/plugin/queue/queued_call.rb b/lib/adhearsion/plugin/queue/queued_call.rb deleted file mode 100644 index d12a560..0000000 --- a/lib/adhearsion/plugin/queue/queued_call.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'countdownlatch' - -module Adhearsion - class Plugin - class Queue - class QueuedCall - attr_accessor :call, :queued_time - - def initialize(call) - @call = call - @queued_time = Time.now - end - - def hold - call.execute 'StartMusicOnHold' - @latch = CountDownLatch.new 1 - @latch.wait - call.execute 'StopMusicOnHold' - end - - def make_ready! - @latch.countdown! - end - end - end - end -end - diff --git a/lib/adhearsion/plugin/queue/round_robin.rb b/lib/adhearsion/plugin/queue/round_robin.rb deleted file mode 100644 index 3cb4dbb..0000000 --- a/lib/adhearsion/plugin/queue/round_robin.rb +++ /dev/null @@ -1,41 +0,0 @@ -require 'thread' -require 'adhearsion/plugin/queue/queue_strategy' - -module Adhearsion - class Plugin - class Queue - class RoundRobin - include QueueStrategy - attr_reader :queue, :conditional - - def initialize - @queue = [] - @conditional = ConditionVariable.new - end - - def next_call - call = nil - synchronize do - @conditional.wait(@mutex) if @queue.length == 0 - call = @queue.shift - end - - call.make_ready! - call - end - - # TODO: Add mechanism to add calls with higher priority to the front of the queue. - - def enqueue(call) - call = wrap_call(call) - synchronize do - @queue << call - @conditional.signal if @queue.length == 1 - end - super - end - end - end - end -end - diff --git a/lib/adhearsion/plugin/queue/round_robin_meetme.rb b/lib/adhearsion/plugin/queue/round_robin_meetme.rb deleted file mode 100644 index a5ab7f4..0000000 --- a/lib/adhearsion/plugin/queue/round_robin_meetme.rb +++ /dev/null @@ -1,38 +0,0 @@ -module Adhearsion - class Plugin - class Queue - class RoundRobinMeetme - include QueueStrategy - - def initialize(call) - @queue = [] - end - - def next_call - call = synchronize do - @queue.pop - end - - call.make_ready! - call - end - - def priority_enqueue(call) - call = wrap_call(call) - - synchronize do - @queue.unshift call - end - super - end - - def enqueue(call) - synchronize do - @queue << call - end - end - end - end - end -end - diff --git a/lib/electric_slide.rb b/lib/electric_slide.rb new file mode 100644 index 0000000..eb068a0 --- /dev/null +++ b/lib/electric_slide.rb @@ -0,0 +1,65 @@ +require 'singleton' +require 'active_support/dependencies/autoload' +require 'adhearsion/foundation/thread_safety' + +class ElectricSlide < Adhearsion::Plugin + extend ActiveSupport::Autoload + + autoload :QueueStrategy + autoload :RoundRobin + autoload :RoundRobinMeetme + + include Singleton + + def initialize + @queues = {} + end + + def create(name, queue_type, agent_type = Agent) + synchronize do + @queues[name] = const_get(queue_type).new unless @queues.has_key?(name) + @queues[name].extend agent_type + end + end + + def get_queue(name) + synchronize do + @queues[name] + end + end + + def self.method_missing(method, *args, &block) + instance.send method, *args, &block + end + + module Agent + def work(agent_call) + loop do + agent_call.execute 'Bridge', @queue.next_call + end + end + end + + class CalloutAgent + def work(agent_channel) + @queue.next_call.each do |next_call| + next_call.dial agent_channel + end + end + end + + class MeetMeAgent + include Agent + + def work(agent_call) + loop do + agent_call.join agent_conf, @queue.next_call + end + end + end + + class BridgeAgent + include Agent + end +end + diff --git a/lib/electric_slide/queue_strategy.rb b/lib/electric_slide/queue_strategy.rb new file mode 100644 index 0000000..27f0877 --- /dev/null +++ b/lib/electric_slide/queue_strategy.rb @@ -0,0 +1,18 @@ +class ElectricSlide + module QueueStrategy + def wrap_call(call) + call = QueuedCall.new(call) unless call.respond_to?(:queued_time) + call + end + + def priority_enqueue(call) + # TODO: Add this call to the front of the line + enqueue call + end + + def enqueue(call) + call.hold + end + end +end + diff --git a/lib/electric_slide/queued_call.rb b/lib/electric_slide/queued_call.rb new file mode 100644 index 0000000..5c387ac --- /dev/null +++ b/lib/electric_slide/queued_call.rb @@ -0,0 +1,24 @@ +require 'countdownlatch' + +class ElectricSlide + class QueuedCall + attr_accessor :call, :queued_time + + def initialize(call) + @call = call + @queued_time = Time.now + end + + def hold + call.execute 'StartMusicOnHold' + @latch = CountDownLatch.new 1 + @latch.wait + call.execute 'StopMusicOnHold' + end + + def make_ready! + @latch.countdown! + end + end +end + diff --git a/lib/electric_slide/round_robin.rb b/lib/electric_slide/round_robin.rb new file mode 100644 index 0000000..273ee73 --- /dev/null +++ b/lib/electric_slide/round_robin.rb @@ -0,0 +1,37 @@ +require 'thread' +require 'adhearsion/plugin/queue/queue_strategy' + +class ElectricSlide + class RoundRobin + include QueueStrategy + attr_reader :queue, :conditional + + def initialize + @queue = [] + @conditional = ConditionVariable.new + end + + def next_call + call = nil + synchronize do + @conditional.wait(@mutex) if @queue.length == 0 + call = @queue.shift + end + + call.make_ready! + call + end + + # TODO: Add mechanism to add calls with higher priority to the front of the queue. + + def enqueue(call) + call = wrap_call(call) + synchronize do + @queue << call + @conditional.signal if @queue.length == 1 + end + super + end + end +end + diff --git a/lib/electric_slide/round_robin_meetme.rb b/lib/electric_slide/round_robin_meetme.rb new file mode 100644 index 0000000..628efb6 --- /dev/null +++ b/lib/electric_slide/round_robin_meetme.rb @@ -0,0 +1,34 @@ +class ElectricSlide + class RoundRobinMeetme + include QueueStrategy + + def initialize(call) + @queue = [] + end + + def next_call + call = synchronize do + @queue.pop + end + + call.make_ready! + call + end + + def priority_enqueue(call) + call = wrap_call(call) + + synchronize do + @queue.unshift call + end + super + end + + def enqueue(call) + synchronize do + @queue << call + end + end + end +end + diff --git a/spec/adhearsion/plugin/queue/queue_strategy_spec.rb b/spec/adhearsion/plugin/queue/queue_strategy_spec.rb deleted file mode 100644 index 1e9d0cf..0000000 --- a/spec/adhearsion/plugin/queue/queue_strategy_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe Adhearsion::Plugin::Queue::QueueStrategy do - include Adhearsion::Plugin::Queue::QueueStrategy - - describe '#wrap_call' do - it 'should pass through a QueuedCall object' do - obj = Adhearsion::Plugin::Queue::QueuedCall.new dummy_call - wrap_call(obj).should be obj - end - - it 'should wrap any object that does not respond to #queued_time' do - wrap_call(dummy_call).should be_a Adhearsion::Plugin::Queue::QueuedCall - end - end -end diff --git a/spec/electric_slide/queue_strategy_spec.rb b/spec/electric_slide/queue_strategy_spec.rb new file mode 100644 index 0000000..beb435e --- /dev/null +++ b/spec/electric_slide/queue_strategy_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe ElectricSlide::QueueStrategy do + include ElectricSlide::QueueStrategy + + describe '#wrap_call' do + it 'should pass through a QueuedCall object' do + obj = ElectricSlide::QueuedCall.new dummy_call + wrap_call(obj).should be obj + end + + it 'should wrap any object that does not respond to #queued_time' do + wrap_call(dummy_call).should be_a ElectricSlide::QueuedCall + end + end +end diff --git a/spec/adhearsion/plugin/queue/queued_call_spec.rb b/spec/electric_slide/queued_call_spec.rb similarity index 83% rename from spec/adhearsion/plugin/queue/queued_call_spec.rb rename to spec/electric_slide/queued_call_spec.rb index 003c6f7..fca81f9 100644 --- a/spec/adhearsion/plugin/queue/queued_call_spec.rb +++ b/spec/electric_slide/queued_call_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' -describe Adhearsion::Plugin::Queue::QueuedCall do +describe ElectricSlide::QueuedCall do it 'should initialize the queued_time to the current time' do now = Time.now flexmock(Time).should_receive(:now).once.and_return now - qcall = Adhearsion::Plugin::Queue::QueuedCall.new dummy_call + qcall = ElectricSlide::QueuedCall.new dummy_call qcall.instance_variable_get(:@queued_time).should == now end @@ -13,7 +13,7 @@ queued_caller = dummy_call flexmock(queued_caller).should_receive(:execute).once.with('StartMusicOnHold') flexmock(queued_caller).should_receive(:execute).once.with('StopMusicOnHold') - qcall = Adhearsion::Plugin::Queue::QueuedCall.new queued_caller + qcall = ElectricSlide::QueuedCall.new queued_caller # Place the call on hold and wait for it to enqueue Thread.new { qcall.hold } @@ -28,7 +28,7 @@ queued_caller = dummy_call flexmock(queued_caller).should_receive(:execute).once.with('StartMusicOnHold') flexmock(queued_caller).should_receive(:execute).once.with('StopMusicOnHold') - qcall = Adhearsion::Plugin::Queue::QueuedCall.new queued_caller + qcall = ElectricSlide::QueuedCall.new queued_caller hold_thread = Thread.new { qcall.hold } diff --git a/spec/adhearsion/plugin/queue/round_robin_spec.rb b/spec/electric_slide/round_robin_spec.rb similarity index 88% rename from spec/adhearsion/plugin/queue/round_robin_spec.rb rename to spec/electric_slide/round_robin_spec.rb index 0d897c0..75e9b17 100644 --- a/spec/adhearsion/plugin/queue/round_robin_spec.rb +++ b/spec/electric_slide/round_robin_spec.rb @@ -1,24 +1,24 @@ require 'spec_helper' -describe Adhearsion::Plugin::Queue::RoundRobin do +describe ElectricSlide::RoundRobin do def dummy_queued_call - dqc = Adhearsion::Plugin::Queue::QueuedCall.new dummy_call + dqc = ElectricSlide::QueuedCall.new dummy_call flexmock(dqc).should_receive(:hold).once flexmock(dqc).should_receive(:make_ready!).once dqc end before :each do - @queue = Adhearsion::Plugin::Queue::RoundRobin.new + @queue = ElectricSlide::RoundRobin.new end describe "Queue is empty at start" do - subject { Adhearsion::Plugin::Queue::RoundRobin.new } + subject { ElectricSlide::RoundRobin.new } its(:queue) { should have(0).items } end it 'should properly enqueue a call' do - call = Adhearsion::Plugin::Queue::QueuedCall.new dummy_call + call = ElectricSlide::QueuedCall.new dummy_call flexmock(call).should_receive(:hold).once @queue.enqueue call @queue.instance_variable_get(:@queue).first.should be call diff --git a/spec/adhearsion/plugin/queue_spec.rb b/spec/electric_slide_spec.rb similarity index 100% rename from spec/adhearsion/plugin/queue_spec.rb rename to spec/electric_slide_spec.rb From 2ff6f900fb917e4cc26be755a701c17d9a4edf6a Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 5 Dec 2011 08:57:46 -0500 Subject: [PATCH 027/136] Fix requires --- lib/electric_slide/round_robin.rb | 2 +- spec/spec_helper.rb | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/electric_slide/round_robin.rb b/lib/electric_slide/round_robin.rb index 273ee73..00faff5 100644 --- a/lib/electric_slide/round_robin.rb +++ b/lib/electric_slide/round_robin.rb @@ -1,5 +1,5 @@ require 'thread' -require 'adhearsion/plugin/queue/queue_strategy' +require 'electric_slide/queue_strategy' class ElectricSlide class RoundRobin diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b846faf..db1db62 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,10 +3,10 @@ %w{ adhearsion - adhearsion/plugin/queue - adhearsion/plugin/queue/queued_call - adhearsion/plugin/queue/queue_strategy - adhearsion/plugin/queue/round_robin + electric_slide + electric_slide/queued_call + electric_slide/queue_strategy + electric_slide/round_robin rspec/core flexmock flexmock/rspec From 122ec866c5903631473813e91391660b83b460a9 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 5 Dec 2011 09:05:07 -0500 Subject: [PATCH 028/136] Make CI's git happy --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 99cdc59..af91ffb 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,4 @@ source :rubygems gemspec -gem 'adhearsion', :git => 'https://github.com/adhearsion/adhearsion.git', :branch => :develop +gem 'adhearsion', :git => 'git://github.com/adhearsion/adhearsion.git', :branch => :develop From e380b2c345bb843c9f6527582b820d1274f5820d Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 5 Dec 2011 09:29:23 -0500 Subject: [PATCH 029/136] Add accessors for waiting agents --- lib/electric_slide/round_robin.rb | 11 ++++++++++- spec/electric_slide/round_robin_spec.rb | 12 ++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/electric_slide/round_robin.rb b/lib/electric_slide/round_robin.rb index 00faff5..6484dde 100644 --- a/lib/electric_slide/round_robin.rb +++ b/lib/electric_slide/round_robin.rb @@ -4,17 +4,20 @@ class ElectricSlide class RoundRobin include QueueStrategy - attr_reader :queue, :conditional + attr_reader :queue def initialize @queue = [] + @agents_waiting = [] @conditional = ConditionVariable.new end def next_call call = nil synchronize do + @agents_waiting << caller @conditional.wait(@mutex) if @queue.length == 0 + @agents_waiting.delete caller call = @queue.shift end @@ -22,6 +25,12 @@ def next_call call end + def agents_waiting + synchronize do + @agents_waiting.dup + end + end + # TODO: Add mechanism to add calls with higher priority to the front of the queue. def enqueue(call) diff --git a/spec/electric_slide/round_robin_spec.rb b/spec/electric_slide/round_robin_spec.rb index 75e9b17..b7e2587 100644 --- a/spec/electric_slide/round_robin_spec.rb +++ b/spec/electric_slide/round_robin_spec.rb @@ -37,15 +37,13 @@ def dummy_queued_call # Give the agent thread a chance to block... sleep 0.5 - condvar = @queue.instance_variable_get(:@conditional) - waiters = condvar.instance_variable_get(:@waiters) - waiters.count.should == 1 + @queue.agents_waiting.count.should == 1 @queue.enqueue call # Give the agent thread a chance to retrieve the call... sleep 0.5 - waiters.count.should == 0 + @queue.agents_waiting.count.should == 0 agent_thread.kill end @@ -57,15 +55,13 @@ def dummy_queued_call # Give the agent threads a chance to block... sleep 0.5 - condvar = @queue.instance_variable_get(:@conditional) - waiters = condvar.instance_variable_get(:@waiters) - waiters.count.should == 2 + @queue.agents_waiting.count.should == 2 @queue.enqueue call # Give the agent thread a chance to retrieve the call... sleep 0.5 - waiters.count.should == 1 + @queue.agents_waiting.count.should == 1 agent1_thread.kill agent2_thread.kill end From bafd32dc93a632b017fd5406166ee69944ee813d Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 5 Dec 2011 09:38:03 -0500 Subject: [PATCH 030/136] Thread.current is better than caller (duh) --- lib/electric_slide/round_robin.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/electric_slide/round_robin.rb b/lib/electric_slide/round_robin.rb index 6484dde..f45bb00 100644 --- a/lib/electric_slide/round_robin.rb +++ b/lib/electric_slide/round_robin.rb @@ -15,9 +15,9 @@ def initialize def next_call call = nil synchronize do - @agents_waiting << caller + @agents_waiting << Thread.current @conditional.wait(@mutex) if @queue.length == 0 - @agents_waiting.delete caller + @agents_waiting.delete Thread.current call = @queue.shift end From d0a5e67a2f7c406e80aded43c90fa0391b3041c6 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 6 Jun 2013 17:37:14 -0400 Subject: [PATCH 031/136] Must require 'date' in the gemspec --- electric_slide.gemspec | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electric_slide.gemspec b/electric_slide.gemspec index 0b910a5..7dba0eb 100644 --- a/electric_slide.gemspec +++ b/electric_slide.gemspec @@ -1,3 +1,5 @@ +require 'date' + Gem::Specification.new do |s| s.name = "electric_slide" s.version = "0.0.1" From 2d0eafee8dcac5e861f4e22959a036d5bbdc6b2c Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 6 Jun 2013 17:37:41 -0400 Subject: [PATCH 032/136] No longer need to rely on Adhearsion develop branch --- Gemfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Gemfile b/Gemfile index af91ffb..ee356d2 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,3 @@ source :rubygems gemspec -gem 'adhearsion', :git => 'git://github.com/adhearsion/adhearsion.git', :branch => :develop From b7cd3e9d603a6fffa79becf6e1ca72da61fbb5f2 Mon Sep 17 00:00:00 2001 From: JustinAiken Date: Wed, 29 Jan 2014 14:35:45 -0700 Subject: [PATCH 033/136] Use the supplied class directly Instead of looking it up with const_get --- lib/electric_slide.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide.rb b/lib/electric_slide.rb index eb068a0..7d87059 100644 --- a/lib/electric_slide.rb +++ b/lib/electric_slide.rb @@ -17,7 +17,7 @@ def initialize def create(name, queue_type, agent_type = Agent) synchronize do - @queues[name] = const_get(queue_type).new unless @queues.has_key?(name) + @queues[name] = queue_type.new unless @queues.has_key?(name) @queues[name].extend agent_type end end From 65d3b43476b9bb853a9b731adf44edf9cbe69963 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sat, 29 Mar 2014 15:08:41 -0400 Subject: [PATCH 034/136] Write down thoughts on V2 of Electric Slide --- README.markdown | 99 +++++++++++++++++++++++- lib/electric_slide/agent_call.rb | 46 +++++++++++ lib/electric_slide/queue.rb | 54 +++++++++++++ lib/electric_slide/queued_call.rb | 22 ++---- lib/electric_slide/round_robin.rb | 63 ++++++++------- lib/electric_slide/round_robin_meetme.rb | 34 -------- lib/electric_slide/strategy/fifo.rb | 28 +++++++ 7 files changed, 264 insertions(+), 82 deletions(-) create mode 100644 lib/electric_slide/agent_call.rb create mode 100644 lib/electric_slide/queue.rb delete mode 100644 lib/electric_slide/round_robin_meetme.rb create mode 100644 lib/electric_slide/strategy/fifo.rb diff --git a/README.markdown b/README.markdown index 284686d..eaffe50 100644 --- a/README.markdown +++ b/README.markdown @@ -1,3 +1,100 @@ -AhnQueue - Automatic Call Distribution (ACD) Services for Adhearsion +Electric Slide - Automatic Call Distribution (ACD) Services for Adhearsion ==================================================================== +This library makes a few assumptions: + +* Individual queues will be declared by creating a class that inherits from ElectricSlide::Queue +* Agents will only be logged into a single queue at a time +* Agent authentication will happen before entering the queue - it is not the queue's concern +* If you have two types of agents (say "support" and "sales") then you should have two queues, each with their own pool of agents +* The default strategy for both agents and callers is FIFO - the first to begin waiting is the first to be connected +* Other (custom) strategies can be implemented by setting `agent_strategy` or `caller_strategy` - see `ElectricSlide::Strategy::Fifo` and `ElectricSlide::Queue#queue_strategy` - this may be useful if you want some kind of special prioritization, for example with VIP callers. +* For now, all agents must be on the phone. If an agent hangs up, he is removed from the queue + +TODO: +* Example for using Matrioska to offer Agents and Callers interactivity while waiting +* How to handle Agent logout only from the phone? +* Is there a way to get some kind of default MOH for Callers? +* Example integrating with external presence events (like XMPP) + +Example Queue +------------- + +```Ruby +class SupportQueue < ElectricSlide::Queue + name "Support Queue" + + caller_strategy :fifo + agent_strategy :fifo + + while_waiting_for_agent do + # Default block to be looped on queued calls (callers) while waiting for an agent + end + + while_waiting_for_calls do + # Default block to be looped on agent calls while waiting for a caller + end +end +``` + + +Example CallController +---------------------- + +```Ruby +class EnterTheQueue < Adhearsion::CallController + def run + answer + SupportQueue.wait_for_agent(call) do + # Play hold music or other features until an agent answers + # This block should loop if necessary + end + + end +end +``` + + +Example Agent Login +------------------- + +```Ruby +class WorkTheQueue < Adhearsion::CallController + def run + answer + SupportQueue.work_queue(call) # Blocks while agent works the queue + say "Thanks for working the queue. You are logged out. Goodbye." + end +end +``` + + +Example Agent Login with Callbacks +---------------------------------- + +```Ruby +class WorkTheQueueWithStyle < Adhearsion::CallController + def run + answer + agent = ElectricSlide::AgentCall.new call + + agent.on_caller do + # Optional + # Block to execute when agent is selected to take a call + # Occurs before the media is bridged + # Returning false indicates that the agent cannot take this call + end + + agent.on_hold do + # Optional + # Play some audio to the agent + # Can also be used to update external status trackers + # Called when the agent has entered the queue and is waiting for a call + end + + SupportQueue.work_queue(agent) # Blocks while agent works the queue + end +end +``` + + diff --git a/lib/electric_slide/agent_call.rb b/lib/electric_slide/agent_call.rb new file mode 100644 index 0000000..c0a1389 --- /dev/null +++ b/lib/electric_slide/agent_call.rb @@ -0,0 +1,46 @@ +# encoding: utf-8 + +class ElectricSlide + class AgentCall + attr_reader :call, :wait_time + + def initialize(queue, call) + return self if call.is_a? self.class + @queue, @call = queue, call + @queued_time = Time.now + + call.auto_hangup = false + + setup_callbacks + end + + def setup_callbacks + @call.on_unjoined do + # Should fire whenever the caller disconnects from the agent + wait_for_call if call.active? + end + + @call.on_end do + logout + end + end + + def wait_for_call + @queue.add_agent @call + end + + + def logout + @queue.remove_agent @call + end + + class << self + def waiting_for_call(&block) + @waiting_for_call = block + end + + end + end +end + + diff --git a/lib/electric_slide/queue.rb b/lib/electric_slide/queue.rb new file mode 100644 index 0000000..aedc3dc --- /dev/null +++ b/lib/electric_slide/queue.rb @@ -0,0 +1,54 @@ +# encoding: utf-8 + +class ElectricSlide + class Queue + include Celluloid + + def initialize(name) + end + + class << self + def name(name) + @name = name + end + + def queue_strategy(strategy) + klass = if strategy.respond_to :new + # We have been passed a class to instantiate + strategy + elsif strategy.is_a? Symbol + # Look up the class within the ElectricSlide::Strategy namespace + klass_name = strategy.to_s.camelcase + ElectricSlide::Strategy.const_get klass_name + else + raise ArgumentError + end + + klass.new + end + + def caller_strategy(strategy = nil) + @caller_strategy ||= queue_strategy(strategy || :fifo) + end + + def agent_strategy(strategy = nil) + @agent_strategy ||= queue_strategy(strategy || :fifo) + end + end + + def to_s + "#" + end + + def wait_for_agent(caller) + caller = QueuedCall.new caller + caller_strategy.add caller + end + + def work_queue(agent) + agent = AgentCall.new agent + @agent_strategy.add agent + end + end +end + diff --git a/lib/electric_slide/queued_call.rb b/lib/electric_slide/queued_call.rb index 5c387ac..593fc16 100644 --- a/lib/electric_slide/queued_call.rb +++ b/lib/electric_slide/queued_call.rb @@ -1,23 +1,15 @@ -require 'countdownlatch' +# encoding: utf-8 class ElectricSlide class QueuedCall - attr_accessor :call, :queued_time + attr_reader :call, :wait_time - def initialize(call) - @call = call - @queued_time = Time.now - end - - def hold - call.execute 'StartMusicOnHold' - @latch = CountDownLatch.new 1 - @latch.wait - call.execute 'StopMusicOnHold' - end + def initialize(queue, call) + return self if call.is_a? self.class - def make_ready! - @latch.countdown! + @queue, @call = queue, call + @call.auto_hangup = false + @wait_time = Time.now end end end diff --git a/lib/electric_slide/round_robin.rb b/lib/electric_slide/round_robin.rb index f45bb00..f6ffc6b 100644 --- a/lib/electric_slide/round_robin.rb +++ b/lib/electric_slide/round_robin.rb @@ -1,46 +1,45 @@ require 'thread' require 'electric_slide/queue_strategy' -class ElectricSlide - class RoundRobin - include QueueStrategy - attr_reader :queue +attr_reader :queue - def initialize - @queue = [] - @agents_waiting = [] - @conditional = ConditionVariable.new - end +class QueueStrategy + def initialize + @queue = [] + @agents_waiting = [] + @conditional = ConditionVariable.new + end - def next_call - call = nil - synchronize do - @agents_waiting << Thread.current - @conditional.wait(@mutex) if @queue.length == 0 - @agents_waiting.delete Thread.current - call = @queue.shift + def next_call + call = nil + synchronize do + @agents_waiting << Thread.current + @conditional.wait(@mutex) if @queue.length == 0 + @agents_waiting.delete Thread.current + queued_call = @queue.shift + until queued_call.call.active? + queued_call = @queue.shift end - - call.make_ready! - call end - def agents_waiting - synchronize do - @agents_waiting.dup - end + call.make_ready! + call + end + + def agents_waiting + synchronize do + @agents_waiting.dup end + end - # TODO: Add mechanism to add calls with higher priority to the front of the queue. + # TODO: Add mechanism to add calls with higher priority to the front of the queue. - def enqueue(call) - call = wrap_call(call) - synchronize do - @queue << call - @conditional.signal if @queue.length == 1 - end - super + def enqueue(call) + call = wrap_call(call) + synchronize do + @queue << call + @conditional.signal if @queue.length == 1 end + super end end - diff --git a/lib/electric_slide/round_robin_meetme.rb b/lib/electric_slide/round_robin_meetme.rb deleted file mode 100644 index 628efb6..0000000 --- a/lib/electric_slide/round_robin_meetme.rb +++ /dev/null @@ -1,34 +0,0 @@ -class ElectricSlide - class RoundRobinMeetme - include QueueStrategy - - def initialize(call) - @queue = [] - end - - def next_call - call = synchronize do - @queue.pop - end - - call.make_ready! - call - end - - def priority_enqueue(call) - call = wrap_call(call) - - synchronize do - @queue.unshift call - end - super - end - - def enqueue(call) - synchronize do - @queue << call - end - end - end -end - diff --git a/lib/electric_slide/strategy/fifo.rb b/lib/electric_slide/strategy/fifo.rb new file mode 100644 index 0000000..cba5ab0 --- /dev/null +++ b/lib/electric_slide/strategy/fifo.rb @@ -0,0 +1,28 @@ +# encoding: utf-8 + +class ElectricSlide + class Strategy + class Fifo + def initialize + @queue = [] + end + + def add(call) + @queue.push call + end + + def next_call + @queue.shift + end + + def remove(call) + @queue.delete call + end + + def count + @queue.length + end + end + end +end + From 9631d7a0a5f3c2a504c8088408d74851de3854d6 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sat, 29 Mar 2014 15:14:45 -0400 Subject: [PATCH 035/136] Additional comments clarifying callbacks --- README.markdown | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index eaffe50..096fc3b 100644 --- a/README.markdown +++ b/README.markdown @@ -16,6 +16,7 @@ TODO: * How to handle Agent logout only from the phone? * Is there a way to get some kind of default MOH for Callers? * Example integrating with external presence events (like XMPP) +* What other callbacks may be needed on QueuedCall and AgentCall? Example Queue ------------- @@ -29,10 +30,12 @@ class SupportQueue < ElectricSlide::Queue while_waiting_for_agent do # Default block to be looped on queued calls (callers) while waiting for an agent + # May be overriden if a callback is supplied on the QueuedCall object end while_waiting_for_calls do # Default block to be looped on agent calls while waiting for a caller + # May be overriden if a callback is supplied on the AgentCall object end end ``` @@ -48,8 +51,13 @@ class EnterTheQueue < Adhearsion::CallController SupportQueue.wait_for_agent(call) do # Play hold music or other features until an agent answers # This block should loop if necessary + # This block overrides the `#while_waiting_for_agent` above end - + + # Do any post-queue activity here, like possibly a satisfaction survey + pass CustomerSatisfactionSurvey + + say "Goodbye" end end ``` From 4bbb85abd493e13f12de92159929ed7c29abefe9 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sat, 29 Mar 2014 15:28:10 -0400 Subject: [PATCH 036/136] Add simple example for XMPP presence integration --- README.markdown | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 096fc3b..af97c75 100644 --- a/README.markdown +++ b/README.markdown @@ -15,7 +15,6 @@ TODO: * Example for using Matrioska to offer Agents and Callers interactivity while waiting * How to handle Agent logout only from the phone? * Is there a way to get some kind of default MOH for Callers? -* Example integrating with external presence events (like XMPP) * What other callbacks may be needed on QueuedCall and AgentCall? Example Queue @@ -106,3 +105,28 @@ end ``` +Example integrating external presence +------------------------------------- + +```Ruby +Adhearsion::XMPP.register_handlers do + client.register_handler(:presence) do |p| + case p.state + when :available + agent = AgentLookup.by_jid p.from # Placeholder - replace with something that gets a voice address + call = Adhearsion::OutboundCall.new + call[:jid] = p.from + call.execute_controller_or_router_on_answer WorkTheQueue + call.dial agent + + when :unavailable + call = Adhearsion.active_calls.values.detect do |call| + call[:jid] = p.from + end + call.hangup + end + end + end +end +``` + From 2386da832ba6c6516adaba5b347232769f1fe0ef Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sat, 29 Mar 2014 15:37:09 -0400 Subject: [PATCH 037/136] invoke, not pass, to remain linear --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index af97c75..3f01946 100644 --- a/README.markdown +++ b/README.markdown @@ -54,7 +54,7 @@ class EnterTheQueue < Adhearsion::CallController end # Do any post-queue activity here, like possibly a satisfaction survey - pass CustomerSatisfactionSurvey + invoke CustomerSatisfactionSurvey say "Goodbye" end From f212c50a186df05fac653c743eb083b8d54c57a1 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sat, 29 Mar 2014 15:39:27 -0400 Subject: [PATCH 038/136] Add `#on_logout` callback to agent --- README.markdown | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.markdown b/README.markdown index 3f01946..4add3f1 100644 --- a/README.markdown +++ b/README.markdown @@ -99,6 +99,15 @@ class WorkTheQueueWithStyle < Adhearsion::CallController # Called when the agent has entered the queue and is waiting for a call end + agent.on_logout do + # Optional + # Can be used to check external presence (like XMPP) and trigger something + # to call the agent and add him back to the queue + # May also be used to update stats + # This block must assume that the call object associated with this + # agent is already inactive (hungup) + end + SupportQueue.work_queue(agent) # Blocks while agent works the queue end end From eeb1ff7136534fc60ab29f22f3780869dd6e976c Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sat, 29 Mar 2014 15:40:39 -0400 Subject: [PATCH 039/136] Equality, not assignment, in example --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 4add3f1..b2e3e9d 100644 --- a/README.markdown +++ b/README.markdown @@ -130,7 +130,7 @@ Adhearsion::XMPP.register_handlers do when :unavailable call = Adhearsion.active_calls.values.detect do |call| - call[:jid] = p.from + call[:jid] == p.from end call.hangup end From 3ae47db70c949ecad2ead09973867911c1c8f9e6 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sat, 29 Mar 2014 15:44:32 -0400 Subject: [PATCH 040/136] Note about use of actors for queues --- README.markdown | 1 + 1 file changed, 1 insertion(+) diff --git a/README.markdown b/README.markdown index b2e3e9d..18250e4 100644 --- a/README.markdown +++ b/README.markdown @@ -10,6 +10,7 @@ This library makes a few assumptions: * The default strategy for both agents and callers is FIFO - the first to begin waiting is the first to be connected * Other (custom) strategies can be implemented by setting `agent_strategy` or `caller_strategy` - see `ElectricSlide::Strategy::Fifo` and `ElectricSlide::Queue#queue_strategy` - this may be useful if you want some kind of special prioritization, for example with VIP callers. * For now, all agents must be on the phone. If an agent hangs up, he is removed from the queue +* Queues will be implemented as a Celluloid Actor, which should protect the call selection strategies against race conditions TODO: * Example for using Matrioska to offer Agents and Callers interactivity while waiting From 820f80c3fc7d7c9165f31d722a71d34bfbdece10 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Sat, 29 Mar 2014 15:48:25 -0400 Subject: [PATCH 041/136] Attempt at proxying method calls to the actor --- lib/electric_slide/queue.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/electric_slide/queue.rb b/lib/electric_slide/queue.rb index aedc3dc..95a4c12 100644 --- a/lib/electric_slide/queue.rb +++ b/lib/electric_slide/queue.rb @@ -4,9 +4,6 @@ class ElectricSlide class Queue include Celluloid - def initialize(name) - end - class << self def name(name) @name = name @@ -34,6 +31,11 @@ def caller_strategy(strategy = nil) def agent_strategy(strategy = nil) @agent_strategy ||= queue_strategy(strategy || :fifo) end + + def method_missing(m, *args, &block) + # TODO: How to instantiate the supervised actor? + Celluloid::Actor[self.class.underscore.to_sym].send m, *args, &block + end end def to_s From 5aec65b775eb89b3f0a165cf144a2c950bed51a3 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 8 May 2014 19:23:44 -0400 Subject: [PATCH 042/136] Add experimental queue from GAC2014 --- lib/electric_slide/agent.rb | 16 +++ lib/electric_slide/call_queue.rb | 171 +++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 lib/electric_slide/agent.rb create mode 100644 lib/electric_slide/call_queue.rb diff --git a/lib/electric_slide/agent.rb b/lib/electric_slide/agent.rb new file mode 100644 index 0000000..cadc0d5 --- /dev/null +++ b/lib/electric_slide/agent.rb @@ -0,0 +1,16 @@ +# encoding: utf-8 +class Agent + attr_accessor :id, :address, :presence + + # @param [Hash] opts Agent parameters + # @option opts [String] :id The Agent's ID + # @option opts [String] :address The Agent's contact address + # @option opts [String] :presence The Agent's current presence + def initialize(opts = {}) + @id = opts[:id] + @address = opts[:address] + @presence = opts[:presence] + end + +end + diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb new file mode 100644 index 0000000..d9a1376 --- /dev/null +++ b/lib/electric_slide/call_queue.rb @@ -0,0 +1,171 @@ +# encoding: utf-8 +class ElectricSlide + class CallQueue + include Celluloid + + def initialize + @free_agents = [] + @agents = [] + @queue = [] + end + + # Checks whether an agent is available to take a call + # @return [Boolean] True if an agent is available + def agent_available? + @free_agents.count > 0 + end + + # Assigns the first available agent, marking the agent :busy + # @return {Agent} + def checkout_agent + agent = @free_agents.shift + agent.presence = :busy + agent + end + + # Returns a copy of the set of agents that are known to the queue + # @return [Array] Array of {Agent} objects + def get_agents + @agents.dup + end + + # Returns a copy of the set of calls waiting to be answered that are known to the queue + # @return [Array] Array of Adhearsion::Call objects + def get_queued_calls + @queue.dup + end + + # Finds an agent known to the queue by that agent's ID + # @param [String] id The ID of the agent to locate + # @return [Agent, Nil] {Agent} object if found, Nil otherwise + def get_agent(id) + @agents.detect { |agent| agent.id == id } + end + + # Registers an agent to the queue + # @param [String] id The ID of the agent to add to the queue + # @param [Hash] params The agent's details, used for creating a new {Agent} object + def add_agent(id, params) + agent = Agent.new params.merge(id: id) + @agents << agent unless @agents.include? agent + @free_agents << agent if agent.presence == :available && !@free_agents.include?(agent) + check_for_connections + end + + # Marks an agent as available to take a call. To be called after an agent completes a call + # and is ready to take the next call. + # @param [Agent] agent The {Agent} that is being returned to the queue + # @param [Symbol] status The {Agent}'s new status + # @param [String, Optional] address The {Agent}'s address. Only specified if it has changed + def return_agent(agent, status = :available, address = nil) + logger.debug "Returning #{agent} to the queue" + agent.presence = status + agent.address = address if address + + if agent.presence == :available + @free_agents << agent unless @free_agents.include? agent + check_for_connections + end + agent + end + + # Removes an agent from the queue entirely + # @param [Agent] agent The {Agent} to be removed from the queue + # @return [Agent, Nil] The Agent object if removed, Nil otherwise + def remove_agent(agent) + @free_agents.delete agent + @agents.delete agent + end + + # Checks to see if any callers are waiting for an agent and attempts to connect them to + # an available agent + def check_for_connections + while call_waiting? && agent_available? + call = get_next_caller + begin + next unless call.active? + rescue Adhearsion::Call::ExpiredError + next + end + result = connect checkout_agent, call + break + end + end + + # Add a call to the head of the queue. Among other reasons, this is used when a caller is sent + # to an agent, but the connection fails because the agent is not available. + # @param [Adhearsion::Call] call Caller to be added to the queue + def priority_enqueue(call) + # Don't reset the enqueue time in case this is a re-insert on agent failure + call[:enqueue_time] ||= Time.now + @queue.unshift call + + check_for_connections + end + + # Add a call to the end of the queue, the normal FIFO queue behavior + # @param [Adhearsion::Call] call Caller to be added to the queue + def enqueue(call) + call[:enqueue_time] = Time.now + @queue << call unless @queue.include? call + + check_for_connections + end + + # Remove a waiting call from the queue. Used if the caller hangs up or is otherwise removed. + # @param [Adhearsion::Call] call Caller to be removed from the queue + def abandon(call) + @queue.delete call + end + + # Connect an {Agent} to a caller + # @param [Agent] agent Agent to be connected + # @param [Adhearsion::Call] call Caller to be connected + def connect(agent, caller) + logger.info "Connecting #{agent} with #{caller.from}" + + metadata = {caller: caller, agent: agent} + + agent_call = Adhearsion::OutboundCall.new + agent_call[:agent] = agent + agent_call[:caller] = caller + # TODO: Make configuration option for controller where agent call should be sent + agent_call.on_end do |end_event| + logger.info "Call ended, returning agent #{agent.id} to queue" + return_agent agent + + unless [:hungup, :"hangup-command"].include?(end_event.reason) + logger.warn "Call to agent #{agent.id} ended with #{end_event.reason}, reinserting into queue" + priority_enqueue caller if caller.active? + end + end + + agent_call.dial agent.address + end + + # Returns the next waiting caller + # @return [Adhearsion::Call] The next waiting caller + def get_next_caller + @queue.shift + end + + # Checks whether any callers are waiting + # @return [Boolean] True if a caller is waiting + def call_waiting? + @queue.length > 0 + end + + # Returns the number of callers waiting in the queue + # @return [Fixnum] + def calls_waiting + @queue.length + end + + class << self + # Wrapper method ensures that all methods are sent to the correct actor + def method_missing(m, *args, &block) + Celluloid::Actor[:call_queue].send m, *args, &block + end + end + end +end From 34a97b526903a4963755de5fc13a058d73ece3e8 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 8 May 2014 19:36:20 -0400 Subject: [PATCH 043/136] Queued call proxy object from GAC2014 --- lib/electric_slide/queued_call.rb | 46 +++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/lib/electric_slide/queued_call.rb b/lib/electric_slide/queued_call.rb index 593fc16..0b4abf4 100644 --- a/lib/electric_slide/queued_call.rb +++ b/lib/electric_slide/queued_call.rb @@ -1,15 +1,49 @@ # encoding: utf-8 +require 'countdownlatch' class ElectricSlide class QueuedCall - attr_reader :call, :wait_time + attr_accessor :call, :queued_time - def initialize(queue, call) - return self if call.is_a? self.class + def initialize(call) + @call = call + @queued_time = Time.now + end + + def lock + @hangup_latch = CountDownLatch.new 1 + @hangup_latch.wait + end + + def free! + @hangup_latch.countdown! + end + + def hold + initiate_moh + @latch = CountDownLatch.new 1 + @latch.wait + suspend_moh + end + + def make_ready! + @latch.countdown! + end + + def initiate_moh + # TODO: Make MOH configurable + @call.execute_controller do + moh_handle = play! moh_audio + metadata[:moh_handle] = moh_handle + end + end - @queue, @call = queue, call - @call.auto_hangup = false - @wait_time = Time.now + def suspend_moh + @call.controllers.each do |controller| + if controller.metadata[:moh_handle] + controller.metadata[:moh_handle].stop! + end + end end end end From d165206f2be4da54dec57f11f63dbd9ffe64ac04 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 11 Aug 2014 19:05:54 -0400 Subject: [PATCH 044/136] Start work on much simplified queue --- README.markdown | 19 ++++++----- lib/electric_slide.rb | 63 ++++++++++++------------------------- spec/electric_slide_spec.rb | 8 +++++ 3 files changed, 37 insertions(+), 53 deletions(-) diff --git a/README.markdown b/README.markdown index 18250e4..d83fae2 100644 --- a/README.markdown +++ b/README.markdown @@ -1,22 +1,21 @@ -Electric Slide - Automatic Call Distribution (ACD) Services for Adhearsion +Electric Slide - Simple Call Distribution for Adhearsion ==================================================================== -This library makes a few assumptions: +This library implements a simple FIFO (First-In, First-Out) call queue for Adhearsion. + +To ensure proper operation, a few things are assumed: -* Individual queues will be declared by creating a class that inherits from ElectricSlide::Queue * Agents will only be logged into a single queue at a time + If you have two types of agents (say "support" and "sales") then you should have two queues, each with their own pool of agents * Agent authentication will happen before entering the queue - it is not the queue's concern -* If you have two types of agents (say "support" and "sales") then you should have two queues, each with their own pool of agents -* The default strategy for both agents and callers is FIFO - the first to begin waiting is the first to be connected -* Other (custom) strategies can be implemented by setting `agent_strategy` or `caller_strategy` - see `ElectricSlide::Strategy::Fifo` and `ElectricSlide::Queue#queue_strategy` - this may be useful if you want some kind of special prioritization, for example with VIP callers. -* For now, all agents must be on the phone. If an agent hangs up, he is removed from the queue +* The strategy for both agents and callers is FIFO - the first (available) of each type to begin waiting is selected +* Other (custom) strategies can be implemented by creating custom queue implementations - see below * Queues will be implemented as a Celluloid Actor, which should protect the call selection strategies against race conditions +* When an agent is selected to take a call, the agent is called. For other behaviors, a custom queue must be implemented TODO: * Example for using Matrioska to offer Agents and Callers interactivity while waiting -* How to handle Agent logout only from the phone? -* Is there a way to get some kind of default MOH for Callers? -* What other callbacks may be needed on QueuedCall and AgentCall? +* How to handle MOH Example Queue ------------- diff --git a/lib/electric_slide.rb b/lib/electric_slide.rb index 7d87059..954777a 100644 --- a/lib/electric_slide.rb +++ b/lib/electric_slide.rb @@ -1,65 +1,42 @@ +# encoding: utf-8 require 'singleton' -require 'active_support/dependencies/autoload' -require 'adhearsion/foundation/thread_safety' class ElectricSlide < Adhearsion::Plugin - extend ActiveSupport::Autoload - - autoload :QueueStrategy - autoload :RoundRobin - autoload :RoundRobinMeetme - include Singleton def initialize + @mutex = Mutex.new @queues = {} end - def create(name, queue_type, agent_type = Agent) - synchronize do - @queues[name] = queue_type.new unless @queues.has_key?(name) - @queues[name].extend agent_type - end - end + def create(name, queue = nil) + queue ||= CallQueue.supervise name - def get_queue(name) - synchronize do - @queues[name] + if @queues.has_key?(name) + fail "Queue with name #{name} already exists!" + else + @queues[name] = queue end end - def self.method_missing(method, *args, &block) - instance.send method, *args, &block + def get_queue(name) + fail "Queue #{name} not found!" unless @queues[name] + @queues[name] end - module Agent - def work(agent_call) - loop do - agent_call.execute 'Bridge', @queue.next_call - end - end - end +private - class CalloutAgent - def work(agent_channel) - @queue.next_call.each do |next_call| - next_call.dial agent_channel - end - end + def shutdown_queue(name) + queue = get_queue name + queue.shutdown! + @queues.delete queue end - class MeetMeAgent - include Agent - - def work(agent_call) - loop do - agent_call.join agent_conf, @queue.next_call - end + def self.method_missing(method, *args, &block) + @@mutex ||= Mutex.new + @@mutex.synchronize do + instance.send method, *args, &block end end - - class BridgeAgent - include Agent - end end diff --git a/spec/electric_slide_spec.rb b/spec/electric_slide_spec.rb index f8ec369..255c4d2 100644 --- a/spec/electric_slide_spec.rb +++ b/spec/electric_slide_spec.rb @@ -1 +1,9 @@ require 'spec_helper' + +describe ElectricSlide do + it "should default to an ElectricSlide::CallQueue if one is not specified" do + ElectricSlide.create "test queue" + ElectricSlide.get_queue("test queue").class.should be ElectricSlide::CallQueue + end + +end From af9edc0baf1b27ae4d939de1d82069e0e7d5d915 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 11 Aug 2014 19:36:06 -0400 Subject: [PATCH 045/136] Remove deprecated code --- lib/electric_slide/agent_call.rb | 46 ------------ lib/electric_slide/queue.rb | 56 -------------- lib/electric_slide/queue_strategy.rb | 18 ----- lib/electric_slide/queued_call.rb | 50 ------------- lib/electric_slide/round_robin.rb | 45 ----------- lib/electric_slide/strategy/fifo.rb | 28 ------- spec/electric_slide/queue_strategy_spec.rb | 16 ---- spec/electric_slide/queued_call_spec.rb | 42 ----------- spec/electric_slide/round_robin_spec.rb | 87 ---------------------- 9 files changed, 388 deletions(-) delete mode 100644 lib/electric_slide/agent_call.rb delete mode 100644 lib/electric_slide/queue.rb delete mode 100644 lib/electric_slide/queue_strategy.rb delete mode 100644 lib/electric_slide/queued_call.rb delete mode 100644 lib/electric_slide/round_robin.rb delete mode 100644 lib/electric_slide/strategy/fifo.rb delete mode 100644 spec/electric_slide/queue_strategy_spec.rb delete mode 100644 spec/electric_slide/queued_call_spec.rb delete mode 100644 spec/electric_slide/round_robin_spec.rb diff --git a/lib/electric_slide/agent_call.rb b/lib/electric_slide/agent_call.rb deleted file mode 100644 index c0a1389..0000000 --- a/lib/electric_slide/agent_call.rb +++ /dev/null @@ -1,46 +0,0 @@ -# encoding: utf-8 - -class ElectricSlide - class AgentCall - attr_reader :call, :wait_time - - def initialize(queue, call) - return self if call.is_a? self.class - @queue, @call = queue, call - @queued_time = Time.now - - call.auto_hangup = false - - setup_callbacks - end - - def setup_callbacks - @call.on_unjoined do - # Should fire whenever the caller disconnects from the agent - wait_for_call if call.active? - end - - @call.on_end do - logout - end - end - - def wait_for_call - @queue.add_agent @call - end - - - def logout - @queue.remove_agent @call - end - - class << self - def waiting_for_call(&block) - @waiting_for_call = block - end - - end - end -end - - diff --git a/lib/electric_slide/queue.rb b/lib/electric_slide/queue.rb deleted file mode 100644 index 95a4c12..0000000 --- a/lib/electric_slide/queue.rb +++ /dev/null @@ -1,56 +0,0 @@ -# encoding: utf-8 - -class ElectricSlide - class Queue - include Celluloid - - class << self - def name(name) - @name = name - end - - def queue_strategy(strategy) - klass = if strategy.respond_to :new - # We have been passed a class to instantiate - strategy - elsif strategy.is_a? Symbol - # Look up the class within the ElectricSlide::Strategy namespace - klass_name = strategy.to_s.camelcase - ElectricSlide::Strategy.const_get klass_name - else - raise ArgumentError - end - - klass.new - end - - def caller_strategy(strategy = nil) - @caller_strategy ||= queue_strategy(strategy || :fifo) - end - - def agent_strategy(strategy = nil) - @agent_strategy ||= queue_strategy(strategy || :fifo) - end - - def method_missing(m, *args, &block) - # TODO: How to instantiate the supervised actor? - Celluloid::Actor[self.class.underscore.to_sym].send m, *args, &block - end - end - - def to_s - "#" - end - - def wait_for_agent(caller) - caller = QueuedCall.new caller - caller_strategy.add caller - end - - def work_queue(agent) - agent = AgentCall.new agent - @agent_strategy.add agent - end - end -end - diff --git a/lib/electric_slide/queue_strategy.rb b/lib/electric_slide/queue_strategy.rb deleted file mode 100644 index 27f0877..0000000 --- a/lib/electric_slide/queue_strategy.rb +++ /dev/null @@ -1,18 +0,0 @@ -class ElectricSlide - module QueueStrategy - def wrap_call(call) - call = QueuedCall.new(call) unless call.respond_to?(:queued_time) - call - end - - def priority_enqueue(call) - # TODO: Add this call to the front of the line - enqueue call - end - - def enqueue(call) - call.hold - end - end -end - diff --git a/lib/electric_slide/queued_call.rb b/lib/electric_slide/queued_call.rb deleted file mode 100644 index 0b4abf4..0000000 --- a/lib/electric_slide/queued_call.rb +++ /dev/null @@ -1,50 +0,0 @@ -# encoding: utf-8 -require 'countdownlatch' - -class ElectricSlide - class QueuedCall - attr_accessor :call, :queued_time - - def initialize(call) - @call = call - @queued_time = Time.now - end - - def lock - @hangup_latch = CountDownLatch.new 1 - @hangup_latch.wait - end - - def free! - @hangup_latch.countdown! - end - - def hold - initiate_moh - @latch = CountDownLatch.new 1 - @latch.wait - suspend_moh - end - - def make_ready! - @latch.countdown! - end - - def initiate_moh - # TODO: Make MOH configurable - @call.execute_controller do - moh_handle = play! moh_audio - metadata[:moh_handle] = moh_handle - end - end - - def suspend_moh - @call.controllers.each do |controller| - if controller.metadata[:moh_handle] - controller.metadata[:moh_handle].stop! - end - end - end - end -end - diff --git a/lib/electric_slide/round_robin.rb b/lib/electric_slide/round_robin.rb deleted file mode 100644 index f6ffc6b..0000000 --- a/lib/electric_slide/round_robin.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'thread' -require 'electric_slide/queue_strategy' - -attr_reader :queue - -class QueueStrategy - def initialize - @queue = [] - @agents_waiting = [] - @conditional = ConditionVariable.new - end - - def next_call - call = nil - synchronize do - @agents_waiting << Thread.current - @conditional.wait(@mutex) if @queue.length == 0 - @agents_waiting.delete Thread.current - queued_call = @queue.shift - until queued_call.call.active? - queued_call = @queue.shift - end - end - - call.make_ready! - call - end - - def agents_waiting - synchronize do - @agents_waiting.dup - end - end - - # TODO: Add mechanism to add calls with higher priority to the front of the queue. - - def enqueue(call) - call = wrap_call(call) - synchronize do - @queue << call - @conditional.signal if @queue.length == 1 - end - super - end -end diff --git a/lib/electric_slide/strategy/fifo.rb b/lib/electric_slide/strategy/fifo.rb deleted file mode 100644 index cba5ab0..0000000 --- a/lib/electric_slide/strategy/fifo.rb +++ /dev/null @@ -1,28 +0,0 @@ -# encoding: utf-8 - -class ElectricSlide - class Strategy - class Fifo - def initialize - @queue = [] - end - - def add(call) - @queue.push call - end - - def next_call - @queue.shift - end - - def remove(call) - @queue.delete call - end - - def count - @queue.length - end - end - end -end - diff --git a/spec/electric_slide/queue_strategy_spec.rb b/spec/electric_slide/queue_strategy_spec.rb deleted file mode 100644 index beb435e..0000000 --- a/spec/electric_slide/queue_strategy_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe ElectricSlide::QueueStrategy do - include ElectricSlide::QueueStrategy - - describe '#wrap_call' do - it 'should pass through a QueuedCall object' do - obj = ElectricSlide::QueuedCall.new dummy_call - wrap_call(obj).should be obj - end - - it 'should wrap any object that does not respond to #queued_time' do - wrap_call(dummy_call).should be_a ElectricSlide::QueuedCall - end - end -end diff --git a/spec/electric_slide/queued_call_spec.rb b/spec/electric_slide/queued_call_spec.rb deleted file mode 100644 index fca81f9..0000000 --- a/spec/electric_slide/queued_call_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'spec_helper' - -describe ElectricSlide::QueuedCall do - it 'should initialize the queued_time to the current time' do - now = Time.now - flexmock(Time).should_receive(:now).once.and_return now - qcall = ElectricSlide::QueuedCall.new dummy_call - qcall.instance_variable_get(:@queued_time).should == now - end - - it 'should start and stop music on hold when put on hold and released' do - # Both tests are combined here so we do not leave too many suspended threads lying about - queued_caller = dummy_call - flexmock(queued_caller).should_receive(:execute).once.with('StartMusicOnHold') - flexmock(queued_caller).should_receive(:execute).once.with('StopMusicOnHold') - qcall = ElectricSlide::QueuedCall.new queued_caller - - # Place the call on hold and wait for it to enqueue - Thread.new { qcall.hold } - sleep 0.5 - - # Release the call from being on hold and sleep to ensure we get the Stop MOH signal - qcall.make_ready! - sleep 0.5 - end - - it 'should block the call when put on hold' do - queued_caller = dummy_call - flexmock(queued_caller).should_receive(:execute).once.with('StartMusicOnHold') - flexmock(queued_caller).should_receive(:execute).once.with('StopMusicOnHold') - qcall = ElectricSlide::QueuedCall.new queued_caller - - hold_thread = Thread.new { qcall.hold } - - # Give the holding thread a chance to block... - sleep 0.5 - hold_thread.status.should == "sleep" - qcall.make_ready! - sleep 0.5 - hold_thread.status.should be false - end -end diff --git a/spec/electric_slide/round_robin_spec.rb b/spec/electric_slide/round_robin_spec.rb deleted file mode 100644 index b7e2587..0000000 --- a/spec/electric_slide/round_robin_spec.rb +++ /dev/null @@ -1,87 +0,0 @@ -require 'spec_helper' - -describe ElectricSlide::RoundRobin do - def dummy_queued_call - dqc = ElectricSlide::QueuedCall.new dummy_call - flexmock(dqc).should_receive(:hold).once - flexmock(dqc).should_receive(:make_ready!).once - dqc - end - - before :each do - @queue = ElectricSlide::RoundRobin.new - end - - describe "Queue is empty at start" do - subject { ElectricSlide::RoundRobin.new } - its(:queue) { should have(0).items } - end - - it 'should properly enqueue a call' do - call = ElectricSlide::QueuedCall.new dummy_call - flexmock(call).should_receive(:hold).once - @queue.enqueue call - @queue.instance_variable_get(:@queue).first.should be call - end - - it 'should return the call object that is passed in' do - call = dummy_queued_call - @queue.enqueue call - @queue.next_call.should be call - end - - it 'should block an agent requesting a call until a call becomes available' do - call = dummy_queued_call - agent_thread = Thread.new { @queue.next_call } - - # Give the agent thread a chance to block... - sleep 0.5 - - @queue.agents_waiting.count.should == 1 - - @queue.enqueue call - - # Give the agent thread a chance to retrieve the call... - sleep 0.5 - @queue.agents_waiting.count.should == 0 - agent_thread.kill - end - - it 'should unblock only one agent per call entering the queue' do - call = dummy_queued_call - agent1_thread = Thread.new { @queue.next_call } - agent2_thread = Thread.new { @queue.next_call } - - # Give the agent threads a chance to block... - sleep 0.5 - - @queue.agents_waiting.count.should == 2 - - @queue.enqueue call - - # Give the agent thread a chance to retrieve the call... - sleep 0.5 - @queue.agents_waiting.count.should == 1 - agent1_thread.kill - agent2_thread.kill - end - - it 'should properly enqueue calls and return them in the same order' do - call1 = dummy_queued_call - call2 = dummy_queued_call - call3 = dummy_queued_call - threads = {} - - threads[:call1] = Thread.new { @queue.enqueue call1 } - sleep 0.5 - threads[:call2] = Thread.new { @queue.enqueue call2 } - sleep 0.5 - threads[:call3] = Thread.new { @queue.enqueue call3 } - sleep 0.5 - - - @queue.next_call.should be call1 - @queue.next_call.should be call2 - @queue.next_call.should be call3 - end -end From d3f3acd8dade178bcaa93fc829d849652eaeb14e Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 11 Aug 2014 19:36:21 -0400 Subject: [PATCH 046/136] Fix for newer rspec --- .rspec | 1 + Rakefile | 5 ++--- spec/spec_helper.rb | 13 +++---------- 3 files changed, 6 insertions(+), 13 deletions(-) create mode 100644 .rspec diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..4e1e0d2 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--color diff --git a/Rakefile b/Rakefile index f4d1532..18ce7fe 100644 --- a/Rakefile +++ b/Rakefile @@ -10,7 +10,6 @@ require 'bundler/setup' require 'rspec/core/rake_task' RSpec::Core::RakeTask.new -require 'ci/reporter/rake/rspec' -task :ci => ['ci:setup:rspec', :spec] -task :default => :spec +task ci: ['ci:setup:rspec', :spec] +task default: :spec diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index db1db62..a472915 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,22 +1,15 @@ $:.push File.join(File.dirname(__FILE__), '..', 'lib') Thread.abort_on_exception = true -%w{ +%w( adhearsion electric_slide - electric_slide/queued_call - electric_slide/queue_strategy - electric_slide/round_robin rspec/core - flexmock - flexmock/rspec -}.each { |r| require r } +).each { |r| require r } RSpec.configure do |config| - config.mock_framework = :flexmock - config.filter_run :focus => true + config.filter_run focus: true config.run_all_when_everything_filtered = true - config.color_enabled = true end def dummy_call From a2963addfbcc44829d52d2368f2f01305441d751 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 11 Aug 2014 19:44:05 -0400 Subject: [PATCH 047/136] Coding standards --- lib/electric_slide.rb | 3 +- lib/electric_slide/call_queue.rb | 49 ++++++++++++++++---------------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/lib/electric_slide.rb b/lib/electric_slide.rb index 954777a..359d9f6 100644 --- a/lib/electric_slide.rb +++ b/lib/electric_slide.rb @@ -12,7 +12,7 @@ def initialize def create(name, queue = nil) queue ||= CallQueue.supervise name - if @queues.has_key?(name) + if @queues.key?(name) fail "Queue with name #{name} already exists!" else @queues[name] = queue @@ -39,4 +39,3 @@ def self.method_missing(method, *args, &block) end end end - diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index d9a1376..5dd5b25 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -2,19 +2,19 @@ class ElectricSlide class CallQueue include Celluloid - + def initialize @free_agents = [] @agents = [] @queue = [] end - + # Checks whether an agent is available to take a call # @return [Boolean] True if an agent is available def agent_available? @free_agents.count > 0 end - + # Assigns the first available agent, marking the agent :busy # @return {Agent} def checkout_agent @@ -22,26 +22,26 @@ def checkout_agent agent.presence = :busy agent end - + # Returns a copy of the set of agents that are known to the queue # @return [Array] Array of {Agent} objects def get_agents @agents.dup end - + # Returns a copy of the set of calls waiting to be answered that are known to the queue # @return [Array] Array of Adhearsion::Call objects def get_queued_calls @queue.dup end - + # Finds an agent known to the queue by that agent's ID # @param [String] id The ID of the agent to locate # @return [Agent, Nil] {Agent} object if found, Nil otherwise def get_agent(id) @agents.detect { |agent| agent.id == id } end - + # Registers an agent to the queue # @param [String] id The ID of the agent to add to the queue # @param [Hash] params The agent's details, used for creating a new {Agent} object @@ -51,7 +51,7 @@ def add_agent(id, params) @free_agents << agent if agent.presence == :available && !@free_agents.include?(agent) check_for_connections end - + # Marks an agent as available to take a call. To be called after an agent completes a call # and is ready to take the next call. # @param [Agent] agent The {Agent} that is being returned to the queue @@ -61,14 +61,14 @@ def return_agent(agent, status = :available, address = nil) logger.debug "Returning #{agent} to the queue" agent.presence = status agent.address = address if address - + if agent.presence == :available @free_agents << agent unless @free_agents.include? agent check_for_connections end agent end - + # Removes an agent from the queue entirely # @param [Agent] agent The {Agent} to be removed from the queue # @return [Agent, Nil] The Agent object if removed, Nil otherwise @@ -76,7 +76,7 @@ def remove_agent(agent) @free_agents.delete agent @agents.delete agent end - + # Checks to see if any callers are waiting for an agent and attempts to connect them to # an available agent def check_for_connections @@ -91,7 +91,7 @@ def check_for_connections break end end - + # Add a call to the head of the queue. Among other reasons, this is used when a caller is sent # to an agent, but the connection fails because the agent is not available. # @param [Adhearsion::Call] call Caller to be added to the queue @@ -99,33 +99,33 @@ def priority_enqueue(call) # Don't reset the enqueue time in case this is a re-insert on agent failure call[:enqueue_time] ||= Time.now @queue.unshift call - + check_for_connections end - + # Add a call to the end of the queue, the normal FIFO queue behavior # @param [Adhearsion::Call] call Caller to be added to the queue def enqueue(call) call[:enqueue_time] = Time.now @queue << call unless @queue.include? call - + check_for_connections end - + # Remove a waiting call from the queue. Used if the caller hangs up or is otherwise removed. # @param [Adhearsion::Call] call Caller to be removed from the queue def abandon(call) @queue.delete call end - + # Connect an {Agent} to a caller # @param [Agent] agent Agent to be connected # @param [Adhearsion::Call] call Caller to be connected def connect(agent, caller) logger.info "Connecting #{agent} with #{caller.from}" - + metadata = {caller: caller, agent: agent} - + agent_call = Adhearsion::OutboundCall.new agent_call[:agent] = agent agent_call[:caller] = caller @@ -133,34 +133,33 @@ def connect(agent, caller) agent_call.on_end do |end_event| logger.info "Call ended, returning agent #{agent.id} to queue" return_agent agent - + unless [:hungup, :"hangup-command"].include?(end_event.reason) logger.warn "Call to agent #{agent.id} ended with #{end_event.reason}, reinserting into queue" priority_enqueue caller if caller.active? end end - + agent_call.dial agent.address end - + # Returns the next waiting caller # @return [Adhearsion::Call] The next waiting caller def get_next_caller @queue.shift end - + # Checks whether any callers are waiting # @return [Boolean] True if a caller is waiting def call_waiting? @queue.length > 0 end - + # Returns the number of callers waiting in the queue # @return [Fixnum] def calls_waiting @queue.length end - class << self # Wrapper method ensures that all methods are sent to the correct actor def method_missing(m, *args, &block) From bb7084e81451b5ee9b222228ffc8e98b995e3b11 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 11 Aug 2014 19:45:19 -0400 Subject: [PATCH 048/136] =?UTF-8?q?Don=E2=80=99t=20hard=20code=20the=20sup?= =?UTF-8?q?ervisor=20so=20we=20can=20have=20multiple?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/electric_slide/call_queue.rb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 5dd5b25..d3bbbe2 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -160,11 +160,5 @@ def call_waiting? def calls_waiting @queue.length end - class << self - # Wrapper method ensures that all methods are sent to the correct actor - def method_missing(m, *args, &block) - Celluloid::Actor[:call_queue].send m, *args, &block - end - end end end From 23c1a0a31be2b8bdc1854325a1835074a7a0d436 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 11 Aug 2014 20:05:46 -0400 Subject: [PATCH 049/136] Basic implementation of queue registry --- lib/electric_slide.rb | 13 ++++++++----- spec/electric_slide_spec.rb | 13 ++++++++++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/electric_slide.rb b/lib/electric_slide.rb index 359d9f6..1a0f192 100644 --- a/lib/electric_slide.rb +++ b/lib/electric_slide.rb @@ -1,7 +1,12 @@ # encoding: utf-8 +require 'celluloid' require 'singleton' +%w( + call_queue + plugin +).each { |f| require "electric_slide/#{f}" } -class ElectricSlide < Adhearsion::Plugin +class ElectricSlide include Singleton def initialize @@ -10,7 +15,7 @@ def initialize end def create(name, queue = nil) - queue ||= CallQueue.supervise name + queue ||= CallQueue.supervise if @queues.key?(name) fail "Queue with name #{name} already exists!" @@ -24,11 +29,9 @@ def get_queue(name) @queues[name] end -private - def shutdown_queue(name) queue = get_queue name - queue.shutdown! + queue.terminate @queues.delete queue end diff --git a/spec/electric_slide_spec.rb b/spec/electric_slide_spec.rb index 255c4d2..f55da7b 100644 --- a/spec/electric_slide_spec.rb +++ b/spec/electric_slide_spec.rb @@ -3,7 +3,18 @@ describe ElectricSlide do it "should default to an ElectricSlide::CallQueue if one is not specified" do ElectricSlide.create "test queue" - ElectricSlide.get_queue("test queue").class.should be ElectricSlide::CallQueue + expect { ElectricSlide.get_queue("test queue") }.to_not raise_error + ElectricSlide.shutdown_queue "test queue" + end + + it "should raise if attempting to work with a queue that doesn't exist" do + expect { ElectricSlide.get_queue("does not exist!") }.to raise_error + expect { ElectricSlide.shutdown_queue("does not exist!") }.to raise_error + end + + it "should preserve a queue object that is passed in" do + ElectricSlide.create :foo, :bar + expect(ElectricSlide.get_queue(:foo)).to be :bar end end From c7ffd46794fec6c9c2b49537ca28bcd864fa05fa Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 11 Aug 2014 20:20:53 -0400 Subject: [PATCH 050/136] Some initial specs for CallQueue --- spec/electric_slide/call_queue_spec.rb | 30 ++++++++++++++++++++++++++ spec/spec_helper.rb | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 spec/electric_slide/call_queue_spec.rb diff --git a/spec/electric_slide/call_queue_spec.rb b/spec/electric_slide/call_queue_spec.rb new file mode 100644 index 0000000..13df11e --- /dev/null +++ b/spec/electric_slide/call_queue_spec.rb @@ -0,0 +1,30 @@ +# encoding: utf-8 +require 'spec_helper' + +describe ElectricSlide::CallQueue do + let(:queue) { ElectricSlide::CallQueue.new } + let(:call_a) { dummy_call } + let(:call_b) { dummy_call } + let(:call_c) { dummy_call } + before :each do + queue.enqueue call_a + queue.enqueue call_b + queue.enqueue call_c + end + + it "should return callers in the same order they were enqueued" do + expect(queue.get_next_caller).to be call_a + expect(queue.get_next_caller).to be call_b + expect(queue.get_next_caller).to be call_c + end + + it "should return a priority caller ahead of the queue" do + call_d = dummy_call + queue.priority_enqueue call_d + expect(queue.get_next_caller).to be call_d + expect(queue.get_next_caller).to be call_a + end + + it "should select the agent that has been waiting the longest" + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a472915..ca91dcb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,6 +13,6 @@ end def dummy_call - Object.new + Hash.new end From 07da888240a096b51fb52da61916681518b387e9 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 11 Aug 2014 20:36:21 -0400 Subject: [PATCH 051/136] Allow instantiating generic queue classes --- lib/electric_slide.rb | 5 +++-- lib/electric_slide/call_queue.rb | 4 ++++ spec/electric_slide_spec.rb | 11 ++++++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/electric_slide.rb b/lib/electric_slide.rb index 1a0f192..9ae3eed 100644 --- a/lib/electric_slide.rb +++ b/lib/electric_slide.rb @@ -14,8 +14,9 @@ def initialize @queues = {} end - def create(name, queue = nil) - queue ||= CallQueue.supervise + def create(name, queue_class = nil, *args) + queue_class ||= CallQueue + queue = queue_class.work *args if @queues.key?(name) fail "Queue with name #{name} already exists!" diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index d3bbbe2..1bbbe59 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -160,5 +160,9 @@ def call_waiting? def calls_waiting @queue.length end + + def self.work + self.supervise + end end end diff --git a/spec/electric_slide_spec.rb b/spec/electric_slide_spec.rb index f55da7b..5c5569f 100644 --- a/spec/electric_slide_spec.rb +++ b/spec/electric_slide_spec.rb @@ -7,14 +7,15 @@ ElectricSlide.shutdown_queue "test queue" end + it "should start the queue upon registration" do + queue = double(:fake_queue) + expect(queue).to receive(:work) + ElectricSlide.create :fake, queue + end + it "should raise if attempting to work with a queue that doesn't exist" do expect { ElectricSlide.get_queue("does not exist!") }.to raise_error expect { ElectricSlide.shutdown_queue("does not exist!") }.to raise_error end - it "should preserve a queue object that is passed in" do - ElectricSlide.create :foo, :bar - expect(ElectricSlide.get_queue(:foo)).to be :bar - end - end From 13f2fa940979a093fb85eb499864fadb0647d338 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 11 Aug 2014 21:58:05 -0400 Subject: [PATCH 052/136] Queue startup/shutdown tests --- spec/electric_slide_spec.rb | 38 ++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/spec/electric_slide_spec.rb b/spec/electric_slide_spec.rb index 5c5569f..5924657 100644 --- a/spec/electric_slide_spec.rb +++ b/spec/electric_slide_spec.rb @@ -1,16 +1,36 @@ require 'spec_helper' describe ElectricSlide do - it "should default to an ElectricSlide::CallQueue if one is not specified" do - ElectricSlide.create "test queue" - expect { ElectricSlide.get_queue("test queue") }.to_not raise_error - ElectricSlide.shutdown_queue "test queue" - end + context "creating a queue" do + after :each do + ElectricSlide.shutdown_queue :fake + end + + let(:queue_class) { double :fake_queue_class } + let(:queue_inst) { double :fake_queue_instance } + + it "should default to an ElectricSlide::CallQueue if one is not specified" do + ElectricSlide.create :fake + expect { ElectricSlide.get_queue :fake }.to_not raise_error + end + + it "should start the queue upon registration" do + expect(queue_class).to receive(:work).once.and_return queue_inst + expect(queue_inst).to receive(:terminate).once + ElectricSlide.create :fake, queue_class + end + + it "should preserve additional queue arguments" do + queue = double(:fake_queue) + expect(queue_class).to receive(:work).with(:foo, :bar, :baz).once.and_return queue_inst + expect(queue_inst).to receive(:terminate).once + ElectricSlide.create :fake, queue_class, :foo, :bar, :baz + end - it "should start the queue upon registration" do - queue = double(:fake_queue) - expect(queue).to receive(:work) - ElectricSlide.create :fake, queue + it "should not allow a second queue to be created with the same name" do + ElectricSlide.create :fake + expect { ElectricSlide.create :fake }.to raise_error + end end it "should raise if attempting to work with a queue that doesn't exist" do From 5aaf8a954cace36375250f08d5e7ef2b0754f2bd Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 11 Aug 2014 21:58:15 -0400 Subject: [PATCH 053/136] Fix bug when shutting down a queue --- lib/electric_slide.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide.rb b/lib/electric_slide.rb index 9ae3eed..8c01eb4 100644 --- a/lib/electric_slide.rb +++ b/lib/electric_slide.rb @@ -33,7 +33,7 @@ def get_queue(name) def shutdown_queue(name) queue = get_queue name queue.terminate - @queues.delete queue + @queues.delete name end def self.method_missing(method, *args, &block) From a0a65208e6746e4de9e3f2c1aee174dcfda753a3 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 11 Aug 2014 21:59:58 -0400 Subject: [PATCH 054/136] Safer behavior for odd case that the queue instance is nil --- lib/electric_slide.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide.rb b/lib/electric_slide.rb index 8c01eb4..a61efed 100644 --- a/lib/electric_slide.rb +++ b/lib/electric_slide.rb @@ -26,7 +26,7 @@ def create(name, queue_class = nil, *args) end def get_queue(name) - fail "Queue #{name} not found!" unless @queues[name] + fail "Queue #{name} not found!" unless @queues.key?(name) @queues[name] end From 9cd08a2e2791b8baea9b1d43e77e550fe93f845c Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 11 Aug 2014 22:02:40 -0400 Subject: [PATCH 055/136] Put version number into code --- electric_slide.gemspec | 5 ++++- lib/electric_slide/version.rb | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 lib/electric_slide/version.rb diff --git a/electric_slide.gemspec b/electric_slide.gemspec index 7dba0eb..97692a0 100644 --- a/electric_slide.gemspec +++ b/electric_slide.gemspec @@ -1,8 +1,11 @@ +# encoding: utf-8 +$:.push File.expand_path("../lib", __FILE__) +require 'electric_slide/version' require 'date' Gem::Specification.new do |s| s.name = "electric_slide" - s.version = "0.0.1" + s.version = ElectricSlide::VERSION s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Ben Klang"] diff --git a/lib/electric_slide/version.rb b/lib/electric_slide/version.rb new file mode 100644 index 0000000..d630129 --- /dev/null +++ b/lib/electric_slide/version.rb @@ -0,0 +1,4 @@ +# encoding: utf-8 +class ElectricSlide + VERSION = '0.1.0' +end From 84e39f61e324d3f4ac785b3c832948726059852c Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 11 Aug 2014 22:04:13 -0400 Subject: [PATCH 056/136] We no longer use flexmock --- electric_slide.gemspec | 1 - 1 file changed, 1 deletion(-) diff --git a/electric_slide.gemspec b/electric_slide.gemspec index 97692a0..c3b2f67 100644 --- a/electric_slide.gemspec +++ b/electric_slide.gemspec @@ -26,7 +26,6 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'countdownlatch' s.add_runtime_dependency 'activesupport' s.add_development_dependency 'rspec', ['>= 2.5.0'] - s.add_development_dependency 'flexmock', ['>= 0.9.0'] s.add_development_dependency 'ci_reporter' s.add_development_dependency 'simplecov' s.add_development_dependency 'simplecov-rcov' From 286d35efcc6571d8826a3c7f59407af877dc54f8 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 15 Aug 2014 13:09:20 -0400 Subject: [PATCH 057/136] Plugin is not a valid file (yet) --- lib/electric_slide.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/electric_slide.rb b/lib/electric_slide.rb index a61efed..ebd7443 100644 --- a/lib/electric_slide.rb +++ b/lib/electric_slide.rb @@ -3,7 +3,6 @@ require 'singleton' %w( call_queue - plugin ).each { |f| require "electric_slide/#{f}" } class ElectricSlide From 731e76e23a76f3a320e60221b2791c39dd3bf358 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 15 Aug 2014 13:33:45 -0400 Subject: [PATCH 058/136] ElectricSlide plugin placeholder --- lib/electric_slide.rb | 1 + lib/electric_slide/plugin.rb | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 lib/electric_slide/plugin.rb diff --git a/lib/electric_slide.rb b/lib/electric_slide.rb index ebd7443..a61efed 100644 --- a/lib/electric_slide.rb +++ b/lib/electric_slide.rb @@ -3,6 +3,7 @@ require 'singleton' %w( call_queue + plugin ).each { |f| require "electric_slide/#{f}" } class ElectricSlide diff --git a/lib/electric_slide/plugin.rb b/lib/electric_slide/plugin.rb new file mode 100644 index 0000000..fedaa79 --- /dev/null +++ b/lib/electric_slide/plugin.rb @@ -0,0 +1,10 @@ +# encoding: utf-8 +require 'adhearsion' + +class ElectricSlide + class Plugin < Adhearsion::Plugin + init do + logger.info 'ElectricSlide plugin loaded.' + end + end +end From 8043011fe8c6c632c42f5eb23b4b94e4d2eaf371 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 15 Aug 2014 14:35:04 -0400 Subject: [PATCH 059/136] Comment on variable usage --- lib/electric_slide/call_queue.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 1bbbe59..4aa31b9 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -4,9 +4,9 @@ class CallQueue include Celluloid def initialize - @free_agents = [] - @agents = [] - @queue = [] + @free_agents = [] # Needed to keep track of waiting order + @agents = [] # Needed to keep track of global list of agents + @queue = [] # Calls waiting for an agent end # Checks whether an agent is available to take a call From b6b40320914a265913c3fdc04aed4d9bab2b1880 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 4 Dec 2014 21:42:28 -0500 Subject: [PATCH 060/136] Better documentation on intended queue usage --- README.markdown | 116 +++++++++--------------------------- lib/electric_slide/agent.rb | 4 +- 2 files changed, 30 insertions(+), 90 deletions(-) diff --git a/README.markdown b/README.markdown index d83fae2..95e9880 100644 --- a/README.markdown +++ b/README.markdown @@ -21,40 +21,32 @@ Example Queue ------------- ```Ruby -class SupportQueue < ElectricSlide::Queue - name "Support Queue" +my_queue = ElectricSlide.create :my_queue - caller_strategy :fifo - agent_strategy :fifo - - while_waiting_for_agent do - # Default block to be looped on queued calls (callers) while waiting for an agent - # May be overriden if a callback is supplied on the QueuedCall object - end - - while_waiting_for_calls do - # Default block to be looped on agent calls while waiting for a caller - # May be overriden if a callback is supplied on the AgentCall object - end -end +# Another way to get a handle on a queue +ElectricSlide.create :my_queue +my_queue = ElectricSlide.get_queue :my_queue ``` -Example CallController ----------------------- +Example CallController for Queued Call +-------------------------------------- ```Ruby class EnterTheQueue < Adhearsion::CallController def run answer - SupportQueue.wait_for_agent(call) do - # Play hold music or other features until an agent answers - # This block should loop if necessary - # This block overrides the `#while_waiting_for_agent` above + + # Play music-on-hold to the caller until joined to an agent + # TODO: Create an ElectricSlide helper to wrap up this function + # with optional looping of playback + player = play 'http://moh-server.example.com/stream.mp3' + call.on_joined do + player.stop! end - # Do any post-queue activity here, like possibly a satisfaction survey - invoke CustomerSatisfactionSurvey + ElectricSlide.get_queue(:my_queue).enqueue call + # Blocks until call is done talking to the agent say "Goodbye" end @@ -62,80 +54,28 @@ end ``` -Example Agent Login -------------------- - -```Ruby -class WorkTheQueue < Adhearsion::CallController - def run - answer - SupportQueue.work_queue(call) # Blocks while agent works the queue - say "Thanks for working the queue. You are logged out. Goodbye." - end -end -``` +Adding an Agent to the Queue +---------------------------- +ElectricSlide expects to be given a objects that quack like an agent. You can use the built-in `ElectricSlide::Agent` class, or you can provide your own. -Example Agent Login with Callbacks ----------------------------------- +To add an agent who will receive calls whenever a call is enqueued, do something like this: ```Ruby -class WorkTheQueueWithStyle < Adhearsion::CallController - def run - answer - agent = ElectricSlide::AgentCall.new call - - agent.on_caller do - # Optional - # Block to execute when agent is selected to take a call - # Occurs before the media is bridged - # Returning false indicates that the agent cannot take this call - end - - agent.on_hold do - # Optional - # Play some audio to the agent - # Can also be used to update external status trackers - # Called when the agent has entered the queue and is waiting for a call - end +agent = ElectricSlide::Agent.new id: 1, address: 'sip:agent1@example.com', presence: :available +ElectricSlide.get_queue(:my_queue).add_agent agent +``` - agent.on_logout do - # Optional - # Can be used to check external presence (like XMPP) and trigger something - # to call the agent and add him back to the queue - # May also be used to update stats - # This block must assume that the call object associated with this - # agent is already inactive (hungup) - end +To inform the queue that the agent is no longer available you *must* use the ElectricSlide queue interface. /Do not attempt to alter agent objects directly!/ - SupportQueue.work_queue(agent) # Blocks while agent works the queue - end -end +```Ruby +ElectricSlide.update_agent 1, presence: offline ``` - -Example integrating external presence -------------------------------------- +If it is more convenient, you may also pass `#update_agent` an Agent-like object: ```Ruby -Adhearsion::XMPP.register_handlers do - client.register_handler(:presence) do |p| - case p.state - when :available - agent = AgentLookup.by_jid p.from # Placeholder - replace with something that gets a voice address - call = Adhearsion::OutboundCall.new - call[:jid] = p.from - call.execute_controller_or_router_on_answer WorkTheQueue - call.dial agent - - when :unavailable - call = Adhearsion.active_calls.values.detect do |call| - call[:jid] == p.from - end - call.hangup - end - end - end -end +agent = ElectricSlide::Agent.new id:1, address: 'sip:agent1@example.com', presence: :offline +ElectricSlide.update_agent 1, agent ``` diff --git a/lib/electric_slide/agent.rb b/lib/electric_slide/agent.rb index cadc0d5..8bc3312 100644 --- a/lib/electric_slide/agent.rb +++ b/lib/electric_slide/agent.rb @@ -1,11 +1,11 @@ # encoding: utf-8 class Agent - attr_accessor :id, :address, :presence + attr_accessor :id, :address, :presence, :connect_callback, :disconnect_callback # @param [Hash] opts Agent parameters # @option opts [String] :id The Agent's ID # @option opts [String] :address The Agent's contact address - # @option opts [String] :presence The Agent's current presence + # @option opts [Symbol] :presence The Agent's current presence. Must be one of :available, :on_call, :away, :offline def initialize(opts = {}) @id = opts[:id] @address = opts[:address] From dd0ecd8844cbb082edc2a40a01631e6c92e4123c Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 4 Dec 2014 21:42:50 -0500 Subject: [PATCH 061/136] =?UTF-8?q?Better=20variable=20name=20(don?= =?UTF-8?q?=E2=80=99t=20override=20#caller)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/electric_slide/call_queue.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 4aa31b9..53350ad 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -121,14 +121,12 @@ def abandon(call) # Connect an {Agent} to a caller # @param [Agent] agent Agent to be connected # @param [Adhearsion::Call] call Caller to be connected - def connect(agent, caller) - logger.info "Connecting #{agent} with #{caller.from}" - - metadata = {caller: caller, agent: agent} + def connect(agent, queued_call) + logger.info "Connecting #{agent} with #{queued_call.from}" agent_call = Adhearsion::OutboundCall.new agent_call[:agent] = agent - agent_call[:caller] = caller + agent_call[:queued_call] = queued_call # TODO: Make configuration option for controller where agent call should be sent agent_call.on_end do |end_event| logger.info "Call ended, returning agent #{agent.id} to queue" @@ -136,7 +134,7 @@ def connect(agent, caller) unless [:hungup, :"hangup-command"].include?(end_event.reason) logger.warn "Call to agent #{agent.id} ended with #{end_event.reason}, reinserting into queue" - priority_enqueue caller if caller.active? + priority_enqueue queued_call if queued_call.active? end end From 227f8393fc4914b0cdb0b415247ae26483c51827 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 4 Dec 2014 21:42:58 -0500 Subject: [PATCH 062/136] Add callbacks on Agent class --- README.markdown | 7 +++++++ lib/electric_slide/agent.rb | 12 ++++++++++++ lib/electric_slide/call_queue.rb | 4 ++++ 3 files changed, 23 insertions(+) diff --git a/README.markdown b/README.markdown index 95e9880..4f001e8 100644 --- a/README.markdown +++ b/README.markdown @@ -79,3 +79,10 @@ agent = ElectricSlide::Agent.new id:1, address: 'sip:agent1@example.com', presen ElectricSlide.update_agent 1, agent ``` +Custom Agent Behavior +---------------------------- + +If you need custom functionality to occur whenever an Agent is selected to take a call, you can use the callbacks on the Agent object: + +* `on_connect` +* `on_disconnect` diff --git a/lib/electric_slide/agent.rb b/lib/electric_slide/agent.rb index 8bc3312..a4975bc 100644 --- a/lib/electric_slide/agent.rb +++ b/lib/electric_slide/agent.rb @@ -12,5 +12,17 @@ def initialize(opts = {}) @presence = opts[:presence] end + # Provide a block to be called when this agent is connected to a caller + # The block will be passed the queue, the agent call and the client call + def on_connect(&block) + @connect_callback = block + end + + # Provide a block to be called when this agent is disconnected to a caller + # The block will be passed the queue, the agent call and the client call + def on_disconnect(&block) + @disconnect_callback = block + end + end diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 53350ad..8f2b0d6 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -132,12 +132,16 @@ def connect(agent, queued_call) logger.info "Call ended, returning agent #{agent.id} to queue" return_agent agent + agent.disconnect_callback.call self, agent_call, queued_call + unless [:hungup, :"hangup-command"].include?(end_event.reason) logger.warn "Call to agent #{agent.id} ended with #{end_event.reason}, reinserting into queue" priority_enqueue queued_call if queued_call.active? end end + agent.connect_callback.call self, agent_call, queued_call + agent_call.dial agent.address end From 65edfac57d9bd158489016c722e0669bdfa36794 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 4 Dec 2014 22:44:01 -0500 Subject: [PATCH 063/136] Alter default queue to be compatible with #work --- lib/electric_slide/call_queue.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 8f2b0d6..fdfb90e 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -3,6 +3,10 @@ class ElectricSlide class CallQueue include Celluloid + def self.work(*args) + self.supervise *args + end + def initialize @free_agents = [] # Needed to keep track of waiting order @agents = [] # Needed to keep track of global list of agents From 7e491623e9e5d8832a1504930fe24b2b16d2493a Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 4 Dec 2014 22:44:16 -0500 Subject: [PATCH 064/136] Simplify --- lib/electric_slide.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/electric_slide.rb b/lib/electric_slide.rb index a61efed..bbc5b53 100644 --- a/lib/electric_slide.rb +++ b/lib/electric_slide.rb @@ -15,13 +15,11 @@ def initialize end def create(name, queue_class = nil, *args) - queue_class ||= CallQueue - queue = queue_class.work *args - if @queues.key?(name) fail "Queue with name #{name} already exists!" else - @queues[name] = queue + queue_class ||= CallQueue + @queues[name] = queue_class.work *args end end From e1873955139dcd6523d1c3a74805d2732d6b4c08 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 4 Dec 2014 22:44:28 -0500 Subject: [PATCH 065/136] Provide callback for providing agent dial options --- lib/electric_slide/agent.rb | 5 +++++ lib/electric_slide/call_queue.rb | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/electric_slide/agent.rb b/lib/electric_slide/agent.rb index a4975bc..6bc6d4e 100644 --- a/lib/electric_slide/agent.rb +++ b/lib/electric_slide/agent.rb @@ -24,5 +24,10 @@ def on_disconnect(&block) @disconnect_callback = block end + # Called to provide options for calling this agent that are passed to #dial + def dial_options_for(queue, queued_call) + {} + end + end diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index fdfb90e..55fcc5e 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -146,7 +146,7 @@ def connect(agent, queued_call) agent.connect_callback.call self, agent_call, queued_call - agent_call.dial agent.address + agent_call.dial agent.address, agent.dial_options_for(self, queued_call) end # Returns the next waiting caller From 45d60fd2b1e412e799229942e727cdd64718942a Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 5 Dec 2014 10:35:45 -0500 Subject: [PATCH 066/136] Remove duplicated method definition --- lib/electric_slide/call_queue.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 55fcc5e..5dc2e16 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -166,9 +166,5 @@ def call_waiting? def calls_waiting @queue.length end - - def self.work - self.supervise - end end end From a01b345a99d42d7d6a0981165b2422fdb41f3630 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 5 Dec 2014 10:35:56 -0500 Subject: [PATCH 067/136] Compatibility with registering a SupervisionGroup as a queue --- lib/electric_slide.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/electric_slide.rb b/lib/electric_slide.rb index bbc5b53..5f2286f 100644 --- a/lib/electric_slide.rb +++ b/lib/electric_slide.rb @@ -25,7 +25,13 @@ def create(name, queue_class = nil, *args) def get_queue(name) fail "Queue #{name} not found!" unless @queues.key?(name) - @queues[name] + queue = @queues[name] + if queue.respond_to? :actors + # In case we have a Celluloid supervision group, get the current actor + queue.actors.first + else + queue + end end def shutdown_queue(name) From 97a3a066090fde26d2e1b3187cc507e600d57e37 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 5 Dec 2014 11:12:47 -0500 Subject: [PATCH 068/136] Pass in agents rather than instantiate them --- lib/electric_slide/call_queue.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 5dc2e16..2fe3f47 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -47,10 +47,8 @@ def get_agent(id) end # Registers an agent to the queue - # @param [String] id The ID of the agent to add to the queue - # @param [Hash] params The agent's details, used for creating a new {Agent} object - def add_agent(id, params) - agent = Agent.new params.merge(id: id) + # @param [Agent] agent The agent to be added to the queue + def add_agent(agent) @agents << agent unless @agents.include? agent @free_agents << agent if agent.presence == :available && !@free_agents.include?(agent) check_for_connections From 53095a3ae39b08aaacf296ea314a76cfb2d3a90d Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 5 Dec 2014 11:26:25 -0500 Subject: [PATCH 069/136] Properize the namespace --- lib/electric_slide/agent.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/agent.rb b/lib/electric_slide/agent.rb index 6bc6d4e..e0d1c93 100644 --- a/lib/electric_slide/agent.rb +++ b/lib/electric_slide/agent.rb @@ -1,5 +1,5 @@ # encoding: utf-8 -class Agent +class ElectricSlide::Agent attr_accessor :id, :address, :presence, :connect_callback, :disconnect_callback # @param [Hash] opts Agent parameters From d914de0b579755e1cee8248ac9167b893c174586 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 5 Dec 2014 12:29:26 -0500 Subject: [PATCH 070/136] Prevent undefined callbacks from raising --- lib/electric_slide/agent.rb | 6 ++++++ lib/electric_slide/call_queue.rb | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/electric_slide/agent.rb b/lib/electric_slide/agent.rb index e0d1c93..f8f0290 100644 --- a/lib/electric_slide/agent.rb +++ b/lib/electric_slide/agent.rb @@ -12,6 +12,12 @@ def initialize(opts = {}) @presence = opts[:presence] end + def callback(type, *args) + callback = instance_variable_get "@#{type}_callback" + callback.call if callback && callback.respond_to?(:call) + end + + # Provide a block to be called when this agent is connected to a caller # The block will be passed the queue, the agent call and the client call def on_connect(&block) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 2fe3f47..8574fb5 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -134,7 +134,7 @@ def connect(agent, queued_call) logger.info "Call ended, returning agent #{agent.id} to queue" return_agent agent - agent.disconnect_callback.call self, agent_call, queued_call + agent.callback :disconnect, self, agent_call, queued_call unless [:hungup, :"hangup-command"].include?(end_event.reason) logger.warn "Call to agent #{agent.id} ended with #{end_event.reason}, reinserting into queue" @@ -142,7 +142,7 @@ def connect(agent, queued_call) end end - agent.connect_callback.call self, agent_call, queued_call + agent.callback :connect, self, agent_call, queued_call agent_call.dial agent.address, agent.dial_options_for(self, queued_call) end From 4e987fc84dfb5131ac2ce703c230d79311defb37 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 5 Dec 2014 12:29:45 -0500 Subject: [PATCH 071/136] Default handling connecting/disconnecting the call --- lib/electric_slide/call_queue.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 8574fb5..75d7ae2 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -129,6 +129,14 @@ def connect(agent, queued_call) agent_call = Adhearsion::OutboundCall.new agent_call[:agent] = agent agent_call[:queued_call] = queued_call + + # TODO: Allow executing a call controller here, specified by the agent + agent_call.on_answer { agent_call.join queued_call } + agent_call.on_unjoined do + ignoring_ended_calls { agent_call.hangup } + ignoring_ended_calls { queued_call.hangup } + end + # TODO: Make configuration option for controller where agent call should be sent agent_call.on_end do |end_event| logger.info "Call ended, returning agent #{agent.id} to queue" @@ -164,5 +172,13 @@ def call_waiting? def calls_waiting @queue.length end + + private + # @private + def ignoring_ended_calls + yield + rescue Celluloid::DeadActorError, Adhearsion::Call::Hangup, Adhearsion::Call::ExpiredError + # This actor may previously have been shut down due to the call ending + end end end From 2ff4bb44aacff4b00a07d23954afe8e4c12ab122 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 5 Dec 2014 13:01:32 -0500 Subject: [PATCH 072/136] Additional ops-friendly logging --- lib/electric_slide/call_queue.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 75d7ae2..1c3d3fb 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -49,6 +49,7 @@ def get_agent(id) # Registers an agent to the queue # @param [Agent] agent The agent to be added to the queue def add_agent(agent) + logger.info "Adding agent #{agent} to the queue" @agents << agent unless @agents.include? agent @free_agents << agent if agent.presence == :available && !@free_agents.include?(agent) check_for_connections @@ -75,6 +76,7 @@ def return_agent(agent, status = :available, address = nil) # @param [Agent] agent The {Agent} to be removed from the queue # @return [Agent, Nil] The Agent object if removed, Nil otherwise def remove_agent(agent) + logger.info "Removing agent #{agent} from the queue" @free_agents.delete agent @agents.delete agent end @@ -108,6 +110,7 @@ def priority_enqueue(call) # Add a call to the end of the queue, the normal FIFO queue behavior # @param [Adhearsion::Call] call Caller to be added to the queue def enqueue(call) + logger.info "Adding call from #{call.from} to the queue" call[:enqueue_time] = Time.now @queue << call unless @queue.include? call @@ -117,6 +120,7 @@ def enqueue(call) # Remove a waiting call from the queue. Used if the caller hangs up or is otherwise removed. # @param [Adhearsion::Call] call Caller to be removed from the queue def abandon(call) + logger.info "Caller #{call.from} has abandoned the queue" @queue.delete call end From 266c7831d5f5efccfe7d8387b0c87d1926a143af Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 5 Dec 2014 15:53:47 -0500 Subject: [PATCH 073/136] Ensure we don't return an agent that was removed or paused --- lib/electric_slide/call_queue.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 1c3d3fb..75271ec 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -144,7 +144,11 @@ def connect(agent, queued_call) # TODO: Make configuration option for controller where agent call should be sent agent_call.on_end do |end_event| logger.info "Call ended, returning agent #{agent.id} to queue" - return_agent agent + + # Ensure we don't return an agent that was removed or paused + if agent && @agents.include?(agent) && agent.presence == :busy + return_agent agent + end agent.callback :disconnect, self, agent_call, queued_call From 64b3b6d7c0279c82552fb7983fb053daeb7fe3c7 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 5 Dec 2014 16:19:51 -0500 Subject: [PATCH 074/136] Track whether the agent actually talks to the queued_call --- lib/electric_slide/call_queue.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 75271ec..a21e769 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -141,18 +141,21 @@ def connect(agent, queued_call) ignoring_ended_calls { queued_call.hangup } end + # Track whether the agent actually talks to the queued_call + connected = false + queued_call.on_joined { connected = true } + # TODO: Make configuration option for controller where agent call should be sent agent_call.on_end do |end_event| - logger.info "Call ended, returning agent #{agent.id} to queue" - # Ensure we don't return an agent that was removed or paused if agent && @agents.include?(agent) && agent.presence == :busy + logger.info "Call ended, returning agent #{agent.id} to queue" return_agent agent end agent.callback :disconnect, self, agent_call, queued_call - unless [:hungup, :"hangup-command"].include?(end_event.reason) + unless connected logger.warn "Call to agent #{agent.id} ended with #{end_event.reason}, reinserting into queue" priority_enqueue queued_call if queued_call.active? end From d18ff2c8cb201807331a12a0182ae7c30fd8f1ad Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 5 Dec 2014 16:26:50 -0500 Subject: [PATCH 075/136] This is no longer TODO --- lib/electric_slide/call_queue.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index a21e769..710c660 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -145,7 +145,6 @@ def connect(agent, queued_call) connected = false queued_call.on_joined { connected = true } - # TODO: Make configuration option for controller where agent call should be sent agent_call.on_end do |end_event| # Ensure we don't return an agent that was removed or paused if agent && @agents.include?(agent) && agent.presence == :busy From cc0b4499d4e37c2f3670c8b26912ec1fdc380fa6 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 5 Dec 2014 16:26:59 -0500 Subject: [PATCH 076/136] No errors if the caller hangs up before the agent answers --- lib/electric_slide/call_queue.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 710c660..bab8226 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -135,7 +135,7 @@ def connect(agent, queued_call) agent_call[:queued_call] = queued_call # TODO: Allow executing a call controller here, specified by the agent - agent_call.on_answer { agent_call.join queued_call } + agent_call.on_answer { ignoring_ended_calls { agent_call.join queued_call } } agent_call.on_unjoined do ignoring_ended_calls { agent_call.hangup } ignoring_ended_calls { queued_call.hangup } From c5743585a4f0b040a49e0b35397ac230822c8226 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 8 Dec 2014 12:15:42 -0500 Subject: [PATCH 077/136] Better logging when agent does not connect --- lib/electric_slide/call_queue.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index bab8226..4fca0a1 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -155,8 +155,8 @@ def connect(agent, queued_call) agent.callback :disconnect, self, agent_call, queued_call unless connected - logger.warn "Call to agent #{agent.id} ended with #{end_event.reason}, reinserting into queue" priority_enqueue queued_call if queued_call.active? + logger.warn "Call did not connect to agent! Agent #{agent.id} call ended with #{end_event.reason}; reinserting caller #{queued_call.from} into queue" end end From fcb7a08d80deccbe441cd7dfccbe3808edfe6f83 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 8 Dec 2014 12:16:06 -0500 Subject: [PATCH 078/136] =?UTF-8?q?Don=E2=80=99t=20let=20a=20disconnected?= =?UTF-8?q?=20queued=5Fcall=20raise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/electric_slide/call_queue.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 4fca0a1..8728e81 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -155,8 +155,8 @@ def connect(agent, queued_call) agent.callback :disconnect, self, agent_call, queued_call unless connected - priority_enqueue queued_call if queued_call.active? logger.warn "Call did not connect to agent! Agent #{agent.id} call ended with #{end_event.reason}; reinserting caller #{queued_call.from} into queue" + ignoring_ended_calls { priority_enqueue queued_call if queued_call.active? } end end From e0f78b582da0ae5a88aa2a1da0b5b3edbf942d72 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 8 Dec 2014 14:16:57 -0500 Subject: [PATCH 079/136] Stash caller ID so we can avoid crashes due to log messages --- lib/electric_slide/call_queue.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 8728e81..b882f2f 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -134,6 +134,9 @@ def connect(agent, queued_call) agent_call[:agent] = agent agent_call[:queued_call] = queued_call + # Stash the caller ID so we don't have to try to get it from a dead call object later + queued_caller_id = queued_call.from + # TODO: Allow executing a call controller here, specified by the agent agent_call.on_answer { ignoring_ended_calls { agent_call.join queued_call } } agent_call.on_unjoined do @@ -155,8 +158,12 @@ def connect(agent, queued_call) agent.callback :disconnect, self, agent_call, queued_call unless connected - logger.warn "Call did not connect to agent! Agent #{agent.id} call ended with #{end_event.reason}; reinserting caller #{queued_call.from} into queue" - ignoring_ended_calls { priority_enqueue queued_call if queued_call.active? } + if queued_call.active? + ignoring_ended_calls { priority_enqueue queued_call } + logger.warn "Call did not connect to agent! Agent #{agent.id} call ended with #{end_event.reason}; reinserting caller #{queued_caller_id} into queue" + else + logger.warn "Caller #{queued_caller_id} hung up before being connected to an agent." + end end end From 66a3fbbb4391b2d76fda90d6601aa5a16620626d Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 8 Dec 2014 14:17:47 -0500 Subject: [PATCH 080/136] =?UTF-8?q?Try=20harder=20to=20make=20sure=20we=20?= =?UTF-8?q?don=E2=80=99t=20lose=20agent=20state=20if=20the=20caller=20has?= =?UTF-8?q?=20hung=20up=20before=20answer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/electric_slide/call_queue.rb | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index b882f2f..5bb72f9 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -138,7 +138,16 @@ def connect(agent, queued_call) queued_caller_id = queued_call.from # TODO: Allow executing a call controller here, specified by the agent - agent_call.on_answer { ignoring_ended_calls { agent_call.join queued_call } } + agent_call.on_answer do + begin + agent_call.join queued_call + rescue Celluloid::DeadActorError, Adhearsion::Call::Hangup, Adhearsion::Call::ExpiredError + # Handle these manually because we need to be able to put the agent back into the queue + logger.info "Queued call #{queued_caller_id} hung up before agent could join; returning agent #{agent.id} to the queue." + conditionally_return_agent agent + end + end + agent_call.on_unjoined do ignoring_ended_calls { agent_call.hangup } ignoring_ended_calls { queued_call.hangup } @@ -150,10 +159,7 @@ def connect(agent, queued_call) agent_call.on_end do |end_event| # Ensure we don't return an agent that was removed or paused - if agent && @agents.include?(agent) && agent.presence == :busy - logger.info "Call ended, returning agent #{agent.id} to queue" - return_agent agent - end + conditionally_return_agent agent agent.callback :disconnect, self, agent_call, queued_call @@ -172,6 +178,15 @@ def connect(agent, queued_call) agent_call.dial agent.address, agent.dial_options_for(self, queued_call) end + def conditionally_return_agent(agent) + if agent && @agents.include?(agent) && agent.presence == :busy + logger.info "Returning agent #{agent.id} to queue" + return_agent agent + else + logger.debug "Not returning agent #{agent.inspect} to the queue" + end + end + # Returns the next waiting caller # @return [Adhearsion::Call] The next waiting caller def get_next_caller From 490f8dc8c7cf32d8fa79c9a4a50645f63f7970ec Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Wed, 10 Dec 2014 09:36:22 -0500 Subject: [PATCH 081/136] Work around Adhearsion bug --- lib/electric_slide/call_queue.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 5bb72f9..0192c54 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -140,7 +140,7 @@ def connect(agent, queued_call) # TODO: Allow executing a call controller here, specified by the agent agent_call.on_answer do begin - agent_call.join queued_call + agent_call.join queued_call.id rescue Celluloid::DeadActorError, Adhearsion::Call::Hangup, Adhearsion::Call::ExpiredError # Handle these manually because we need to be able to put the agent back into the queue logger.info "Queued call #{queued_caller_id} hung up before agent could join; returning agent #{agent.id} to the queue." From 4c13d075158448b38d942fb5b9ed4c42355f00e7 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Wed, 10 Dec 2014 09:48:04 -0500 Subject: [PATCH 082/136] =?UTF-8?q?Don=E2=80=99t=20duplicate=20code=20to?= =?UTF-8?q?=20re-add=20agent=20to=20the=20queue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/electric_slide/call_queue.rb | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 0192c54..9eaf080 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -138,15 +138,7 @@ def connect(agent, queued_call) queued_caller_id = queued_call.from # TODO: Allow executing a call controller here, specified by the agent - agent_call.on_answer do - begin - agent_call.join queued_call.id - rescue Celluloid::DeadActorError, Adhearsion::Call::Hangup, Adhearsion::Call::ExpiredError - # Handle these manually because we need to be able to put the agent back into the queue - logger.info "Queued call #{queued_caller_id} hung up before agent could join; returning agent #{agent.id} to the queue." - conditionally_return_agent agent - end - end + agent_call.on_answer { ignoring_ended_calls { agent_call.join queued_call.uri } } agent_call.on_unjoined do ignoring_ended_calls { agent_call.hangup } From 9658e3b8fc5cba3cda8280b406aa851556fbbce4 Mon Sep 17 00:00:00 2001 From: Luca Pradovera Date: Wed, 10 Dec 2014 20:13:25 +0100 Subject: [PATCH 083/136] Fix spec failures --- lib/electric_slide/call_queue.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 9eaf080..e113cdb 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -110,7 +110,6 @@ def priority_enqueue(call) # Add a call to the end of the queue, the normal FIFO queue behavior # @param [Adhearsion::Call] call Caller to be added to the queue def enqueue(call) - logger.info "Adding call from #{call.from} to the queue" call[:enqueue_time] = Time.now @queue << call unless @queue.include? call From c60f9a2528e25d8484f0fc92dbb39c74975a6e71 Mon Sep 17 00:00:00 2001 From: Luca Pradovera Date: Wed, 10 Dec 2014 20:23:49 +0100 Subject: [PATCH 084/136] Adding support for executing confirmation controllers --- lib/electric_slide/call_queue.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index e113cdb..d06acfc 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -166,7 +166,11 @@ def connect(agent, queued_call) agent.callback :connect, self, agent_call, queued_call - agent_call.dial agent.address, agent.dial_options_for(self, queued_call) + dial_options = agent.dial_options_for(self, queued_call) + + agent_call.execute_controller_or_router_on_answer dial_options.delete(:confirm), dial_options.delete(:confirm_metadata) + + agent_call.dial agent.address, dial_options end def conditionally_return_agent(agent) From ed415f005a4b9f95b493130fe099fc6c7889557b Mon Sep 17 00:00:00 2001 From: Luca Pradovera Date: Wed, 10 Dec 2014 20:41:46 +0100 Subject: [PATCH 085/136] Added logging line back in with proper spec objects --- lib/electric_slide/call_queue.rb | 1 + spec/spec_helper.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index d06acfc..7b07cef 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -110,6 +110,7 @@ def priority_enqueue(call) # Add a call to the end of the queue, the normal FIFO queue behavior # @param [Adhearsion::Call] call Caller to be added to the queue def enqueue(call) + logger.info "Adding call from #{call.from} to the queue" call[:enqueue_time] = Time.now @queue << call unless @queue.include? call diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ca91dcb..769d7c5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,6 +13,6 @@ end def dummy_call - Hash.new + Adhearsion::Call.new end From bd4d02df0a8067be33017217695fc79b1ac5f170 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Wed, 10 Dec 2014 11:10:17 -0500 Subject: [PATCH 086/136] Prevent raising if the call is expired too --- lib/electric_slide/call_queue.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 7b07cef..43230a1 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -156,7 +156,7 @@ def connect(agent, queued_call) agent.callback :disconnect, self, agent_call, queued_call unless connected - if queued_call.active? + if queued_call.alive? && queued_call.active? ignoring_ended_calls { priority_enqueue queued_call } logger.warn "Call did not connect to agent! Agent #{agent.id} call ended with #{end_event.reason}; reinserting caller #{queued_caller_id} into queue" else From bd98e8703cfb2950121efa495b02a2e39e150044 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Wed, 10 Dec 2014 11:10:31 -0500 Subject: [PATCH 087/136] Disconnect the agent if the caller hangs up --- lib/electric_slide/call_queue.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 43230a1..6c5751f 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -140,6 +140,9 @@ def connect(agent, queued_call) # TODO: Allow executing a call controller here, specified by the agent agent_call.on_answer { ignoring_ended_calls { agent_call.join queued_call.uri } } + # Disconnect agent if caller hangs up before agent answers + queued_call.on_end { ignoring_ended_calls { agent_call.hangup } } + agent_call.on_unjoined do ignoring_ended_calls { agent_call.hangup } ignoring_ended_calls { queued_call.hangup } From 9ffe93254b4a3515dc6e6ed84db24d9326cc3485 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 12 Jan 2015 16:51:06 -0500 Subject: [PATCH 088/136] Add Guard for tests --- Gemfile | 2 +- Guardfile | 9 +++++++++ electric_slide.gemspec | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 Guardfile diff --git a/Gemfile b/Gemfile index ee356d2..06618ce 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ -source :rubygems +source 'https://rubygems.org' gemspec diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000..eb34976 --- /dev/null +++ b/Guardfile @@ -0,0 +1,9 @@ +# encoding: utf-8 + +group 'rspec' do + guard 'rspec', cmd: 'bundle exec rspec' do + watch(%r{^spec/.+_spec\.rb$}) + watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } + watch('spec/spec_helper.rb') { "spec/" } + end +end diff --git a/electric_slide.gemspec b/electric_slide.gemspec index c3b2f67..fcf5c86 100644 --- a/electric_slide.gemspec +++ b/electric_slide.gemspec @@ -27,6 +27,8 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'activesupport' s.add_development_dependency 'rspec', ['>= 2.5.0'] s.add_development_dependency 'ci_reporter' + s.add_development_dependency 'guard' + s.add_development_dependency 'guard-rspec' s.add_development_dependency 'simplecov' s.add_development_dependency 'simplecov-rcov' From f6fbfaa6dd7b6594135f2378610e626b239376ab Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 12 Jan 2015 23:25:31 -0500 Subject: [PATCH 089/136] Separate out agent distribution strategy into classes --- .../agent_strategy/fixed_priority.rb | 39 +++++++++++++++++ .../agent_strategy/longest_idle.rb | 32 ++++++++++++++ lib/electric_slide/call_queue.rb | 21 ++++++---- .../agent_strategy/fixed_priority_spec.rb | 42 +++++++++++++++++++ 4 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 lib/electric_slide/agent_strategy/fixed_priority.rb create mode 100644 lib/electric_slide/agent_strategy/longest_idle.rb create mode 100644 spec/electric_slide/agent_strategy/fixed_priority_spec.rb diff --git a/lib/electric_slide/agent_strategy/fixed_priority.rb b/lib/electric_slide/agent_strategy/fixed_priority.rb new file mode 100644 index 0000000..8b8c939 --- /dev/null +++ b/lib/electric_slide/agent_strategy/fixed_priority.rb @@ -0,0 +1,39 @@ +# encoding: utf-8 + +class ElectricSlide + class AgentStrategy + class FixedPriority + def initialize + @priorities = {} + end + + def agent_available? + !!@priorities.detect do |priority, agents| + agents.present? + end + end + + def checkout_agent + _, agents = @priorities.detect do |priority, agents| + agents.present? + end + agents.shift + end + + def <<(agent) + # TODO: How aggressively do we check for agents duplicated in multiple priorities? + raise ArgumentError, "Agents must have a specified priority" unless agent[:priority] + priority = agent[:priority] + @priorities[priority] ||= [] + @priorities[priority] << agent unless @priorities[priority].include? agent + end + + def delete(agent) + @priorities.detect do |priority, agents| + agents.delete(agent) + end + end + end + end +end + diff --git a/lib/electric_slide/agent_strategy/longest_idle.rb b/lib/electric_slide/agent_strategy/longest_idle.rb new file mode 100644 index 0000000..c96d456 --- /dev/null +++ b/lib/electric_slide/agent_strategy/longest_idle.rb @@ -0,0 +1,32 @@ +# encoding: utf-8 + +class ElectricSlide + class AgentStrategy + class LongestIdle + def initialize + @free_agents = [] # Needed to keep track of waiting order + end + + # Checks whether an agent is available to take a call + # @return [Boolean] True if an agent is available + def agent_available? + @free_agents.count > 0 + end + + # Assigns the first available agent, marking the agent :busy + # @return {Agent} + def checkout_agent + @free_agents.shift + end + + def <<(agent) + @free_agents << agent unless @free_agents.include?(agent) + end + + def delete(agent) + @free_agents.delete(agent) + end + end + end +end + diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 6c5751f..1968b9c 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -1,4 +1,8 @@ # encoding: utf-8 + +# The default agent strategy +require 'electric_slide/agent_strategy/longest_idle' + class ElectricSlide class CallQueue include Celluloid @@ -7,23 +11,24 @@ def self.work(*args) self.supervise *args end - def initialize - @free_agents = [] # Needed to keep track of waiting order + def initialize(agent_strategy = AgentStrategy::LongestIdle) @agents = [] # Needed to keep track of global list of agents @queue = [] # Calls waiting for an agent + + @strategy = agent_strategy.new end # Checks whether an agent is available to take a call # @return [Boolean] True if an agent is available def agent_available? - @free_agents.count > 0 + @strategy.agent_available? end # Assigns the first available agent, marking the agent :busy # @return {Agent} def checkout_agent - agent = @free_agents.shift - agent.presence = :busy + agent = @strategy.checkout_agent + agent.status = :busy agent end @@ -51,7 +56,7 @@ def get_agent(id) def add_agent(agent) logger.info "Adding agent #{agent} to the queue" @agents << agent unless @agents.include? agent - @free_agents << agent if agent.presence == :available && !@free_agents.include?(agent) + @strategy << agent if agent.presence == :available check_for_connections end @@ -66,7 +71,7 @@ def return_agent(agent, status = :available, address = nil) agent.address = address if address if agent.presence == :available - @free_agents << agent unless @free_agents.include? agent + @strategy << agent check_for_connections end agent @@ -77,7 +82,7 @@ def return_agent(agent, status = :available, address = nil) # @return [Agent, Nil] The Agent object if removed, Nil otherwise def remove_agent(agent) logger.info "Removing agent #{agent} from the queue" - @free_agents.delete agent + @strategy.delete agent @agents.delete agent end diff --git a/spec/electric_slide/agent_strategy/fixed_priority_spec.rb b/spec/electric_slide/agent_strategy/fixed_priority_spec.rb new file mode 100644 index 0000000..24c41c9 --- /dev/null +++ b/spec/electric_slide/agent_strategy/fixed_priority_spec.rb @@ -0,0 +1,42 @@ +# encoding: utf-8 + +require 'spec_helper' +require 'electric_slide/agent_strategy/fixed_priority' + +describe ElectricSlide::AgentStrategy::FixedPriority do + let(:subject) { ElectricSlide::AgentStrategy::FixedPriority.new } + it 'should allow adding an agent with a specified priority' do + subject.agent_available?.should be false + subject << { id: 101, priority: 1 } + subject.agent_available?.should be true + end + + it 'should allow adding multiple agents at the same priority' do + agent1 = { id: 101, priority: 2 } + agent2 = { id: 102, priority: 2 } + subject << agent1 + subject << agent2 + subject.checkout_agent.should == agent1 + end + + it 'should return all agents of a higher priority before returning an agent of a lower priority' do + agent1 = { id: 101, priority: 2 } + agent2 = { id: 102, priority: 2 } + agent3 = { id: 103, priority: 3 } + subject << agent1 + subject << agent2 + subject << agent3 + subject.checkout_agent.should == agent1 + subject.checkout_agent.should == agent2 + subject.checkout_agent.should == agent3 + end + + it 'should detect an agent available if one is available at any priority' do + agent1 = { id: 101, priority: 2 } + agent2 = { id: 102, priority: 3 } + subject << agent1 + subject << agent2 + subject.checkout_agent + subject.agent_available?.should == true + end +end From 9fbff63770f8ace4823dd27c53939a9b65df5710 Mon Sep 17 00:00:00 2001 From: Luca Pradovera Date: Fri, 16 Jan 2015 16:21:22 +0100 Subject: [PATCH 090/136] Agent objects use methods, not has syntax --- .../agent_strategy/fixed_priority.rb | 4 ++-- .../agent_strategy/fixed_priority_spec.rb | 17 +++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/electric_slide/agent_strategy/fixed_priority.rb b/lib/electric_slide/agent_strategy/fixed_priority.rb index 8b8c939..419ea2e 100644 --- a/lib/electric_slide/agent_strategy/fixed_priority.rb +++ b/lib/electric_slide/agent_strategy/fixed_priority.rb @@ -22,8 +22,8 @@ def checkout_agent def <<(agent) # TODO: How aggressively do we check for agents duplicated in multiple priorities? - raise ArgumentError, "Agents must have a specified priority" unless agent[:priority] - priority = agent[:priority] + raise ArgumentError, "Agents must have a specified priority" unless agent.respond_to?(:priority) + priority = agent.priority @priorities[priority] ||= [] @priorities[priority] << agent unless @priorities[priority].include? agent end diff --git a/spec/electric_slide/agent_strategy/fixed_priority_spec.rb b/spec/electric_slide/agent_strategy/fixed_priority_spec.rb index 24c41c9..2d40c56 100644 --- a/spec/electric_slide/agent_strategy/fixed_priority_spec.rb +++ b/spec/electric_slide/agent_strategy/fixed_priority_spec.rb @@ -2,27 +2,28 @@ require 'spec_helper' require 'electric_slide/agent_strategy/fixed_priority' +require 'ostruct' describe ElectricSlide::AgentStrategy::FixedPriority do let(:subject) { ElectricSlide::AgentStrategy::FixedPriority.new } it 'should allow adding an agent with a specified priority' do subject.agent_available?.should be false - subject << { id: 101, priority: 1 } + subject << OpenStruct.new({ id: 101, priority: 1 }) subject.agent_available?.should be true end it 'should allow adding multiple agents at the same priority' do - agent1 = { id: 101, priority: 2 } - agent2 = { id: 102, priority: 2 } + agent1 = OpenStruct.new({ id: 101, priority: 2 }) + agent2 = OpenStruct.new({ id: 102, priority: 2 }) subject << agent1 subject << agent2 subject.checkout_agent.should == agent1 end it 'should return all agents of a higher priority before returning an agent of a lower priority' do - agent1 = { id: 101, priority: 2 } - agent2 = { id: 102, priority: 2 } - agent3 = { id: 103, priority: 3 } + agent1 = OpenStruct.new({ id: 101, priority: 2 }) + agent2 = OpenStruct.new({ id: 102, priority: 2 }) + agent3 = OpenStruct.new({ id: 103, priority: 3 }) subject << agent1 subject << agent2 subject << agent3 @@ -32,8 +33,8 @@ end it 'should detect an agent available if one is available at any priority' do - agent1 = { id: 101, priority: 2 } - agent2 = { id: 102, priority: 3 } + agent1 = OpenStruct.new({ id: 101, priority: 2 }) + agent2 = OpenStruct.new({ id: 102, priority: 3 }) subject << agent1 subject << agent2 subject.checkout_agent From d9a2c65746d3760f6bed5211ad4feab1ecc7ad18 Mon Sep 17 00:00:00 2001 From: Luca Pradovera Date: Fri, 16 Jan 2015 17:00:10 +0100 Subject: [PATCH 091/136] Agents have presence, not status --- lib/electric_slide/call_queue.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 1968b9c..576756f 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -28,7 +28,7 @@ def agent_available? # @return {Agent} def checkout_agent agent = @strategy.checkout_agent - agent.status = :busy + agent.presence = :busy agent end From b36540e7ee7f2d8b0cebd4d538970d1ca6f638d2 Mon Sep 17 00:00:00 2001 From: Luca Pradovera Date: Fri, 16 Jan 2015 18:02:37 +0100 Subject: [PATCH 092/136] Added sensible default to priority --- lib/electric_slide/agent_strategy/fixed_priority.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/agent_strategy/fixed_priority.rb b/lib/electric_slide/agent_strategy/fixed_priority.rb index 419ea2e..a09dbe9 100644 --- a/lib/electric_slide/agent_strategy/fixed_priority.rb +++ b/lib/electric_slide/agent_strategy/fixed_priority.rb @@ -23,7 +23,7 @@ def checkout_agent def <<(agent) # TODO: How aggressively do we check for agents duplicated in multiple priorities? raise ArgumentError, "Agents must have a specified priority" unless agent.respond_to?(:priority) - priority = agent.priority + priority = agent.priority || 999999 @priorities[priority] ||= [] @priorities[priority] << agent unless @priorities[priority].include? agent end From 2b2e30a2002594f441533b3abe0fa676d1360a32 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Wed, 14 Jan 2015 17:33:00 -0500 Subject: [PATCH 093/136] Remove unused variable assignment --- lib/electric_slide/call_queue.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 576756f..2a7af11 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -96,7 +96,7 @@ def check_for_connections rescue Adhearsion::Call::ExpiredError next end - result = connect checkout_agent, call + connect checkout_agent, call break end end From e809ba24a9b33edaf7a0be61e50aab02a1db8851 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Wed, 14 Jan 2015 17:43:03 -0500 Subject: [PATCH 094/136] Break out connection type to a dedicated method --- lib/electric_slide/call_queue.rb | 89 ++++++++++++++++---------- spec/electric_slide/call_queue_spec.rb | 3 + 2 files changed, 57 insertions(+), 35 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 2a7af11..3b8c6d7 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -7,11 +7,21 @@ class ElectricSlide class CallQueue include Celluloid + CONNECTION_TYPES = [ + :call, + ].freeze + def self.work(*args) self.supervise *args end - def initialize(agent_strategy = AgentStrategy::LongestIdle) + def initialize(opts = {}) + agent_strategy = opts[:agent_strategy] || AgentStrategy::LongestIdle + connection_type = opts[:connection_type] || :call + + raise ArgumentError, "Invalid connection type; must be one of #{CONNECTION_TYPES.join ','}" unless CONNECTION_TYPES.include? connection_type + + @free_agents = [] # Needed to keep track of waiting order @agents = [] # Needed to keep track of global list of agents @queue = [] # Calls waiting for an agent @@ -134,7 +144,50 @@ def abandon(call) # @param [Adhearsion::Call] call Caller to be connected def connect(agent, queued_call) logger.info "Connecting #{agent} with #{queued_call.from}" + case @connection_type + when :call + call_agent agent, queued_call + when :bridge + bridge_agent agent, queued_call + end + end + + def conditionally_return_agent(agent) + if agent && @agents.include?(agent) && agent.presence == :busy + logger.info "Returning agent #{agent.id} to queue" + return_agent agent + else + logger.debug "Not returning agent #{agent.inspect} to the queue" + end + end + + # Returns the next waiting caller + # @return [Adhearsion::Call] The next waiting caller + def get_next_caller + @queue.shift + end + + # Checks whether any callers are waiting + # @return [Boolean] True if a caller is waiting + def call_waiting? + @queue.length > 0 + end + + # Returns the number of callers waiting in the queue + # @return [Fixnum] + def calls_waiting + @queue.length + end + + private + # @private + def ignoring_ended_calls + yield + rescue Celluloid::DeadActorError, Adhearsion::Call::Hangup, Adhearsion::Call::ExpiredError + # This actor may previously have been shut down due to the call ending + end + def call_agent(agent, queued_call) agent_call = Adhearsion::OutboundCall.new agent_call[:agent] = agent agent_call[:queued_call] = queued_call @@ -182,39 +235,5 @@ def connect(agent, queued_call) agent_call.dial agent.address, dial_options end - def conditionally_return_agent(agent) - if agent && @agents.include?(agent) && agent.presence == :busy - logger.info "Returning agent #{agent.id} to queue" - return_agent agent - else - logger.debug "Not returning agent #{agent.inspect} to the queue" - end - end - - # Returns the next waiting caller - # @return [Adhearsion::Call] The next waiting caller - def get_next_caller - @queue.shift - end - - # Checks whether any callers are waiting - # @return [Boolean] True if a caller is waiting - def call_waiting? - @queue.length > 0 - end - - # Returns the number of callers waiting in the queue - # @return [Fixnum] - def calls_waiting - @queue.length - end - - private - # @private - def ignoring_ended_calls - yield - rescue Celluloid::DeadActorError, Adhearsion::Call::Hangup, Adhearsion::Call::ExpiredError - # This actor may previously have been shut down due to the call ending - end end end diff --git a/spec/electric_slide/call_queue_spec.rb b/spec/electric_slide/call_queue_spec.rb index 13df11e..734eebe 100644 --- a/spec/electric_slide/call_queue_spec.rb +++ b/spec/electric_slide/call_queue_spec.rb @@ -27,4 +27,7 @@ it "should select the agent that has been waiting the longest" + it "should raise when given an invalid connection type" do + expect { ElectricSlide::CallQueue.new :blah }.to raise_error + end end From 6b829182aaa2256c00b86ede9a1c1d537b9b10db Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Wed, 14 Jan 2015 17:43:18 -0500 Subject: [PATCH 095/136] Placeholder for :bridge connection type --- lib/electric_slide/call_queue.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 3b8c6d7..84490c5 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -9,6 +9,7 @@ class CallQueue CONNECTION_TYPES = [ :call, + :bridge, ].freeze def self.work(*args) @@ -235,5 +236,7 @@ def call_agent(agent, queued_call) agent_call.dial agent.address, dial_options end + def bridge_agent(agent, queued_call) + end end end From 6c201f3ffb2cd4b78352f8b001558fa7f52a0c59 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Wed, 14 Jan 2015 19:17:04 -0500 Subject: [PATCH 096/136] Start work on bridge connection type --- lib/electric_slide/agent.rb | 9 +++++++++ lib/electric_slide/call_queue.rb | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/lib/electric_slide/agent.rb b/lib/electric_slide/agent.rb index f8f0290..1eaaaa5 100644 --- a/lib/electric_slide/agent.rb +++ b/lib/electric_slide/agent.rb @@ -35,5 +35,14 @@ def dial_options_for(queue, queued_call) {} end + def join(queued_call) + # For use in queues that need bridge connections + @call.join queued_call + end + + # FIXME: Use delegator? + def from + @call.from + end end diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 84490c5..b16fea3 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -237,6 +237,28 @@ def call_agent(agent, queued_call) end def bridge_agent(agent, queued_call) + # Stash caller ID to make log messages work even if calls end + queued_caller_id = queued_call.from + + agent.call.on_unjoined do + ignoring_ended_calls { queued_call.hangup } + end + + agent.join queued_call + rescue Celluloid::DeadActorError, Adhearsion::Call::Hangup, Adhearsion::Call::ExpiredError + ignoring_ended_calls do + if agent.active? + logger.info "Caller #{queued_caller_id} failed to connect to Agent #{agent.id} due to caller hangup" + conditionally_return_agent agent + end + end + + ignoring_ended_calls do + if queued_call.active? + priority_enqueue queued_call + logger.warn "Call failed to connect to Agent #{agent.id} due to agent hangup; reinserting caller #{queued_caller_id} into queue" + end + end end end end From 372aa1b3ad15229ae8eefdaa88f02ed752bca028 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 16 Jan 2015 15:00:44 -0500 Subject: [PATCH 097/136] Fix false positive in the spec --- lib/electric_slide/call_queue.rb | 2 +- spec/electric_slide/call_queue_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index b16fea3..bd7afba 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -17,7 +17,7 @@ def self.work(*args) end def initialize(opts = {}) - agent_strategy = opts[:agent_strategy] || AgentStrategy::LongestIdle + agent_strategy = opts[:agent_strategy] || AgentStrategy::LongestIdle connection_type = opts[:connection_type] || :call raise ArgumentError, "Invalid connection type; must be one of #{CONNECTION_TYPES.join ','}" unless CONNECTION_TYPES.include? connection_type diff --git a/spec/electric_slide/call_queue_spec.rb b/spec/electric_slide/call_queue_spec.rb index 734eebe..234fad1 100644 --- a/spec/electric_slide/call_queue_spec.rb +++ b/spec/electric_slide/call_queue_spec.rb @@ -28,6 +28,6 @@ it "should select the agent that has been waiting the longest" it "should raise when given an invalid connection type" do - expect { ElectricSlide::CallQueue.new :blah }.to raise_error + expect { ElectricSlide::CallQueue.new connection_type: :blah }.to raise_error end end From 15fc602e5337ea53520cde629e39963ff9ac30bb Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 16 Jan 2015 15:04:45 -0500 Subject: [PATCH 098/136] This spec has been moved to a strategy --- spec/electric_slide/call_queue_spec.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/electric_slide/call_queue_spec.rb b/spec/electric_slide/call_queue_spec.rb index 234fad1..54b1d99 100644 --- a/spec/electric_slide/call_queue_spec.rb +++ b/spec/electric_slide/call_queue_spec.rb @@ -25,8 +25,6 @@ expect(queue.get_next_caller).to be call_a end - it "should select the agent that has been waiting the longest" - it "should raise when given an invalid connection type" do expect { ElectricSlide::CallQueue.new connection_type: :blah }.to raise_error end From 9c7a6c222c659d62a167c10b79b19b83554856a7 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 16 Jan 2015 15:05:38 -0500 Subject: [PATCH 099/136] Spec for call abandonment --- spec/electric_slide/call_queue_spec.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spec/electric_slide/call_queue_spec.rb b/spec/electric_slide/call_queue_spec.rb index 54b1d99..f9179e3 100644 --- a/spec/electric_slide/call_queue_spec.rb +++ b/spec/electric_slide/call_queue_spec.rb @@ -25,6 +25,13 @@ expect(queue.get_next_caller).to be call_a end + it "should remove a caller who abandons the queue" do + queue.enqueue call_a + queue.enqueue call_b + queue.abandon call_a + expect(queue.get_next_caller).to be call_b + end + it "should raise when given an invalid connection type" do expect { ElectricSlide::CallQueue.new connection_type: :blah }.to raise_error end From 5fe597ca9ffd9577a0572f5916779c74eb5394fd Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 16 Jan 2015 16:07:34 -0500 Subject: [PATCH 100/136] Store the connection type for reference later --- lib/electric_slide/call_queue.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index bd7afba..4e8363d 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -17,10 +17,10 @@ def self.work(*args) end def initialize(opts = {}) - agent_strategy = opts[:agent_strategy] || AgentStrategy::LongestIdle - connection_type = opts[:connection_type] || :call + agent_strategy = opts[:agent_strategy] || AgentStrategy::LongestIdle + @connection_type = opts[:connection_type] || :call - raise ArgumentError, "Invalid connection type; must be one of #{CONNECTION_TYPES.join ','}" unless CONNECTION_TYPES.include? connection_type + raise ArgumentError, "Invalid connection type; must be one of #{CONNECTION_TYPES.join ','}" unless CONNECTION_TYPES.include? @connection_type @free_agents = [] # Needed to keep track of waiting order @agents = [] # Needed to keep track of global list of agents From 7bc42af6a5c833c8d81530fce7cb0151eadc48e4 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 16 Jan 2015 16:07:44 -0500 Subject: [PATCH 101/136] Try to make sure the agent we add is useable --- lib/electric_slide/call_queue.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 4e8363d..c44daa0 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -65,6 +65,14 @@ def get_agent(id) # Registers an agent to the queue # @param [Agent] agent The agent to be added to the queue def add_agent(agent) + case @connection_type + when :call + # FIXME: We want this to raise in the caller, and not kill the Queue actor + raise ArgumentError, "Agent has no callable address" unless agent.address + when :bridge + raise ArgumentError, "Agent has no active call" unless agent.call && agent.call.active? + end + logger.info "Adding agent #{agent} to the queue" @agents << agent unless @agents.include? agent @strategy << agent if agent.presence == :available From 8573bdff8822abed250c6bf6ac70d73c20f6c1e3 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 16 Jan 2015 16:07:53 -0500 Subject: [PATCH 102/136] Fix end-of-call conditions for agents --- lib/electric_slide/call_queue.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index c44daa0..8083ec0 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -250,12 +250,17 @@ def bridge_agent(agent, queued_call) agent.call.on_unjoined do ignoring_ended_calls { queued_call.hangup } + ignoring_ended_calls { conditionally_return_agent agent if agent.call.active? } + end + + agent.call.on_end do + remove_agent agent end agent.join queued_call rescue Celluloid::DeadActorError, Adhearsion::Call::Hangup, Adhearsion::Call::ExpiredError ignoring_ended_calls do - if agent.active? + if agent.call.active? logger.info "Caller #{queued_caller_id} failed to connect to Agent #{agent.id} due to caller hangup" conditionally_return_agent agent end From dde0cc50f45b1e91e50d0fdd4cc9e00cb39b1199 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 16 Jan 2015 17:41:45 -0500 Subject: [PATCH 103/136] Get the number of available agents from the queue --- .../agent_strategy/fixed_priority.rb | 16 ++++++++++++++++ .../agent_strategy/longest_idle.rb | 7 +++++++ lib/electric_slide/call_queue.rb | 9 +++++++++ 3 files changed, 32 insertions(+) diff --git a/lib/electric_slide/agent_strategy/fixed_priority.rb b/lib/electric_slide/agent_strategy/fixed_priority.rb index a09dbe9..8987294 100644 --- a/lib/electric_slide/agent_strategy/fixed_priority.rb +++ b/lib/electric_slide/agent_strategy/fixed_priority.rb @@ -13,6 +13,22 @@ def agent_available? end end + # Returns information about the number of available agents + # The data returned depends on the AgentStrategy in use. + # @return [Hash] Summary information about agents available, depending on strategy + # :total: The total number of available agents + # :priorities: A Hash containing the number of available agents at each priority + def available_agent_summary + @priorities.inject({}) do |summary, data| + priority, agents = *data + summary[:total] ||= 0 + summary[:total] += agents.count + summary[:priorities] ||= {} + summary[:priorities][priority] = agents.count + summary + end + end + def checkout_agent _, agents = @priorities.detect do |priority, agents| agents.present? diff --git a/lib/electric_slide/agent_strategy/longest_idle.rb b/lib/electric_slide/agent_strategy/longest_idle.rb index c96d456..b9ff5e5 100644 --- a/lib/electric_slide/agent_strategy/longest_idle.rb +++ b/lib/electric_slide/agent_strategy/longest_idle.rb @@ -13,6 +13,13 @@ def agent_available? @free_agents.count > 0 end + # Returns a count of the number of available agents + # @return [Hash] Hash of information about available agents + # This strategy only returns the count of agents available with :total + def count_available_agents + { total: @free_agents } + end + # Assigns the first available agent, marking the agent :busy # @return {Agent} def checkout_agent diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 8083ec0..1bfd0a7 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -35,6 +35,15 @@ def agent_available? @strategy.agent_available? end + # Returns information about the number of available agents + # The data returned depends on the AgentStrategy in use. + # The data will always include a :total count of the agents available + # @return [Hash] Summary information about agents available, depending on strategy + def available_agent_summary + # TODO: Make this a delegator? + @strategy.available_agent_summary + end + # Assigns the first available agent, marking the agent :busy # @return {Agent} def checkout_agent From 673dd7bbd071bd26dc859eafa090aa9335814a21 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 16 Jan 2015 18:30:51 -0500 Subject: [PATCH 104/136] Documentation update --- README.markdown | 65 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/README.markdown b/README.markdown index 4f001e8..e3f71a8 100644 --- a/README.markdown +++ b/README.markdown @@ -8,10 +8,11 @@ To ensure proper operation, a few things are assumed: * Agents will only be logged into a single queue at a time If you have two types of agents (say "support" and "sales") then you should have two queues, each with their own pool of agents * Agent authentication will happen before entering the queue - it is not the queue's concern -* The strategy for both agents and callers is FIFO - the first (available) of each type to begin waiting is selected -* Other (custom) strategies can be implemented by creating custom queue implementations - see below +* The strategy for callers is FIFO: the caller who has been waiting the longest is the next to get an agent * Queues will be implemented as a Celluloid Actor, which should protect the call selection strategies against race conditions -* When an agent is selected to take a call, the agent is called. For other behaviors, a custom queue must be implemented +* There are two ways to connect an agent: + - If the Agent object provides an `address` attribute, and the queue's `connection_type` is set to `call`, then the queue will call the agent when a caller is waiting + - If the Agent object provides a `call` attribute, and the queue's `connection_type` is set to `bridge`, then the call queue will bridge the agent to the caller. In this mode, the agent hanging up will log him out of the queue TODO: * Example for using Matrioska to offer Agents and Callers interactivity while waiting @@ -20,8 +21,11 @@ TODO: Example Queue ------------- -```Ruby -my_queue = ElectricSlide.create :my_queue +```ruby +my_queue = ElectricSlide.create :my_queue, ElectricSlide::CallQueue +# While you can have ElectricSlide keep track of custom queues, it is recommended to use the built-in CallQueue object +# NOTE! The authors of ElectricSlide recommend NOT to subclass, monkeypatch, or otherwise alter the CallQueue implementation, as +# the likelihood of creating race conditions is high. # Another way to get a handle on a queue ElectricSlide.create :my_queue @@ -32,7 +36,7 @@ my_queue = ElectricSlide.get_queue :my_queue Example CallController for Queued Call -------------------------------------- -```Ruby +```ruby class EnterTheQueue < Adhearsion::CallController def run answer @@ -46,9 +50,10 @@ class EnterTheQueue < Adhearsion::CallController end ElectricSlide.get_queue(:my_queue).enqueue call - # Blocks until call is done talking to the agent - - say "Goodbye" + + # The controller will exit, but the call will remain up + # The call will automatically hang up after speaking to an agent + call.auto_hangup = false end end ``` @@ -61,24 +66,58 @@ ElectricSlide expects to be given a objects that quack like an agent. You can us To add an agent who will receive calls whenever a call is enqueued, do something like this: -```Ruby +```ruby agent = ElectricSlide::Agent.new id: 1, address: 'sip:agent1@example.com', presence: :available ElectricSlide.get_queue(:my_queue).add_agent agent ``` To inform the queue that the agent is no longer available you *must* use the ElectricSlide queue interface. /Do not attempt to alter agent objects directly!/ -```Ruby +```ruby ElectricSlide.update_agent 1, presence: offline ``` If it is more convenient, you may also pass `#update_agent` an Agent-like object: -```Ruby -agent = ElectricSlide::Agent.new id:1, address: 'sip:agent1@example.com', presence: :offline +```ruby +options = { + id: 1, + address: 'sip:agent1@example.com', + presence: offline +} +agent = ElectricSlide::Agent.new options ElectricSlide.update_agent 1, agent ``` +Switching connection types +-------------------------- + +ElectricSlide provides two methods for connecting callers to agents: +- `:call`: (default) If the Agent object provides an `address` attribute, and the queue's `connection_type` is set to `call`, then the queue will call the agent when a caller is waiting +- `:bridge`: If the Agent object provides a `call` attribute, and the queue's `connection_type` is set to `bridge`, then the call queue will bridge the agent to the caller. In this mode, the agent hanging up will log him out of the queue + +To select the connection type, specify it when creating the queue: + +```ruby +ElectricSlide.create_queue :my_queue, ElectricSlide::CallQueue, connection_type: :bridge +``` + +Selecting an Agent distribution strategy +---------------------------------------- + +Different use-cases have different requirements for selecting the next agent to take a call. ElectricSlide provides two strategies which may be used. You are also welcome to create your own distribution strategy by implementing the same interface as described in `ElectricSlide::AgentStrategy::LongestIdle`. + +To select an agent strategy, specify it when creating the queue: + +```ruby +ElectricSlide.create_queue :my_queue, ElectricSlide::CallQueue, agent_strategy: ElectricSlide::AgentStrategy::LongestIdle +``` + +Two strategies are provided out-of-the-box: + +* `ElectricSlide::AgentStrategy::LongestIdle` selects the agent that has been idle for the longest amount of time. +* `ElectricSlide::AgentStrategy::FixedPriority` selects the agent with the lowest numeric priority first. In the event that more than one agent is available at a given priority, then the agent that has been idle the longest at the lowest numeric priority is selected. + Custom Agent Behavior ---------------------------- From 3ee64ac3ec84c8c0cfc4bc6310e99cbfc08ef25a Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 16 Jan 2015 18:31:58 -0500 Subject: [PATCH 105/136] Preserve arguments to agent callbacks --- lib/electric_slide/agent.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/agent.rb b/lib/electric_slide/agent.rb index 1eaaaa5..965075d 100644 --- a/lib/electric_slide/agent.rb +++ b/lib/electric_slide/agent.rb @@ -14,7 +14,7 @@ def initialize(opts = {}) def callback(type, *args) callback = instance_variable_get "@#{type}_callback" - callback.call if callback && callback.respond_to?(:call) + callback.call(*args) if callback && callback.respond_to?(:call) end From ff0f55cdd2d6a41014696347c5a4ff0b2d5638a1 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 16 Jan 2015 19:33:50 -0500 Subject: [PATCH 106/136] Fix callback definition & execution --- lib/electric_slide/agent.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/electric_slide/agent.rb b/lib/electric_slide/agent.rb index 965075d..2aca35b 100644 --- a/lib/electric_slide/agent.rb +++ b/lib/electric_slide/agent.rb @@ -13,20 +13,20 @@ def initialize(opts = {}) end def callback(type, *args) - callback = instance_variable_get "@#{type}_callback" + callback = self.class.instance_variable_get "@#{type}_callback" callback.call(*args) if callback && callback.respond_to?(:call) end # Provide a block to be called when this agent is connected to a caller # The block will be passed the queue, the agent call and the client call - def on_connect(&block) + def self.on_connect(&block) @connect_callback = block end # Provide a block to be called when this agent is disconnected to a caller # The block will be passed the queue, the agent call and the client call - def on_disconnect(&block) + def self.on_disconnect(&block) @disconnect_callback = block end From 89be8699da48919a2c1d4f579d34992ea30e03cc Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 16 Jan 2015 21:17:54 -0500 Subject: [PATCH 107/136] Push exceptions back into the caller when adding an invalid agent --- lib/electric_slide/call_queue.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 1bfd0a7..3cb4189 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -76,10 +76,9 @@ def get_agent(id) def add_agent(agent) case @connection_type when :call - # FIXME: We want this to raise in the caller, and not kill the Queue actor - raise ArgumentError, "Agent has no callable address" unless agent.address + abort ArgumentError, "Agent has no callable address" unless agent.address when :bridge - raise ArgumentError, "Agent has no active call" unless agent.call && agent.call.active? + abort ArgumentError, "Agent has no active call" unless agent.call && agent.call.active? end logger.info "Adding agent #{agent} to the queue" From 2df54d5f873747f58e979d587627aac73d2ecba2 Mon Sep 17 00:00:00 2001 From: Luca Pradovera Date: Mon, 19 Jan 2015 23:41:49 +0100 Subject: [PATCH 108/136] Made it possible to actually run a confirmation controller on agent answer --- README.markdown | 27 ++++++++++++++++++++++++++- lib/electric_slide/call_queue.rb | 9 +++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/README.markdown b/README.markdown index e3f71a8..e2315bf 100644 --- a/README.markdown +++ b/README.markdown @@ -50,7 +50,7 @@ class EnterTheQueue < Adhearsion::CallController end ElectricSlide.get_queue(:my_queue).enqueue call - + # The controller will exit, but the call will remain up # The call will automatically hang up after speaking to an agent call.auto_hangup = false @@ -125,3 +125,28 @@ If you need custom functionality to occur whenever an Agent is selected to take * `on_connect` * `on_disconnect` + +Confirmation Controllers +------------------------ + +In case you need to execute a confirmation controller on the call that is placed to the agent, such as "Press 1 to accept the call", you currently need to pass in the confirmation class name and the call object as metadata in the `call_options_for` callback in your `ElectricSlide::Agent` subclass. + +```ruby +# an example from the Agent subclass +def dial_options_for(queue, queued_call) + { + from: caller_digits(queued_call.from), + timeout: on_pstn? ? APP_CONFIG.agent_timeout * 3 : APP_CONFIG.agent_timeout, + confirm: MyConfirmationController, + confirm_metadata: {caller: queued_call, agent: self}, + } +end +``` + +You then need to handle the join in your confirmation controller, using for example: + +```ruby +call.join metadata[:caller] if confirm! +``` + +where `confirm!` is your logic for deciding if you want the call to be connected or not. Hanging up during the confirmation controller or letting it finish without any action will result in the call being sent to the next agent. diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 3cb4189..ac76859 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -212,8 +212,11 @@ def call_agent(agent, queued_call) # Stash the caller ID so we don't have to try to get it from a dead call object later queued_caller_id = queued_call.from - # TODO: Allow executing a call controller here, specified by the agent - agent_call.on_answer { ignoring_ended_calls { agent_call.join queued_call.uri } } + # The call controller is actually run by #dial, here we skip joining if we do not have one + dial_options = agent.dial_options_for(self, queued_call) + unless dial_options[:confirm] + agent_call.on_answer { ignoring_ended_calls { agent_call.join queued_call.uri } } + end # Disconnect agent if caller hangs up before agent answers queued_call.on_end { ignoring_ended_calls { agent_call.hangup } } @@ -245,8 +248,6 @@ def call_agent(agent, queued_call) agent.callback :connect, self, agent_call, queued_call - dial_options = agent.dial_options_for(self, queued_call) - agent_call.execute_controller_or_router_on_answer dial_options.delete(:confirm), dial_options.delete(:confirm_metadata) agent_call.dial agent.address, dial_options From 6aaa6fac911f59506c9e65295719c2034dd0209c Mon Sep 17 00:00:00 2001 From: Lloyd Hughes Date: Sat, 24 Jan 2015 21:03:53 +0200 Subject: [PATCH 109/136] Fixed available_agent_summary issue --- lib/electric_slide/agent_strategy/longest_idle.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/agent_strategy/longest_idle.rb b/lib/electric_slide/agent_strategy/longest_idle.rb index b9ff5e5..e4db0f7 100644 --- a/lib/electric_slide/agent_strategy/longest_idle.rb +++ b/lib/electric_slide/agent_strategy/longest_idle.rb @@ -16,7 +16,7 @@ def agent_available? # Returns a count of the number of available agents # @return [Hash] Hash of information about available agents # This strategy only returns the count of agents available with :total - def count_available_agents + def available_agent_summary { total: @free_agents } end From e3d5eac927adbc2b7d0f217956699a1ae9d33f94 Mon Sep 17 00:00:00 2001 From: Lloyd Hughes Date: Sat, 24 Jan 2015 21:13:22 +0200 Subject: [PATCH 110/136] Added callbacks to the :bridge connection method --- lib/electric_slide/call_queue.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index ac76859..64d0759 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -258,6 +258,7 @@ def bridge_agent(agent, queued_call) queued_caller_id = queued_call.from agent.call.on_unjoined do + agent.callback :disconnect, self, agent.call, queued_call ignoring_ended_calls { queued_call.hangup } ignoring_ended_calls { conditionally_return_agent agent if agent.call.active? } end @@ -266,6 +267,10 @@ def bridge_agent(agent, queued_call) remove_agent agent end + agent.call.on_joined do + agent.callback :connect, self, agent.call, queued_call + end + agent.join queued_call rescue Celluloid::DeadActorError, Adhearsion::Call::Hangup, Adhearsion::Call::ExpiredError ignoring_ended_calls do From 1ff5f2ece4b947c49de80041e62c6a503bb293d1 Mon Sep 17 00:00:00 2001 From: Lloyd Hughes Date: Sat, 24 Jan 2015 23:05:05 +0200 Subject: [PATCH 111/136] Return count and not array of free agents --- lib/electric_slide/agent_strategy/longest_idle.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/agent_strategy/longest_idle.rb b/lib/electric_slide/agent_strategy/longest_idle.rb index e4db0f7..23a9a19 100644 --- a/lib/electric_slide/agent_strategy/longest_idle.rb +++ b/lib/electric_slide/agent_strategy/longest_idle.rb @@ -17,7 +17,7 @@ def agent_available? # @return [Hash] Hash of information about available agents # This strategy only returns the count of agents available with :total def available_agent_summary - { total: @free_agents } + { total: @free_agents.count } end # Assigns the first available agent, marking the agent :busy From 6a0f19048708ab143e93313fc05ca9dee73bd4e8 Mon Sep 17 00:00:00 2001 From: Lloyd Hughes Date: Sun, 25 Jan 2015 14:44:08 +0200 Subject: [PATCH 112/136] Added ability to select :manual or :auto returning of an agent to the available queue --- lib/electric_slide/call_queue.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 64d0759..34e8928 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -12,6 +12,11 @@ class CallQueue :bridge, ].freeze + AGENT_RETURN_METHODS = [ + :auto, + :manual, + ].freeze + def self.work(*args) self.supervise *args end @@ -19,8 +24,10 @@ def self.work(*args) def initialize(opts = {}) agent_strategy = opts[:agent_strategy] || AgentStrategy::LongestIdle @connection_type = opts[:connection_type] || :call + @agent_return_method = opts[:agent_return_method] || :auto raise ArgumentError, "Invalid connection type; must be one of #{CONNECTION_TYPES.join ','}" unless CONNECTION_TYPES.include? @connection_type + raise ArgumentError, "Invalid requeue method; must be one of #{AGENT_RETURN_METHODS.join ','}" unless AGENT_RETURN_METHODS.include? @agent_return_method @free_agents = [] # Needed to keep track of waiting order @agents = [] # Needed to keep track of global list of agents @@ -170,7 +177,7 @@ def connect(agent, queued_call) end def conditionally_return_agent(agent) - if agent && @agents.include?(agent) && agent.presence == :busy + if agent && @agents.include?(agent) && agent.presence == :busy && @agent_return_method == :auto logger.info "Returning agent #{agent.id} to queue" return_agent agent else @@ -256,11 +263,13 @@ def call_agent(agent, queued_call) def bridge_agent(agent, queued_call) # Stash caller ID to make log messages work even if calls end queued_caller_id = queued_call.from + agent.call[:queued_call] = queued_call agent.call.on_unjoined do agent.callback :disconnect, self, agent.call, queued_call ignoring_ended_calls { queued_call.hangup } ignoring_ended_calls { conditionally_return_agent agent if agent.call.active? } + agent.call[:queued_call] = nil end agent.call.on_end do From 00367bfc4e9fd64fb1a7370e6801071efbad6bf2 Mon Sep 17 00:00:00 2001 From: Victor Luft Date: Mon, 26 Jan 2015 08:11:02 -0800 Subject: [PATCH 113/136] Avoid firing callbacks in bridge mode for every call agent has received this session. --- lib/electric_slide/call_queue.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 34e8928..8f556ae 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -265,7 +265,7 @@ def bridge_agent(agent, queued_call) queued_caller_id = queued_call.from agent.call[:queued_call] = queued_call - agent.call.on_unjoined do + agent.call.register_tmp_handler :event, Punchblock::Event::Unjoined do agent.callback :disconnect, self, agent.call, queued_call ignoring_ended_calls { queued_call.hangup } ignoring_ended_calls { conditionally_return_agent agent if agent.call.active? } @@ -276,9 +276,7 @@ def bridge_agent(agent, queued_call) remove_agent agent end - agent.call.on_joined do - agent.callback :connect, self, agent.call, queued_call - end + agent.callback :connect, self, agent.call, queued_call agent.join queued_call rescue Celluloid::DeadActorError, Adhearsion::Call::Hangup, Adhearsion::Call::ExpiredError From 9819d08b72d000be23928acd5447eb2f65363e12 Mon Sep 17 00:00:00 2001 From: Lloyd Hughes Date: Sun, 25 Jan 2015 14:44:08 +0200 Subject: [PATCH 114/136] Fixed issue where agents are not added back to queue when a call is dropped --- lib/electric_slide/call_queue.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 8f556ae..ac1d214 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -176,8 +176,12 @@ def connect(agent, queued_call) end end - def conditionally_return_agent(agent) - if agent && @agents.include?(agent) && agent.presence == :busy && @agent_return_method == :auto + def conditionally_return_agent(agent, tmp_return_method = nil) + method = tmp_return_method ||= @agent_return_method + + raise ArgumentError, "Invalid requeue method; must be one of #{AGENT_RETURN_METHODS.join ','}" unless AGENT_RETURN_METHODS.include? method + + if agent && @agents.include?(agent) && agent.presence == :busy && method == :auto logger.info "Returning agent #{agent.id} to queue" return_agent agent else @@ -283,7 +287,7 @@ def bridge_agent(agent, queued_call) ignoring_ended_calls do if agent.call.active? logger.info "Caller #{queued_caller_id} failed to connect to Agent #{agent.id} due to caller hangup" - conditionally_return_agent agent + conditionally_return_agent agent, :auto end end From 466dcce170dbc92f3efae635e1c951d0840fd224 Mon Sep 17 00:00:00 2001 From: Lloyd Hughes Date: Tue, 27 Jan 2015 21:21:18 +0200 Subject: [PATCH 115/136] Reduced complexity --- lib/electric_slide/call_queue.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index ac1d214..9c70ca5 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -176,12 +176,10 @@ def connect(agent, queued_call) end end - def conditionally_return_agent(agent, tmp_return_method = nil) - method = tmp_return_method ||= @agent_return_method + def conditionally_return_agent(agent, return_method = @agent_return_method) + raise ArgumentError, "Invalid requeue method; must be one of #{AGENT_RETURN_METHODS.join ','}" unless AGENT_RETURN_METHODS.include? return_method - raise ArgumentError, "Invalid requeue method; must be one of #{AGENT_RETURN_METHODS.join ','}" unless AGENT_RETURN_METHODS.include? method - - if agent && @agents.include?(agent) && agent.presence == :busy && method == :auto + if agent && @agents.include?(agent) && agent.presence == :busy && return_method == :auto logger.info "Returning agent #{agent.id} to queue" return_agent agent else From 9e1bd096c62cdd51f74fe53f35003e40a29b3633 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Tue, 17 Feb 2015 15:34:53 -0500 Subject: [PATCH 116/136] Be more paranoid about preventing exceptions from killing the queue actor The Agent may contain a reference to a call which has since expired. This can raise by the implicit #to_s in the logger statement. --- lib/electric_slide/call_queue.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 9c70ca5..3d02f83 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -115,9 +115,10 @@ def return_agent(agent, status = :available, address = nil) # @param [Agent] agent The {Agent} to be removed from the queue # @return [Agent, Nil] The Agent object if removed, Nil otherwise def remove_agent(agent) - logger.info "Removing agent #{agent} from the queue" @strategy.delete agent @agents.delete agent + logger.info "Removing agent #{agent} from the queue" + rescue Adhearsion::Call::ExpiredError end # Checks to see if any callers are waiting for an agent and attempts to connect them to From f05c484640e00ece4aa4b4cdce68cae785cc496a Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Tue, 17 Feb 2015 18:08:13 -0500 Subject: [PATCH 117/136] Log the correct remote party address Closes #14 --- lib/electric_slide/call_queue.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 3d02f83..0867cbf 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -168,7 +168,8 @@ def abandon(call) # @param [Agent] agent Agent to be connected # @param [Adhearsion::Call] call Caller to be connected def connect(agent, queued_call) - logger.info "Connecting #{agent} with #{queued_call.from}" + remote_party = queued_call.is_a?(Adhearsion::OutboundCall) ? queued_call.to : queued_call.from + logger.info "Connecting #{agent} with #{remote_party}" case @connection_type when :call call_agent agent, queued_call From 62d3607705939a86fc93f65029f2d917e1a85893 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Wed, 18 Feb 2015 08:41:46 -0500 Subject: [PATCH 118/136] #abort only takes a single argument --- lib/electric_slide/call_queue.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 0867cbf..5e7b61d 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -83,9 +83,9 @@ def get_agent(id) def add_agent(agent) case @connection_type when :call - abort ArgumentError, "Agent has no callable address" unless agent.address + abort ArgumentError.new("Agent has no callable address") unless agent.address when :bridge - abort ArgumentError, "Agent has no active call" unless agent.call && agent.call.active? + abort ArgumentError.new("Agent has no active call") unless agent.call && agent.call.active? end logger.info "Adding agent #{agent} to the queue" From b920760c0711f915e668f5d0eb5b7353a247feb9 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Wed, 18 Feb 2015 10:02:01 -0500 Subject: [PATCH 119/136] Prevent a dead call being enqueued from killing the Queue --- lib/electric_slide/call_queue.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 5e7b61d..f03de87 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -150,11 +150,13 @@ def priority_enqueue(call) # Add a call to the end of the queue, the normal FIFO queue behavior # @param [Adhearsion::Call] call Caller to be added to the queue def enqueue(call) - logger.info "Adding call from #{call.from} to the queue" - call[:enqueue_time] = Time.now - @queue << call unless @queue.include? call + ignoring_ended_calls do + logger.info "Adding call from #{call.from} to the queue" + call[:enqueue_time] = Time.now + @queue << call unless @queue.include? call - check_for_connections + check_for_connections + end end # Remove a waiting call from the queue. Used if the caller hangs up or is otherwise removed. From 6290a8857e15b594426dc2c5596dad59a2c2dca4 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Wed, 18 Feb 2015 10:42:16 -0500 Subject: [PATCH 120/136] Make logging the remote party more consistent --- lib/electric_slide/call_queue.rb | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index f03de87..16f98e4 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -151,7 +151,7 @@ def priority_enqueue(call) # @param [Adhearsion::Call] call Caller to be added to the queue def enqueue(call) ignoring_ended_calls do - logger.info "Adding call from #{call.from} to the queue" + logger.info "Adding call from #{remote_party call} to the queue" call[:enqueue_time] = Time.now @queue << call unless @queue.include? call @@ -162,7 +162,7 @@ def enqueue(call) # Remove a waiting call from the queue. Used if the caller hangs up or is otherwise removed. # @param [Adhearsion::Call] call Caller to be removed from the queue def abandon(call) - logger.info "Caller #{call.from} has abandoned the queue" + logger.info "Caller #{remote_party call} has abandoned the queue" @queue.delete call end @@ -170,8 +170,7 @@ def abandon(call) # @param [Agent] agent Agent to be connected # @param [Adhearsion::Call] call Caller to be connected def connect(agent, queued_call) - remote_party = queued_call.is_a?(Adhearsion::OutboundCall) ? queued_call.to : queued_call.from - logger.info "Connecting #{agent} with #{remote_party}" + logger.info "Connecting #{agent} with #{remote_party queued_call}" case @connection_type when :call call_agent agent, queued_call @@ -210,6 +209,14 @@ def calls_waiting end private + # Get the caller ID of the remote party. + # If this is an OutboundCall, use Call#to + # Otherwise, use Call#from + def remote_party(call) + call.is_a?(Adhearsion::OutboundCall) ? call.to : call.from + end + + # @private def ignoring_ended_calls yield @@ -223,7 +230,7 @@ def call_agent(agent, queued_call) agent_call[:queued_call] = queued_call # Stash the caller ID so we don't have to try to get it from a dead call object later - queued_caller_id = queued_call.from + queued_caller_id = remote_party queued_call # The call controller is actually run by #dial, here we skip joining if we do not have one dial_options = agent.dial_options_for(self, queued_call) @@ -268,7 +275,7 @@ def call_agent(agent, queued_call) def bridge_agent(agent, queued_call) # Stash caller ID to make log messages work even if calls end - queued_caller_id = queued_call.from + queued_caller_id = remote_party queued_call agent.call[:queued_call] = queued_call agent.call.register_tmp_handler :event, Punchblock::Event::Unjoined do From 4ab90efbd42221cb234d3a90527140b18d559073 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 23 Feb 2015 14:10:56 -0500 Subject: [PATCH 121/136] Only fire the disconnect callback once per agent The old code would attach the callback each time the agent was given a call --- lib/electric_slide/call_queue.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 16f98e4..fce322b 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -86,6 +86,10 @@ def add_agent(agent) abort ArgumentError.new("Agent has no callable address") unless agent.address when :bridge abort ArgumentError.new("Agent has no active call") unless agent.call && agent.call.active? + unless agent.call[:electric_slide_callback_set] + agent.call.on_end { remove_agent agent } + agent.call[:electric_slide_callback_set] = true + end end logger.info "Adding agent #{agent} to the queue" @@ -285,10 +289,6 @@ def bridge_agent(agent, queued_call) agent.call[:queued_call] = nil end - agent.call.on_end do - remove_agent agent - end - agent.callback :connect, self, agent.call, queued_call agent.join queued_call From 98c544da6d622729401701a531cc4df42c063936 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 5 Mar 2015 11:19:43 -0500 Subject: [PATCH 122/136] Prevent CommandTimeout from killing the Queue actor --- lib/electric_slide/call_queue.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index fce322b..4e91f36 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -292,7 +292,7 @@ def bridge_agent(agent, queued_call) agent.callback :connect, self, agent.call, queued_call agent.join queued_call - rescue Celluloid::DeadActorError, Adhearsion::Call::Hangup, Adhearsion::Call::ExpiredError + rescue Celluloid::DeadActorError, Adhearsion::Call::Hangup, Adhearsion::Call::ExpiredError, Adhearsion::Call::CommandTimeout ignoring_ended_calls do if agent.call.active? logger.info "Caller #{queued_caller_id} failed to connect to Agent #{agent.id} due to caller hangup" From 38bee23367552890f41db1de78818a7c94bcd6f0 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 5 Mar 2015 11:19:56 -0500 Subject: [PATCH 123/136] =?UTF-8?q?If=20an=20agent=E2=80=99s=20call=20is?= =?UTF-8?q?=20not=20active=20when=20in=20bridged=20mode,=20log=20him=20out?= =?UTF-8?q?=20of=20the=20queue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/electric_slide/call_queue.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 4e91f36..7fdeaf2 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -297,6 +297,9 @@ def bridge_agent(agent, queued_call) if agent.call.active? logger.info "Caller #{queued_caller_id} failed to connect to Agent #{agent.id} due to caller hangup" conditionally_return_agent agent, :auto + else + # Agent's call has ended, so remove him from the queue + remove_agent agent end end From d53b479de4a65ec44b2348ae7d2120789d59a5bc Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 5 Mar 2015 12:49:49 -0500 Subject: [PATCH 124/136] Disallow adding a nil agent to the queue --- lib/electric_slide/call_queue.rb | 1 + spec/electric_slide/call_queue_spec.rb | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 7fdeaf2..c112493 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -81,6 +81,7 @@ def get_agent(id) # Registers an agent to the queue # @param [Agent] agent The agent to be added to the queue def add_agent(agent) + abort ArgumentError.new("#add_agent called with nil object") if agent.nil? case @connection_type when :call abort ArgumentError.new("Agent has no callable address") unless agent.address diff --git a/spec/electric_slide/call_queue_spec.rb b/spec/electric_slide/call_queue_spec.rb index f9179e3..8e66ff9 100644 --- a/spec/electric_slide/call_queue_spec.rb +++ b/spec/electric_slide/call_queue_spec.rb @@ -35,4 +35,8 @@ it "should raise when given an invalid connection type" do expect { ElectricSlide::CallQueue.new connection_type: :blah }.to raise_error end + + it "should raise when given an invalid Agent" do + expect { queue.add_agent nil }.to raise_error + end end From f7a71f53e4b5b79f576ce432a60bb4ef577c9307 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Mon, 9 Mar 2015 11:53:35 -0400 Subject: [PATCH 125/136] Try harder to avoid joining dead calls And rescue ProtocolError to support https://github.com/adhearsion/punchblock/commit/2015d69e6926c0e567e24ee32658e458ea74d263 And DRY exception list --- lib/electric_slide/call_queue.rb | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index c112493..afd61b7 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -6,6 +6,13 @@ class ElectricSlide class CallQueue include Celluloid + ENDED_CALL_EXCEPTIONS = [ + Adhearsion::Call::Hangup, + Adhearsion::Call::ExpiredError, + Adhearsion::Call::CommandTimeout, + Celluloid::DeadActorError, + Punchblock::ProtocolError + ] CONNECTION_TYPES = [ :call, @@ -225,7 +232,7 @@ def remote_party(call) # @private def ignoring_ended_calls yield - rescue Celluloid::DeadActorError, Adhearsion::Call::Hangup, Adhearsion::Call::ExpiredError + rescue *ENDED_CALL_EXCEPTIONS # This actor may previously have been shut down due to the call ending end @@ -240,7 +247,7 @@ def call_agent(agent, queued_call) # The call controller is actually run by #dial, here we skip joining if we do not have one dial_options = agent.dial_options_for(self, queued_call) unless dial_options[:confirm] - agent_call.on_answer { ignoring_ended_calls { agent_call.join queued_call.uri } } + agent_call.on_answer { ignoring_ended_calls { agent_call.join queued_call.uri if queued_call.active? } } end # Disconnect agent if caller hangs up before agent answers @@ -292,8 +299,8 @@ def bridge_agent(agent, queued_call) agent.callback :connect, self, agent.call, queued_call - agent.join queued_call - rescue Celluloid::DeadActorError, Adhearsion::Call::Hangup, Adhearsion::Call::ExpiredError, Adhearsion::Call::CommandTimeout + agent.join queued_call if queued_call.active? + rescue *ENDED_CALL_EXCEPTIONS ignoring_ended_calls do if agent.call.active? logger.info "Caller #{queued_caller_id} failed to connect to Agent #{agent.id} due to caller hangup" From 67c7a11babc6d0deb22dd30106d63cfe3595d692 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Wed, 11 Mar 2015 09:23:40 -0400 Subject: [PATCH 126/136] `call.active?` can context switch the queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …making this unsafe. Instead, rely on the fail-safe in the connection mechanism to return the caller to the queue if the agent call is inactive --- lib/electric_slide/call_queue.rb | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index afd61b7..863f8db 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -136,16 +136,7 @@ def remove_agent(agent) # Checks to see if any callers are waiting for an agent and attempts to connect them to # an available agent def check_for_connections - while call_waiting? && agent_available? - call = get_next_caller - begin - next unless call.active? - rescue Adhearsion::Call::ExpiredError - next - end - connect checkout_agent, call - break - end + connect checkout_agent, get_next_caller while call_waiting? && agent_available? end # Add a call to the head of the queue. Among other reasons, this is used when a caller is sent From b0292c939493236693234cc5a1515ae37094e6b3 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 12 Mar 2015 02:50:51 -0400 Subject: [PATCH 127/136] Be more careful about dead calls in #connect With the previous change to loop over calls in the queue being made smaller to ensure atomicity, dead calls are leaking into #connect. This adds checks to prevent against dead calls from killing the queue actor --- lib/electric_slide/call_queue.rb | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 863f8db..4f1361f 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -165,7 +165,7 @@ def enqueue(call) # Remove a waiting call from the queue. Used if the caller hangs up or is otherwise removed. # @param [Adhearsion::Call] call Caller to be removed from the queue def abandon(call) - logger.info "Caller #{remote_party call} has abandoned the queue" + ignoring_ended_calls { logger.info "Caller #{remote_party call} has abandoned the queue" } @queue.delete call end @@ -173,13 +173,35 @@ def abandon(call) # @param [Agent] agent Agent to be connected # @param [Adhearsion::Call] call Caller to be connected def connect(agent, queued_call) + unless queued_call.active? + logger.warn "Inactive queued call found in #connect" + return_agent agent + end + logger.info "Connecting #{agent} with #{remote_party queued_call}" case @connection_type when :call call_agent agent, queued_call when :bridge + unless agent.call.active? + logger.warn "Inactive agent call found in #connect, returning caller to queue" + priority_enqueue queued_call + end bridge_agent agent, queued_call end + rescue *ENDED_CALL_EXCEPTIONS + ignoring_ended_calls do + if queued_call.active? + logger.warn "Dead call exception in #connect but queued_call still alive, reinserting into queue" + priority_enqueue queued_call + end + end + ignoring_ended_calls do + if agent.call && agent.call.active? + logger.warn "Dead call exception in #connect but agent call still alive, reinserting into queue" + add_agent queued_call + end + end end def conditionally_return_agent(agent, return_method = @agent_return_method) From d56411de1b768220e50047c82926476197aabe06 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 12 Mar 2015 03:31:10 -0400 Subject: [PATCH 128/136] Use correct method to return an agent to the queue --- lib/electric_slide/call_queue.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 4f1361f..422e0b1 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -199,7 +199,7 @@ def connect(agent, queued_call) ignoring_ended_calls do if agent.call && agent.call.active? logger.warn "Dead call exception in #connect but agent call still alive, reinserting into queue" - add_agent queued_call + return_agent queued_call end end end From 053119b9c7eed7a70381c423da39c20ba1dad737 Mon Sep 17 00:00:00 2001 From: Lloyd Hughes Date: Tue, 17 Mar 2015 21:24:50 +0200 Subject: [PATCH 129/136] Fixed issue of agents not being returned to queue Copy and paste bug I assume. Returning the hungup call to the queue instead of the agent. --- lib/electric_slide/call_queue.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/call_queue.rb b/lib/electric_slide/call_queue.rb index 422e0b1..194d55e 100644 --- a/lib/electric_slide/call_queue.rb +++ b/lib/electric_slide/call_queue.rb @@ -199,7 +199,7 @@ def connect(agent, queued_call) ignoring_ended_calls do if agent.call && agent.call.active? logger.warn "Dead call exception in #connect but agent call still alive, reinserting into queue" - return_agent queued_call + return_agent agent end end end From e190ae8caa6f73969c9a068bdc8bb66492d553bb Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 7 May 2015 16:45:03 -0400 Subject: [PATCH 130/136] Improve docs on MoH handling --- README.markdown | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.markdown b/README.markdown index e2315bf..5bd6079 100644 --- a/README.markdown +++ b/README.markdown @@ -42,9 +42,7 @@ class EnterTheQueue < Adhearsion::CallController answer # Play music-on-hold to the caller until joined to an agent - # TODO: Create an ElectricSlide helper to wrap up this function - # with optional looping of playback - player = play 'http://moh-server.example.com/stream.mp3' + player = play 'http://moh-server.example.com/stream.mp3', repeat_times: 0 call.on_joined do player.stop! end From beb605b6bafe5bb6028beabc612a7f5eff22cb5f Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 7 May 2015 16:45:39 -0400 Subject: [PATCH 131/136] Make warning more obvious --- README.markdown | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.markdown b/README.markdown index 5bd6079..b76868f 100644 --- a/README.markdown +++ b/README.markdown @@ -18,14 +18,17 @@ TODO: * Example for using Matrioska to offer Agents and Callers interactivity while waiting * How to handle MOH +## WARNING! + +While you can have ElectricSlide keep track of custom queues, it is recommended to use the built-in CallQueue object. + +The authors of ElectricSlide recommend NOT to subclass, monkeypatch, or otherwise alter the CallQueue implementation, as the likelihood of creating subtle race conditions is high. + Example Queue ------------- ```ruby my_queue = ElectricSlide.create :my_queue, ElectricSlide::CallQueue -# While you can have ElectricSlide keep track of custom queues, it is recommended to use the built-in CallQueue object -# NOTE! The authors of ElectricSlide recommend NOT to subclass, monkeypatch, or otherwise alter the CallQueue implementation, as -# the likelihood of creating race conditions is high. # Another way to get a handle on a queue ElectricSlide.create :my_queue From cd9d45ebe0422a24e0aead2b23bd2c835e3df138 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Thu, 7 May 2015 16:46:23 -0400 Subject: [PATCH 132/136] Make `create_queue` behavior match README --- lib/electric_slide.rb | 18 +++++++++++------- spec/electric_slide_spec.rb | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/electric_slide.rb b/lib/electric_slide.rb index 5f2286f..609a587 100644 --- a/lib/electric_slide.rb +++ b/lib/electric_slide.rb @@ -15,16 +15,20 @@ def initialize end def create(name, queue_class = nil, *args) - if @queues.key?(name) - fail "Queue with name #{name} already exists!" - else - queue_class ||= CallQueue - @queues[name] = queue_class.work *args - end + fail "Queue with name #{name} already exists!" if @queues.key? name + + queue_class ||= CallQueue + @queues[name] = queue_class.work *args + # Return the queue instance or current actor + get_queue name end - def get_queue(name) + def get_queue!(name) fail "Queue #{name} not found!" unless @queues.key?(name) + get_queue name + end + + def get_queue(name) queue = @queues[name] if queue.respond_to? :actors # In case we have a Celluloid supervision group, get the current actor diff --git a/spec/electric_slide_spec.rb b/spec/electric_slide_spec.rb index 5924657..9d2659a 100644 --- a/spec/electric_slide_spec.rb +++ b/spec/electric_slide_spec.rb @@ -34,7 +34,7 @@ end it "should raise if attempting to work with a queue that doesn't exist" do - expect { ElectricSlide.get_queue("does not exist!") }.to raise_error + expect { ElectricSlide.get_queue!("does not exist!") }.to raise_error expect { ElectricSlide.shutdown_queue("does not exist!") }.to raise_error end From ea25971142c2cede6abfa6738aded2a41ca3088e Mon Sep 17 00:00:00 2001 From: Luca Pradovera Date: Mon, 20 Jul 2015 14:47:06 +0200 Subject: [PATCH 133/136] Agent callbacks should be executed on the instance --- lib/electric_slide/agent.rb | 2 +- spec/electric_slide/agent_spec.rb | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 spec/electric_slide/agent_spec.rb diff --git a/lib/electric_slide/agent.rb b/lib/electric_slide/agent.rb index 2aca35b..b5778bb 100644 --- a/lib/electric_slide/agent.rb +++ b/lib/electric_slide/agent.rb @@ -14,7 +14,7 @@ def initialize(opts = {}) def callback(type, *args) callback = self.class.instance_variable_get "@#{type}_callback" - callback.call(*args) if callback && callback.respond_to?(:call) + instance_exec *args, &callback if callback && callback.respond_to?(:call) end diff --git a/spec/electric_slide/agent_spec.rb b/spec/electric_slide/agent_spec.rb new file mode 100644 index 0000000..07d8313 --- /dev/null +++ b/spec/electric_slide/agent_spec.rb @@ -0,0 +1,23 @@ +# encoding: utf-8 +require 'spec_helper' +require 'electric_slide/agent' + +describe ElectricSlide::Agent do + let(:options) { { id: 1, address: '123@foo.com', presence: :available} } + + class MyAgent < ElectricSlide::Agent + on_connect do + foo + end + + def foo + :bar + end + end + + subject {MyAgent.new options} + + it 'executes a connect callback' do + expect(subject.callback(:connect)).to eql :bar + end +end From e4bd696f0cb48a55a93c98c3362143cc9baa3a6e Mon Sep 17 00:00:00 2001 From: Ben Langfeld Date: Thu, 23 Jul 2015 13:49:13 -0300 Subject: [PATCH 134/136] Avoid NoMethodError when someone hangs up When we handle a hangup, we also deal with the Agent's call. This handling was added specifically for the bridge strategy, but this strategy depends on several items implemented in an application outside of ES, and is incomplete. It does, however, introduce the requirement that `ElectricSlide::Agent#call` exist, but not that it ever take a value. This is sufficient to get outbound calling to agents working, but does not complete the bridge strategy, which should be completed in ES with a re-evaluation of why tests pass with such bugs present. --- lib/electric_slide/agent.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/agent.rb b/lib/electric_slide/agent.rb index b5778bb..5e65c1e 100644 --- a/lib/electric_slide/agent.rb +++ b/lib/electric_slide/agent.rb @@ -1,6 +1,6 @@ # encoding: utf-8 class ElectricSlide::Agent - attr_accessor :id, :address, :presence, :connect_callback, :disconnect_callback + attr_accessor :id, :address, :presence, :call, :connect_callback, :disconnect_callback # @param [Hash] opts Agent parameters # @option opts [String] :id The Agent's ID From 5115273c4295529af1ba802a1113f6e6b4a0eb33 Mon Sep 17 00:00:00 2001 From: Ben Langfeld Date: Thu, 23 Jul 2015 18:54:02 -0300 Subject: [PATCH 135/136] Avoid confusion around dead calls Backport https://github.com/adhearsion/adhearsion/commit/8c6855612c70dd822fb4e4c2006d1fdc9d05fe23 --- lib/electric_slide.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/electric_slide.rb b/lib/electric_slide.rb index 609a587..766985c 100644 --- a/lib/electric_slide.rb +++ b/lib/electric_slide.rb @@ -1,6 +1,21 @@ # encoding: utf-8 require 'celluloid' require 'singleton' + +require 'adhearsion/version' + +if Gem::Version.new(Adhearsion::VERSION) < Gem::Version.new('3.0.0') + # Backport https://github.com/adhearsion/adhearsion/commit/8c6855612c70dd822fb4e4c2006d1fdc9d05fe23 to avoid confusion around dead calls + require 'adhearsion/call' + class Adhearsion::Call::ActorProxy < Celluloid::ActorProxy + def active? + alive? && super + rescue Adhearsion::Call::ExpiredError + false + end + end +end + %w( call_queue plugin From f7d3f51efa4ecdecc9d9ce6d3b5ebd1c3f7b125d Mon Sep 17 00:00:00 2001 From: Ben Langfeld Date: Thu, 23 Jul 2015 23:18:16 -0300 Subject: [PATCH 136/136] Bump to 0.2.0 --- lib/electric_slide/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electric_slide/version.rb b/lib/electric_slide/version.rb index d630129..1427afd 100644 --- a/lib/electric_slide/version.rb +++ b/lib/electric_slide/version.rb @@ -1,4 +1,4 @@ # encoding: utf-8 class ElectricSlide - VERSION = '0.1.0' + VERSION = '0.2.0' end