Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a9b1485
started EventsController
williampj Nov 10, 2020
a4da75c
latest controller change
williampj Nov 10, 2020
26ebd02
Merge branch 'master' of https://github.com/angeljruiz/BridgeAPI.rb i…
williampj Nov 10, 2020
60b7e77
Merge branch 'master' of https://github.com/angeljruiz/BridgeAPI.rb i…
williampj Nov 10, 2020
c93f543
.
williampj Nov 10, 2020
5b92135
fast forwarded
williampj Nov 10, 2020
2bc0290
.
williampj Nov 10, 2020
47d2906
updated user model
williampj Nov 10, 2020
112c694
.
williampj Nov 10, 2020
3b35da6
.
williampj Nov 10, 2020
14dbaa0
updated db schema
williampj Nov 10, 2020
88463d1
fixed type
williampj Nov 10, 2020
5bf7e3b
binary to jsonb
williampj Nov 11, 2020
250f782
.
williampj Nov 11, 2020
4f8920e
progressing
williampj Nov 11, 2020
95f2735
Merge branch 'master' of https://github.com/angeljruiz/BridgeAPI.rb i…
williampj Nov 11, 2020
45e38b8
removed null constraint from status_code attribute
williampj Nov 11, 2020
efc0d24
http request functionality in sidekiq
williampj Nov 12, 2020
e2de02f
latest
williampj Nov 12, 2020
f83a65f
finished draft for index, create and show eventscontroller actions
williampj Nov 12, 2020
cc00730
.
williampj Nov 12, 2020
8c135cd
.
williampj Nov 12, 2020
216b7e0
added destroy action
williampj Nov 12, 2020
0649385
updating retry and delay logic
williampj Nov 13, 2020
2912f95
updated create action and event_worker to work with delays, retries a…
williampj Nov 14, 2020
ca94904
cleaned up actions
williampj Nov 14, 2020
cc6d0ea
fast forwarded master
williampj Nov 16, 2020
7bbc6ad
updated event_worker
williampj Nov 17, 2020
4049037
updated seed db
williampj Nov 17, 2020
b70bbc3
DRY events_controller
williampj Nov 17, 2020
29e4b20
finished events_controller and event worker
williampj Nov 18, 2020
d72afe5
created event_spec test file
williampj Nov 18, 2020
3d348ca
addressed rubocop complaints
williampj Nov 18, 2020
d8f61de
editet data formatting and added sidekiq retry middleware
williampj Nov 19, 2020
918d0c1
added dynamic delay to event_worker
williampj Nov 19, 2020
257af7e
small cleanup
williampj Nov 19, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ gem 'puma', '~> 4.1'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1.7'
gem 'jwt'
Expand Down
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ DEPENDENCIES
puma (~> 4.1)
rack-cors
rails (~> 6.0.3, >= 6.0.3.4)
redis (~> 4.0)
rspec-rails (~> 4.0.1)
rubocop
sidekiq
Expand Down
120 changes: 69 additions & 51 deletions app/controllers/events_controller.rb
Original file line number Diff line number Diff line change
@@ -1,68 +1,86 @@
# frozen_string_literal: true

class EventsController < ApplicationController
before_action :authorize_request
before_action :set_user
# Needs to find all events based on bridge_id or event_id
# Needs to return id for each event as well
# def index
# events = @current_user.bridges
# events.map! do |event|
# updated_at = String(event.updated_at)
# date = date_format(updated_at.split(' ')[1])
# time = updated_at.split(' ')[0]
# { id: 1,
# time: time,
# date: date,
# status_code: event.status_code }
# end
# render json: { events: events }, status: 200 # OK
# end
# before_action :authorize_request
before_action :set_events, only: :index
before_action :set_event, only: %i[show destroy]

def show; end
def index
render json: @events, status: 200
rescue ActiveRecord::RecordInvalid
render json: { error: 'neither event_id nor bridge_id were valid' }, status: 400 # Bad Request
rescue ActiveRecord::RecordNotFound
render json: { error: 'events matching that id were not found' }, status: 400
rescue ActiveRecord::NotNullViolation
render json: { error: 'neither event_id nor bridge_id were not submitted' }, status: 400 # Bad Request
end

def show
render json: @event, status: 200
rescue ActiveRecord::RecordNotFound
render json: { error: 'an event by that id was not found' }, status: 400
end

def destroy
@event.destroy
end

