diff --git a/app/jobs/base_job.rb b/app/jobs/base_job.rb index bbfcc786a..d73587930 100644 --- a/app/jobs/base_job.rb +++ b/app/jobs/base_job.rb @@ -22,9 +22,11 @@ def scheduler_options end def call - after_start - execute - before_finish + logger.tagged(self.class.name) do + after_start + execute + before_finish + end end def execute diff --git a/app/jobs/jobs/calls_monitoring.rb b/app/jobs/jobs/calls_monitoring.rb index ed1c197b7..f481eef3c 100644 --- a/app/jobs/jobs/calls_monitoring.rb +++ b/app/jobs/jobs/calls_monitoring.rb @@ -147,13 +147,43 @@ def after_start end def execute - detect_customers_calls_to_reject - detect_customers_auth_calls_to_reject - detect_vendors_calls_to_reject - detect_gateway_calls_to_reject - detect_random_calls_to_reject + log_time('detect_customers_calls_to_reject') do + detect_customers_calls_to_reject + end + + log_time('detect_customers_auth_calls_to_reject') do + detect_customers_auth_calls_to_reject + end + + log_time('detect_vendors_calls_to_reject') do + detect_vendors_calls_to_reject + end + + log_time('detect_gateway_calls_to_reject') do + detect_gateway_calls_to_reject + end + + log_time('detect_random_calls_to_reject') do + detect_random_calls_to_reject + end end + def before_finish + log_time('save_stats') do + save_stats + end + + log_time('send_prometheus_metrics') do + send_prometheus_metrics + end + + log_time('terminate_calls!') do + terminate_calls! + end + end + + private + # random_disconnect_enable | f # random_disconnect_length | 7000 def detect_random_calls_to_reject @@ -172,9 +202,7 @@ def detect_customers_calls_to_reject account = active_customers_balances[acc_id] if account - call_collection = CallCollection.new(calls, - key: :destination, - account: account) + call_collection = CallCollection.new(calls, key: :destination, account: account) if call_collection.exceed_min_balance? @terminate_calls.merge!( @@ -254,6 +282,87 @@ def detect_gateway_calls_to_reject end end + def send_prometheus_metrics + return unless PrometheusConfig.enabled? + + metrics = [] + + total = active_calls.values.sum(&:count) + metrics << ActiveCallsProcessor.collect(total: total) + + customers_active_calls.each do |account_id, calls| + account_external_id = calls.first[:customer_acc_external_id] + collection = CallCollection.new(calls, key: :destination, account: []) + src_prefixes = calls.map { |c| c[:src_prefix_routing] } + dst_prefixes = calls.map { |c| c[:dst_prefix_routing] } + + metrics << ActiveCallsProcessor.collect( + account_originated: calls.count, + account_originated_unique_src: src_prefixes.uniq.count, + account_originated_unique_dst: dst_prefixes.uniq.count, + account_price_originated: collection.total_calls_cost, + labels: { account_external_id: account_external_id, account_id: account_id } + ) + end + + vendors_active_calls.each do |account_id, calls| + account_external_id = calls.first[:vendor_acc_external_id] + collection = CallCollection.new(calls, key: :dialpeer, account: []) + + metrics << ActiveCallsProcessor.collect( + account_terminated: calls.count, + account_price_terminated: collection.total_calls_cost, + labels: { account_external_id: account_external_id, account_id: account_id } + ) + end + + client = PrometheusExporter::Client.default + metrics.each { |metric| client.send_json(metric) } + end + + def save_stats + Stats::ActiveCall.transaction do + ActiveCalls::CreateStats.call( + calls: active_calls, + current_time: now + ) + + if YetiConfig.calls_monitoring.write_account_stats + ActiveCalls::CreateAccountStats.call( + customer_calls: customers_active_calls, + vendor_calls: vendors_active_calls, + current_time: now + ) + end + if YetiConfig.calls_monitoring.write_gateway_stats + ActiveCalls::CreateOriginationGatewayStats.call( + calls: flatten_calls.group_by { |c| c[:orig_gw_id] }, + current_time: now + ) + ActiveCalls::CreateTerminationGatewayStats.call( + calls: flatten_calls.group_by { |c| c[:term_gw_id] }, + current_time: now + ) + end + end + end + + def terminate_calls! + logger.info { "Going to terminate #{@terminate_calls.keys.size} call(s)." } + nodes = Node.all.index_by(&:id) + @terminate_calls.each do |local_tag, call| + logger.warn { "Terminate call Node: #{call[:node_id]}, local_tag :#{local_tag}" } + begin + node_id = call[:node_id].to_i + nodes[node_id].drop_call(local_tag) + rescue StandardError => e + node_id = call.is_a?(Hash) ? call[:node_id] : nil + capture_error(e, extra: { local_tag: local_tag, node_id: node_id }) + logger.error "#{e.class} #{e.message}" + end + end + end + def flatten_calls @flatten_calls ||= active_calls.values.flatten end @@ -336,82 +445,10 @@ def active_calls end end - def before_finish - save_stats - send_prometheus_metrics - terminate_calls! - end - - private - - def send_prometheus_metrics - return unless PrometheusConfig.enabled? - - metrics = [] - - total = active_calls.values.sum(&:count) - metrics << ActiveCallsProcessor.collect(total: total) - - customers_active_calls.each do |account_id, calls| - account_external_id = calls.first[:customer_acc_external_id] - collection = CallCollection.new(calls, key: :destination, account: []) - src_prefixes = calls.map { |c| c[:src_prefix_routing] } - dst_prefixes = calls.map { |c| c[:dst_prefix_routing] } - - metrics << ActiveCallsProcessor.collect( - account_originated: calls.count, - account_originated_unique_src: src_prefixes.uniq.count, - account_originated_unique_dst: dst_prefixes.uniq.count, - account_price_originated: collection.total_calls_cost, - labels: { account_external_id: account_external_id, account_id: account_id } - ) - end - - vendors_active_calls.each do |account_id, calls| - account_external_id = calls.first[:vendor_acc_external_id] - collection = CallCollection.new(calls, key: :dialpeer, account: []) - - metrics << ActiveCallsProcessor.collect( - account_terminated: calls.count, - account_price_terminated: collection.total_calls_cost, - labels: { account_external_id: account_external_id, account_id: account_id } - ) - end - - client = PrometheusExporter::Client.default - metrics.each { |metric| client.send_json(metric) } - end - - def save_stats - Stats::ActiveCall.transaction do - Stats::ActiveCall.create_stats(active_calls, now) - if YetiConfig.calls_monitoring.write_account_stats - ActiveCalls::CreateAccountStats.call( - customer_calls: customers_active_calls, - vendor_calls: vendors_active_calls, - current_time: now - ) - end - orig_gw_grouped_calls = flatten_calls.group_by { |c| c[:orig_gw_id] } - Stats::ActiveCallOrigGateway.create_stats(orig_gw_grouped_calls, now) - term_gw_grouped_calls = flatten_calls.group_by { |c| c[:term_gw_id] } - Stats::ActiveCallTermGateway.create_stats(term_gw_grouped_calls, now) - end - end - - def terminate_calls! - nodes = Node.all.index_by(&:id) - @terminate_calls.each do |local_tag, call| - logger.warn { "CallsMonitoring#terminate_calls! Node #{call[:node_id]}, local_tag :#{local_tag}" } - begin - node_id = call[:node_id].to_i - nodes[node_id].drop_call(local_tag) - rescue StandardError => e - node_id = call.is_a?(Hash) ? call[:node_id] : nil - capture_error(e, extra: { local_tag: local_tag, node_id: node_id }) - logger.error e.message - end - end + def log_time(name, &block) + logger.info { "Operation #{name} started." } + seconds = logger.tagged(name) { ::Benchmark.realtime(&block) } + logger.info { format("Operation #{name} finished %.6f sec.", seconds) } end end end diff --git a/app/models/concerns/chart.rb b/app/models/concerns/chart.rb index a89dc6d77..aa455d7ed 100644 --- a/app/models/concerns/chart.rb +++ b/app/models/concerns/chart.rb @@ -14,10 +14,6 @@ module Chart end class_methods do - def create_stats(calls = {}, now_time) - super calls, now_time, chart_entity_klass.all, chart_entity_column - end - def to_chart(id, options = {}) time_column = options.delete(:time_column) || :created_at count_column = options.delete(:count_column) || :count diff --git a/app/services/active_calls/create_origination_gateway_stats.rb b/app/services/active_calls/create_origination_gateway_stats.rb new file mode 100644 index 000000000..b37715841 --- /dev/null +++ b/app/services/active_calls/create_origination_gateway_stats.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module ActiveCalls + class CreateOriginationGatewayStats < ApplicationService + parameter :calls, required: true + parameter :current_time, required: true + + def call + attrs_list = build_calls_attrs_list + missing_gateway_ids = Gateway.where.not(id: calls.keys).pluck(:id) + attrs_list.concat build_empty_attrs_list(missing_gateway_ids) + return if attrs_list.empty? + + Stats::ActiveCallOrigGateway.insert_all!(attrs_list) + end + + private + + def build_calls_attrs_list + calls.map do |gateway_id, sub_calls| + { + count: sub_calls.count, + created_at: current_time, + gateway_id: gateway_id + } + end + end + + def build_empty_attrs_list(gateway_ids) + gateway_ids.map do |gateway_id| + { + count: 0, + created_at: current_time, + gateway_id: gateway_id + } + end + end + end +end diff --git a/app/services/active_calls/create_stats.rb b/app/services/active_calls/create_stats.rb new file mode 100644 index 000000000..7ebab7f79 --- /dev/null +++ b/app/services/active_calls/create_stats.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module ActiveCalls + class CreateStats < ApplicationService + parameter :calls, required: true + parameter :current_time, required: true + + def call + attrs_list = build_calls_attrs_list + missing_node_ids = Node.where.not(id: calls.keys).pluck(:id) + attrs_list.concat build_empty_attrs_list(missing_node_ids) + return if attrs_list.empty? + + Stats::ActiveCall.insert_all!(attrs_list) + end + + private + + def build_calls_attrs_list + calls.map do |node_id, sub_calls| + { + count: sub_calls.count, + created_at: current_time, + node_id: node_id + } + end + end + + def build_empty_attrs_list(node_ids) + node_ids.map do |node_id| + { + count: 0, + created_at: current_time, + node_id: node_id + } + end + end + end +end diff --git a/app/services/active_calls/create_termination_gateway_stats.rb b/app/services/active_calls/create_termination_gateway_stats.rb new file mode 100644 index 000000000..9803d2d1a --- /dev/null +++ b/app/services/active_calls/create_termination_gateway_stats.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module ActiveCalls + class CreateTerminationGatewayStats < ApplicationService + parameter :calls, required: true + parameter :current_time, required: true + + def call + attrs_list = build_calls_attrs_list + missing_gateway_ids = Gateway.where.not(id: calls.keys).pluck(:id) + attrs_list.concat build_empty_attrs_list(missing_gateway_ids) + return if attrs_list.empty? + + Stats::ActiveCallTermGateway.insert_all!(attrs_list) + end + + private + + def build_calls_attrs_list + calls.map do |gateway_id, sub_calls| + { + count: sub_calls.count, + created_at: current_time, + gateway_id: gateway_id + } + end + end + + def build_empty_attrs_list(gateway_ids) + gateway_ids.map do |gateway_id| + { + count: 0, + created_at: current_time, + gateway_id: gateway_id + } + end + end + end +end diff --git a/config/initializers/_config.rb b/config/initializers/_config.rb index 4181f4d54..3ae6c49b7 100644 --- a/config/initializers/_config.rb +++ b/config/initializers/_config.rb @@ -24,6 +24,7 @@ def self.setting_files(config_root, _env) required(:calls_monitoring).schema do required(:write_account_stats).value(:bool?) + required(:write_gateway_stats).value(:bool?) end required(:api).schema do diff --git a/config/yeti_web.yml.ci b/config/yeti_web.yml.ci index bb625f5e8..e5b15b7f8 100644 --- a/config/yeti_web.yml.ci +++ b/config/yeti_web.yml.ci @@ -2,6 +2,7 @@ site_title: "Yeti Admin" site_title_image: "yeti.png" calls_monitoring: write_account_stats: true + write_gateway_stats: true api: token_lifetime: 600 # jwt token lifetime in seconds, empty string means permanent tokens cdr_export: diff --git a/config/yeti_web.yml.development b/config/yeti_web.yml.development index bb625f5e8..e5b15b7f8 100644 --- a/config/yeti_web.yml.development +++ b/config/yeti_web.yml.development @@ -2,6 +2,7 @@ site_title: "Yeti Admin" site_title_image: "yeti.png" calls_monitoring: write_account_stats: true + write_gateway_stats: true api: token_lifetime: 600 # jwt token lifetime in seconds, empty string means permanent tokens cdr_export: diff --git a/config/yeti_web.yml.distr b/config/yeti_web.yml.distr index 44d07013b..075728574 100644 --- a/config/yeti_web.yml.distr +++ b/config/yeti_web.yml.distr @@ -2,6 +2,7 @@ site_title: "Yeti Admin" site_title_image: "yeti.png" calls_monitoring: write_account_stats: true + write_gateway_stats: true api: token_lifetime: 600 # jwt token lifetime in seconds, empty string means permanent tokens cdr_export: diff --git a/spec/config/yeti_web_spec.rb b/spec/config/yeti_web_spec.rb index 4abced915..e997904fd 100644 --- a/spec/config/yeti_web_spec.rb +++ b/spec/config/yeti_web_spec.rb @@ -10,7 +10,8 @@ site_title: be_kind_of(String), site_title_image: be_kind_of(String), calls_monitoring: { - write_account_stats: be_one_of(true, false) + write_account_stats: be_one_of(true, false), + write_gateway_stats: be_one_of(true, false) }, api: { token_lifetime: be_kind_of(Integer) diff --git a/spec/jobs/jobs/calls_monitoring_spec.rb b/spec/jobs/jobs/calls_monitoring_spec.rb index a7f83986d..2db09acee 100644 --- a/spec/jobs/jobs/calls_monitoring_spec.rb +++ b/spec/jobs/jobs/calls_monitoring_spec.rb @@ -238,6 +238,59 @@ end end + context 'when YetiConfig.calls_monitoring.write_gateway_stats=true' do + before do + expect(YetiConfig.calls_monitoring).to receive(:write_gateway_stats).and_return(true) + end + + it 'creates Stats::ActiveCallOrigGateway and Stats::ActiveCallTermGateway' do + expect(ActiveCalls::CreateOriginationGatewayStats).to receive(:call).with( + calls: be_present, + current_time: be_within(2).of(Time.now) + ).and_call_original + + expect(ActiveCalls::CreateTerminationGatewayStats).to receive(:call).with( + calls: be_present, + current_time: be_within(2).of(Time.now) + ).and_call_original + + expect { subject }.to change { Stats::ActiveCallOrigGateway.count }.by(2).and( + change { Stats::ActiveCallTermGateway.count }.by(2) + ) + + orig_gw1_stats = Stats::ActiveCallOrigGateway.where(gateway_id: origin_gateway.id).to_a + expect(orig_gw1_stats.size).to eq 1 + expect(orig_gw1_stats.first).to have_attributes(count: 2) + + orig_gw2_stats = Stats::ActiveCallOrigGateway.where(gateway_id: term_gateway.id).to_a + expect(orig_gw2_stats.size).to eq 1 + expect(orig_gw2_stats.first).to have_attributes(count: 0) + + term_gw1_stats = Stats::ActiveCallTermGateway.where(gateway_id: term_gateway.id).to_a + expect(term_gw1_stats.size).to eq 1 + expect(term_gw1_stats.first).to have_attributes(count: 2) + + term_gw2_stats = Stats::ActiveCallTermGateway.where(gateway_id: origin_gateway.id).to_a + expect(term_gw2_stats.size).to eq 1 + expect(term_gw2_stats.first).to have_attributes(count: 0) + end + end + + context 'when YetiConfig.calls_monitoring.write_gateway_stats=false' do + before do + expect(YetiConfig.calls_monitoring).to receive(:write_gateway_stats).and_return(false) + end + + it 'does not create Stats::ActiveCallOrigGateway and Stats::ActiveCallTermGateway' do + expect(ActiveCalls::CreateOriginationGatewayStats).not_to receive(:call) + expect(ActiveCalls::CreateTerminationGatewayStats).not_to receive(:call) + + expect { subject }.to change { Stats::ActiveCallOrigGateway.count }.by(0).and( + change { Stats::ActiveCallTermGateway.count }.by(0) + ) + end + end + context 'when Customer and Vendor have enough money' do it 'does not send prometheus metrics' do expect { subject }.to send_prometheus_metrics.exactly(0) diff --git a/spec/services/active_calls/create_origination_gateway_stats_spec.rb b/spec/services/active_calls/create_origination_gateway_stats_spec.rb new file mode 100644 index 000000000..742ea3eec --- /dev/null +++ b/spec/services/active_calls/create_origination_gateway_stats_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +RSpec.describe ActiveCalls::CreateOriginationGatewayStats, '.call' do + subject do + described_class.call(service_params) + end + + let(:service_params) do + { + calls: calls, + current_time: 1.minute.ago + } + end + let!(:gateways) do + FactoryBot.create_list(:gateway, 5) + end + + context 'without calls' do + let!(:calls) { {} } + + it 'creates correct stats' do + expect { subject }.to change { Stats::ActiveCallOrigGateway.count }.by(gateways.size) + + gateways.each do |gateway| + stats = Stats::ActiveCallOrigGateway.where(gateway_id: gateway.id).to_a + expect(stats.size).to eq(1) + expect(stats.first).to have_attributes( + count: 0, + created_at: be_within(1).of(service_params[:current_time]) + ) + end + end + end + + context 'with calls' do + let(:calls) do + { + gateways.first.id.to_s => [double, double], + gateways.second.id.to_s => [double], + gateways.third.id.to_s => [double, double, double, double] + } + end + + it 'creates correct stats' do + expect { subject }.to change { Stats::ActiveCallOrigGateway.count }.by(gateways.size) + + gateway1_stats = Stats::ActiveCallOrigGateway.where(gateway_id: gateways.first.id).to_a + expect(gateway1_stats.size).to eq(1) + expect(gateway1_stats.first).to have_attributes( + count: 2, + created_at: be_within(1).of(service_params[:current_time]) + ) + + gateway2_stats = Stats::ActiveCallOrigGateway.where(gateway_id: gateways.second.id).to_a + expect(gateway2_stats.size).to eq(1) + expect(gateway2_stats.first).to have_attributes( + count: 1, + created_at: be_within(1).of(service_params[:current_time]) + ) + + gateway3_stats = Stats::ActiveCallOrigGateway.where(gateway_id: gateways.third.id).to_a + expect(gateway3_stats.size).to eq(1) + expect(gateway3_stats.first).to have_attributes( + count: 4, + created_at: be_within(1).of(service_params[:current_time]) + ) + + other_gateways = gateways - [gateways.first, gateways.second, gateways.third] + other_gateways.each do |gateway| + stats = Stats::ActiveCallOrigGateway.where(gateway_id: gateway.id).to_a + expect(stats.size).to eq(1) + expect(stats.first).to have_attributes( + count: 0, + created_at: be_within(1).of(service_params[:current_time]) + ) + end + end + end + + context 'without gateways' do + let!(:calls) { {} } + let!(:gateways) { nil } + + it 'does not create any stats' do + expect { subject }.to change { Stats::ActiveCallOrigGateway.count }.by(0) + end + end +end diff --git a/spec/services/active_calls/create_stats_spec.rb b/spec/services/active_calls/create_stats_spec.rb new file mode 100644 index 000000000..7928e1457 --- /dev/null +++ b/spec/services/active_calls/create_stats_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +RSpec.describe ActiveCalls::CreateStats, '.call' do + subject do + described_class.call(service_params) + end + + let(:service_params) do + { + calls: calls, + current_time: 1.minute.ago + } + end + let!(:nodes) do + FactoryBot.create_list(:node, 5) + end + + context 'without calls' do + let!(:calls) { {} } + + it 'creates correct stats' do + expect { subject }.to change { Stats::ActiveCall.count }.by(nodes.size) + + nodes.each do |node| + stats = Stats::ActiveCall.where(node_id: node.id).to_a + expect(stats.size).to eq(1) + expect(stats.first).to have_attributes( + count: 0, + created_at: be_within(1).of(service_params[:current_time]) + ) + end + end + end + + context 'with calls' do + let(:calls) do + { + nodes.first.id.to_s => [double, double], + nodes.second.id.to_s => [double], + nodes.third.id.to_s => [double, double, double, double] + } + end + + it 'creates correct stats' do + expect { subject }.to change { Stats::ActiveCall.count }.by(nodes.size) + + node1_stats = Stats::ActiveCall.where(node_id: nodes.first.id).to_a + expect(node1_stats.size).to eq(1) + expect(node1_stats.first).to have_attributes( + count: 2, + created_at: be_within(1).of(service_params[:current_time]) + ) + + node2_stats = Stats::ActiveCall.where(node_id: nodes.second.id).to_a + expect(node2_stats.size).to eq(1) + expect(node2_stats.first).to have_attributes( + count: 1, + created_at: be_within(1).of(service_params[:current_time]) + ) + + node3_stats = Stats::ActiveCall.where(node_id: nodes.third.id).to_a + expect(node3_stats.size).to eq(1) + expect(node3_stats.first).to have_attributes( + count: 4, + created_at: be_within(1).of(service_params[:current_time]) + ) + + other_nodes = nodes - [nodes.first, nodes.second, nodes.third] + other_nodes.each do |node| + stats = Stats::ActiveCall.where(node_id: node.id).to_a + expect(stats.size).to eq(1) + expect(stats.first).to have_attributes( + count: 0, + created_at: be_within(1).of(service_params[:current_time]) + ) + end + end + end + + context 'without nodes' do + let!(:calls) { {} } + let!(:nodes) { nil } + + it 'does not create any stats' do + expect { subject }.to change { Stats::ActiveCall.count }.by(0) + end + end +end diff --git a/spec/services/active_calls/create_termination_gateway_stats_spec.rb b/spec/services/active_calls/create_termination_gateway_stats_spec.rb new file mode 100644 index 000000000..04b76f3e2 --- /dev/null +++ b/spec/services/active_calls/create_termination_gateway_stats_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +RSpec.describe ActiveCalls::CreateTerminationGatewayStats, '.call' do + subject do + described_class.call(service_params) + end + + let(:service_params) do + { + calls: calls, + current_time: 1.minute.ago + } + end + let!(:gateways) do + FactoryBot.create_list(:gateway, 5) + end + + context 'without calls' do + let!(:calls) { {} } + + it 'creates correct stats' do + expect { subject }.to change { Stats::ActiveCallTermGateway.count }.by(gateways.size) + + gateways.each do |gateway| + stats = Stats::ActiveCallTermGateway.where(gateway_id: gateway.id).to_a + expect(stats.size).to eq(1) + expect(stats.first).to have_attributes( + count: 0, + created_at: be_within(1).of(service_params[:current_time]) + ) + end + end + end + + context 'with calls' do + let(:calls) do + { + gateways.first.id.to_s => [double, double], + gateways.second.id.to_s => [double], + gateways.third.id.to_s => [double, double, double, double] + } + end + + it 'creates correct stats' do + expect { subject }.to change { Stats::ActiveCallTermGateway.count }.by(gateways.size) + + gateway1_stats = Stats::ActiveCallTermGateway.where(gateway_id: gateways.first.id).to_a + expect(gateway1_stats.size).to eq(1) + expect(gateway1_stats.first).to have_attributes( + count: 2, + created_at: be_within(1).of(service_params[:current_time]) + ) + + gateway2_stats = Stats::ActiveCallTermGateway.where(gateway_id: gateways.second.id).to_a + expect(gateway2_stats.size).to eq(1) + expect(gateway2_stats.first).to have_attributes( + count: 1, + created_at: be_within(1).of(service_params[:current_time]) + ) + + gateway3_stats = Stats::ActiveCallTermGateway.where(gateway_id: gateways.third.id).to_a + expect(gateway3_stats.size).to eq(1) + expect(gateway3_stats.first).to have_attributes( + count: 4, + created_at: be_within(1).of(service_params[:current_time]) + ) + + other_gateways = gateways - [gateways.first, gateways.second, gateways.third] + other_gateways.each do |gateway| + stats = Stats::ActiveCallTermGateway.where(gateway_id: gateway.id).to_a + expect(stats.size).to eq(1) + expect(stats.first).to have_attributes( + count: 0, + created_at: be_within(1).of(service_params[:current_time]) + ) + end + end + end + + context 'without gateways' do + let!(:calls) { {} } + let!(:gateways) { nil } + + it 'does not create any stats' do + expect { subject }.to change { Stats::ActiveCallTermGateway.count }.by(0) + end + end +end