Skip to content
Merged
1 change: 1 addition & 0 deletions app/api/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
require_relative 'internal/transactions'
require_relative 'internal/utilization'
require_relative 'internal/service_tokens'
require_relative 'internal/stats'
25 changes: 25 additions & 0 deletions app/api/internal/stats.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module ThreeScale
module Backend
module API
internal_api '/services/:service_id/stats' do
before do
respond_with_404('service not found') unless Service.exists?(params[:service_id])
end

delete '' do |service_id|
Comment thread
eguzki marked this conversation as resolved.
delete_stats_job_attrs = api_params Stats::DeleteJobDef
delete_stats_job_attrs[:service_id] = service_id
delete_stats_job_attrs[:from] = delete_stats_job_attrs[:from].to_i
delete_stats_job_attrs[:to] = delete_stats_job_attrs[:to].to_i
begin
Stats::DeleteJobDef.new(delete_stats_job_attrs).run_async
Comment thread
eguzki marked this conversation as resolved.
rescue DeleteServiceStatsValidationError => e
[400, headers, { status: :error, error: e.message }.to_json]
else
{ status: :to_be_deleted }.to_json
end
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/3scale/backend.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
require '3scale/backend/queue_storage'
require '3scale/backend/transaction_storage'
require '3scale/backend/errors'
require '3scale/backend/stats/aggregator'
require '3scale/backend/stats'
require '3scale/backend/usage_limit'
require '3scale/backend/user'
require '3scale/backend/alerts'
Expand Down
5 changes: 5 additions & 0 deletions lib/3scale/backend/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -385,5 +385,10 @@ def initialize(id)
end
end

class DeleteServiceStatsValidationError < Error
def initialize(service_id, msg)
super "Delete stats job context validation error. Service: #{service_id}. Error: #{msg}"
end
end
end
end
3 changes: 3 additions & 0 deletions lib/3scale/backend/stats.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require '3scale/backend/stats/aggregator'
require '3scale/backend/stats/partition_generator_job'
require '3scale/backend/stats/delete_job_def'
65 changes: 65 additions & 0 deletions lib/3scale/backend/stats/delete_job_def.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
module ThreeScale
module Backend
module Stats
class DeleteJobDef
Comment thread
eguzki marked this conversation as resolved.
ATTRIBUTES = %i[service_id applications metrics users from to context_info].freeze
private_constant :ATTRIBUTES
attr_reader(*ATTRIBUTES)

def self.attribute_names
ATTRIBUTES
end

def initialize(params = {})
ATTRIBUTES.each do |key|
instance_variable_set("@#{key}".to_sym, params[key]) unless params[key].nil?
end
validate
end

def run_async
Resque.enqueue(PartitionGeneratorJob, Time.now.getutc.to_f, service_id, applications,
metrics, users, from, to, context_info)
end

def to_json
to_hash.to_json
end

def to_hash
Hash[ATTRIBUTES.collect { |key| [key, send(key)] }]
end

private

def validate
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to mention that we should probably put some kind of limit on the to parameter or on the time interval between to and from. Otherwise, someone could send a call to delete all the keys from 1970 up until now or something like that. We can fix this in the integration branch later.

# from and to valid epoch times
raise_validation_error('from field not integer') unless from.is_a? Integer
raise_validation_error('from field is zero') if from.zero?
raise_validation_error('to field not integer') unless to.is_a? Integer
raise_validation_error('to field is zero') if to.zero?
raise_validation_error('from < to fields') if Time.at(to) < Time.at(from)
# application is array
raise_validation_error('applications field') unless applications.is_a? Array
raise_validation_error('applications values') unless applications.all? do |x|
x.is_a?(String) || x.is_a?(Integer)
end
# metrics is array
raise_validation_error('metrics field') unless metrics.is_a? Array
raise_validation_error('metrics values') unless metrics.all? do |x|
x.is_a?(String) || x.is_a?(Integer)
end
# users is array
raise_validation_error('users field') unless users.is_a? Array
raise_validation_error('users values') unless users.all? do |x|
x.is_a?(String) || x.is_a?(Integer)
end
end

def raise_validation_error(msg)
raise DeleteServiceStatsValidationError.new(service_id, msg)
end
end
end
end
end
20 changes: 20 additions & 0 deletions lib/3scale/backend/stats/partition_generator_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module ThreeScale
module Backend
module Stats
class PartitionGeneratorJob < BackgroundJob
# low priority queue
@queue = :main

class << self
def perform_logged(_enqueue_time, service_id, applications, metrics, users, from, to, context_info = {}) end

private

def enqueue_time
@args[0]
end
end
end
end
end
end
68 changes: 68 additions & 0 deletions spec/acceptance/api/internal/stats_api_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
require_relative '../../../spec_helpers/acceptance_spec_helper'

resource 'Stats (prefix: /services/:service_id/stats)' do
header 'Accept', 'application/json'
header 'Content-Type', 'application/json'