# receive bridge id + data
def create
bridge = Bridge.find(params[:id])
data = { inbound: request.body, outbounds: [] }
event = Event.new(data: data, bridge_id: bridge.id)
if event.save
# EventWorker.perform(id of event just created)
status 201 # Created
else
status 400 # Bad Request
end
event = create_event_object(create_data_object, find_bridge)
event.save!
EventWorker.perform_async(event.id)
render json: {}, status: 202 # Accepted (asynchronous processing)
rescue ActiveRecord::RecordNotFound
render json: { error: 'a bridge by that id was not found' }, status: 400
rescue ActiveRecord::RecordInvalid
render json: { error: 'payload, bridge_id, or urls were invalid' }, status: 400 # Bad Request
rescue ActiveRecord::NotNullViolation
render json: { error: 'payload, bridge_id, or urls fields were not submitted' }, status: 400 # Bad Request
end

private

def date_format(_date)
year = time.split('-')[0]
month = time.split('-')[1]
day = time.split('-')[2]
"#{year}-#{month}-#{day}"
def event_params
params.permit(:id, :bridge_id, :event_id, :test)
end

def set_user
# bridge_id or #event_id
# => User
def create_data_object
{ 'inbound' => {
'payload' => JSON.parse(request.body.read),
'dateTime' => DateTime.now.utc,
'ip' => request.ip,
'contentLength' => request.content_length
},
'outbound' => [] }
end
end

# Step 1
# Convert binary to jsonb
def create_event_object(data, bridge)
Event.new(
data: data.to_json,
bridge_id: bridge.id,
test: event_params[:test] || false
)
end

# Step 2
# payload:
{
test: 'user entered string from editor',
production: 'user entered string from editor'
}
def set_events
@events = if event_params[:bridge_id]
Event.where(bridge_id: event_params[:bridge_id]).order(completed_at: :desc).limit(100)
elsif event_params[:event_id]
Event.where(bridge_id: find_event.bridge_id).order(completed_at: :desc).limit(100)
else
raise ActiveRecord::NotNullViolation
end
end

def set_event
@event = find_event
end

# Step 3
#
def find_event
Event.find(event_params[:event_id])
end

# NB: Need to run `bundle exec sidekiq` in separate terminal
# localhost:3000/sidekiq to monitor sidekiq while running
# after turning it on
# => mount Sidekiq::Web => '/sidekiq
def find_bridge
Bridge.find(event_params[:bridge_id])
end
end
6 changes: 6 additions & 0 deletions app/lib/exceptions/large_status_code.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

module Sidekiq
class LargeStatusCode < StandardError
end
end
35 changes: 34 additions & 1 deletion app/models/event.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,40 @@
# frozen_string_literal: true

BOOLEAN = [true, false].freeze

class Event < ApplicationRecord
validates :outbound_url, presence: true
before_validation :set_urls
validates :test, inclusion: [true, false]
validates :completed, inclusion: [true, false]
validates :status_code, numericality: { greater_than_or_equal_to: 100, less_than_or_equal_to: 599 }, allow_nil: true
validate :completed_at_format
validate :data_json_object

belongs_to :bridge

private

def set_urls
self.inbound_url = bridge.inbound_url
self.outbound_url = bridge.outbound_url
end

def data_json_object
data = JSON.parse(self.data)
%w[inbound outbound].all? { |key| data.include?(key) } &&
%w[payload dateTime ip contentLength].all? { |key| data['inbound'].include?(key) } ||
errors.add(
:data,
'must include the keys: "inbound", "outbound",
while "inbound" must include the keys "payload", "dateTime", "ip", "contentLength"'
)
rescue JSON::ParserError, TypeError
errors.add(:data, 'object must be a valid json object')
end

def completed_at_format
return if completed_at.nil? || completed_at.instance_of?(ActiveSupport::TimeWithZone)

errors.add(:completed_at, '"completed_at" must be a Time instance if event is completed')
end
end
126 changes: 119 additions & 7 deletions app/workers/event_worker.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,127 @@
# frozen_string_literal: true

require 'net/http'
require_relative '../lib/exceptions/large_status_code'

class EventWorker
include Sidekiq::Worker
# attr_accessor :retry_count

# sidekiq_retry_in { 5 } # TODO: SET RETRY_IN DYNAMICALLY

SCHEME = 'http://'

def perform(event_id, retries = 0)
event = Event.find(event_id)
bridge = Bridge.find(event.bridge_id)
execute_request_response_cycle(event, bridge)
complete_event(event)
rescue StandardError => e
binding.pry
save_http_error(event, e) unless e.instance_of?(Sidekiq::LargeStatusCode)
complete_event(event) && return if retries >= bridge.retries