let(:existing_service_id) { '10000' }
let(:service_id) { existing_service_id }
let(:provider_key) { 'statsfoo' }
let(:applications) { %w[1 2 3] }
let(:metrics) { %w[10 20 30] }
let(:users) { %w[100 200 300] }
let(:from) { Time.new(2002, 10, 31).to_i }
let(:to) { Time.new(2003, 10, 31).to_i }
let(:req_body) do
{
deletejobdef: {
applications: applications,
metrics: metrics,
users: users,
from: from,
to: to
}
}
end
# From and To fields are sent as string, even though they are integers in req_body
let(:raw_post) { req_body }

before do
ThreeScale::Backend::Service.save!(provider_key: provider_key, id: existing_service_id)
end

delete '/services/:service_id/stats' do
parameter :service_id, 'Service ID', required: true

context 'PartitionGeneratorJob is enqueued' do
before do
ResqueSpec.reset!
end

example_request 'Deleting stats' do
expect(status).to eq 200
expect(response_json['status']).to eq 'to_be_deleted'
expect(ThreeScale::Backend::Stats::PartitionGeneratorJob).to have_queued(anything,
existing_service_id,
applications,
metrics, users,
from, to, nil)
end
end

context 'service does not exist' do
let(:service_id) { existing_service_id + 'foo' }

example_request 'Deleting stats' do
expect(status).to eq 404
end
end

context 'invalid param sent' do
let(:from) { 'adfsadfasd' }

example_request 'Deleting stats' do
expect(status).to eq 400
end
end
end
end
165 changes: 165 additions & 0 deletions spec/unit/stats/delete_job_def_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
require_relative '../../spec_helper'

RSpec.shared_examples 'job hash is correct' do
it 'has service_id' do
expect(job).to include(service_id: service_id)
end

it 'has applications' do
expect(job).to include(applications: applications)
end

it 'has metrics' do
expect(job).to include(metrics: metrics)
end

it 'has users' do
expect(job).to include(users: users)
end

it 'has from' do
expect(job).to include(from: from)
end

it 'has to' do
expect(job).to include(to: to)
end
end

RSpec.shared_examples 'validation error' do
it 'raise validation error' do
expect { subject }.to raise_error(ThreeScale::Backend::DeleteServiceStatsValidationError)
end
end

RSpec.describe ThreeScale::Backend::Stats::DeleteJobDef do
let(:service_id) { 'some_service_id' }
let(:applications) { %w[1 2 3] }
let(:metrics) { %w[10 20 30] }
let(:users) { %w[100 200 300] }
let(:from) { Time.new(2002, 10, 31).to_i }
let(:to) { Time.new(2003, 10, 31).to_i }
let(:params) do
{
service_id: service_id,
applications: applications,
metrics: metrics,
users: users,
from: from,
to: to
}
end
subject { described_class.new params }

context '#initialize' do
context 'happy path' do
it 'does not raise' do should_not be_nil end
end

context 'from field is nil' do
let(:from) { nil }
include_examples 'validation error'
end

context 'from field is string' do
let(:from) { '12345' }
include_examples 'validation error'
end

context 'from field is zero' do
let(:from) { 0 }
include_examples 'validation error'
end

context 'to field is nil' do
let(:to) { nil }
include_examples 'validation error'
end

context 'to field is string' do
let(:to) { '12345' }
include_examples 'validation error'
end

context 'to field is zero' do
let(:to) { 0 }
include_examples 'validation error'
end

context 'to field happens before from field' do
let(:from) { Time.new(2005, 10, 31).to_i }
let(:to) { Time.new(2003, 10, 31).to_i }
include_examples 'validation error'
end

context 'applicatoins field is nil' do
let(:applications) { nil }
include_examples 'validation error'
end

context 'applicatoins field is not array' do
let(:applications) { 3 }
include_examples 'validation error'
end

context 'applicatoins field constains bad element' do
let(:applications) { ['3', {}, '4'] }
include_examples 'validation error'
end

context 'metrics field is nil' do
let(:metrics) { nil }
include_examples 'validation error'
end

context 'metrics field is not array' do
let(:metrics) { 3 }
include_examples 'validation error'
end

context 'metrics field constains element bad string' do
let(:metrics) { ['3', [], '4'] }
include_examples 'validation error'
end

context 'users field is nil' do
let(:users) { nil }
include_examples 'validation error'
end

context 'users field is not array' do
let(:users) { 3 }
include_examples 'validation error'
end

context 'users field constains element bad string' do
let(:users) { ['3', [], '4'] }
include_examples 'validation error'
end
end

context '#run_async' do
before do
ResqueSpec.reset!
end

it 'partition generator job is queued' do
subject.run_async
expect(ThreeScale::Backend::Stats::PartitionGeneratorJob).to have_queued(anything,
service_id,
applications,
metrics, users,
from, to, nil)
end
end

context '#to_json' do
let(:job) { JSON.parse(subject.to_json, symbolize_names: true) }
include_examples 'job hash is correct'
end

context '#to_hash' do
let(:job) { subject.to_hash }
include_examples 'job hash is correct'
end
end