EventWorker.perform_in(bridge.delay.minutes, event_id, retries + 1)
end

private

def set_headers(req, bridge)
bridge.headers.each { |header| req[header['key']] = header['value'] }
end

# takes event id argument
# find event =>
# find bridge =>
# git payload & outbound url
# => send it
def complete_event(event)
event.completed = true
event.completed_at = Time.now.utc
event.save!
end

def create_error_response(error)
{
message: error.message
}
end

def save_http_error(event, error)
response = create_error_response(error)
event_data = JSON.parse(event.data)

event_data['outbound'].last['response'] = response
event.data = event_data.to_json
event.save
end

def create_response_object(payload, response)
{
dateTime: DateTime.now.utc,
statusCode: response.code,
message: response.message,
size: response.size,
payload: payload
}
end

def save_response(event, resp)
resp_code = resp.code.to_i
payload = (resp_code >= 300 ? {} : JSON.parse(resp.body))
response = create_response_object(payload, resp)
event_data = JSON.parse(event.data)

event_data['outbound'].last['response'] = response
event.data = event_data.to_json
event.save
raise Sidekiq::LargeStatusCode if resp_code >= 300
end

def create_request_object(payload, length)
{
payload: payload,
dateTime: DateTime.now.utc,
contentLength: length
}
end

def save_request(event, length, payload)
request = create_request_object(payload, length)
event_data = JSON.parse(event.data)

event_data['outbound'].push({ 'request' => request, 'response' => {} })
event.data = event_data.to_json
event.save
end

def prepend_scheme(uri)
return uri if uri.starts_with?('https')

minus_scheme = uri.split('//').last
"#{SCHEME}#{minus_scheme}"
end

def generate_http_request(bridge, payload)
method = bridge.method.capitalize
uri = URI(prepend_scheme(bridge.outbound_url))
http = Net::HTTP.new(uri.host, uri.port)
req = "Net::HTTP::#{method}".constantize.new(uri, 'Content-Type' => 'application/json')

http.use_ssl = (uri.scheme == 'https')
set_headers(req, bridge)
req.body = payload.to_json
[http, req]
end

def extract_payload(bridge, event)
if event.test
JSON.parse(bridge.data['test_payload'])
else
JSON.parse(bridge.data['payload'])
end
end

def perform(event_id)
# do something
def execute_request_response_cycle(event, bridge)
payload = extract_payload(bridge, event)
http, req = generate_http_request(bridge, payload)
save_request(event, req.length, payload)
response = http.request(req)
save_response(event, response)
end
end
12 changes: 12 additions & 0 deletions config/initializers/sidekiq.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class SidekiqMiddleware
def call(worker, job, _queue)
worker.retry_count = job['retry_count'] if worker.respond_to?(:retry_count)
yield
end
end

Sidekiq.configure_server do |config|
config.server_middleware do |chain|
chain.add SidekiqMiddleware
end
end
6 changes: 6 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# frozen_string_literal: true

require 'sidekiq/web'

Rails.application.routes.draw do
resource :user, except: %i[new edit]
resources :bridges
resources :headers, :environment_variables, only: :destroy

post 'login', to: 'sessions#create'
post 'events', to: 'events#create'
get 'events', to: 'events#index'
get 'events/:event_id', to: 'events#show'
delete 'events/:event_id', to: 'events#destroy'
mount Sidekiq::Web => '/sidekiq'
end
Binary file added db/dump.rdb
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
class ChangeBinaryColumnsToJsonB < ActiveRecord::Migration[6.0]
def change
def up
change_column :bridges, :payload, :jsonb, using: 'payload::text::jsonb', null: false
change_column :events, :data, :jsonb, using: 'data::text::jsonb', null: false
end

def down
change_column :bridges, :payload, :text
change_column :events, :data, :text
end
end
5 changes: 5 additions & 0 deletions db/migrate/20201111174614_remove_default_attribute.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class RemoveDefaultAttribute < ActiveRecord::Migration[6.0]
def change
change_column_default :bridges, :payload, nil
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class RemoveNotNullStatusCodeInEvents < ActiveRecord::Migration[6.0]
def change
change_column_null :events, :status_code, true
end
end
5 changes: 5 additions & 0 deletions db/migrate/20201113001336_add_test_to_events.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddTestToEvents < ActiveRecord::Migration[6.0]
def change
add_column :events, :test, :boolean, default: false, null: false
end
end
Loading