Skip to content

Commit

Permalink
Add V2 Polling Lambda (#10123)
Browse files Browse the repository at this point in the history
* add api gateway

* add lambda function

* update terraform so we can use ruby3.3 in lambdas

* add hooks to mark a registration as processing

* fix typo

* fix typo v2

* move registration_processing_cache_key to helper method

* destructure array in before_enqueue
  • Loading branch information
FinnIckler authored Nov 1, 2024
1 parent 65a3819 commit 4e06080
Show file tree
Hide file tree
Showing 13 changed files with 381 additions and 1 deletion.
5 changes: 5 additions & 0 deletions app/jobs/add_registration_job.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# frozen_string_literal: true

class AddRegistrationJob < ApplicationJob
before_enqueue do |job|
_, competition_id, user_id = job.arguments
Rails.cache.write(CacheAccess.registration_processing_cache_key(competition_id, user_id), true)
end

def self.prepare_task(user_id, competition_id)
message_deduplication_id = "competing-registration-#{competition_id}-#{user_id}"
message_group_id = competition_id
Expand Down
6 changes: 6 additions & 0 deletions app/models/registration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ class Registration < ApplicationRecord
end
end

after_create :mark_registration_processing_as_done

private def mark_registration_processing_as_done
Rails.cache.delete(CacheAccess.registration_processing_cache_key(competition_id, user_id))
end

def guest_limit
competition.guests_per_registration_limit
end
Expand Down
4 changes: 4 additions & 0 deletions infra/wca_on_rails/lambda/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Gemfile.lock
vendor
.bundle
processing_status.zip
9 changes: 9 additions & 0 deletions infra/wca_on_rails/lambda/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

source 'https://rubygems.org'

ruby '3.3.5'

gem 'aws-sdk-sqs'
gem 'superconfig'
gem 'redis'
7 changes: 7 additions & 0 deletions infra/wca_on_rails/lambda/package_lambda.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
bundle install --path vendor/bundle
# remove old zip if it exists
rm -f processing_status.zip
zip -r processing_status.zip processing_status.rb vendor
# remove any bundler or vendor files
rm -rf .bundle
rm -rf vendor
72 changes: 72 additions & 0 deletions infra/wca_on_rails/lambda/processing_status.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

require 'json'
require 'redis'
require 'aws-sdk-sqs'
require 'superconfig'

EnvConfig = SuperConfig.new do
mandatory :REDIS_URL, :string
mandatory :QUEUE_URL, :string
mandatory :AWS_REGION, :string
end

RedisConn = Redis.new(url: EnvConfig.REDIS_URL)

def lambda_handler(event:, context:)
# Parse the input event
query = event['queryStringParameters']
if query.nil? || query['competition_id'].nil? || query['user_id'].nil?
response = {
statusCode: 400,
body: JSON.generate({ status: 'Missing fields in request' }),
headers: {
"Access-Control-Allow-Headers" => "*",
"Access-Control-Allow-Origin" => "*",
"Access-Control-Allow-Methods" => "OPTIONS,POST,GET",
},
}
else
sqs_client = Aws::SQS::Client.new(region: EnvConfig.AWS_REGION)

queue_attributes = sqs_client.get_queue_attributes({
queue_url: EnvConfig.QUEUE_URL,
attribute_names: ['ApproximateNumberOfMessages'],
})
message_count = queue_attributes.attributes['ApproximateNumberOfMessages'].to_i

processing = RedisConn.get("#{query['competition_id']}-#{query['user_id']}-processing")

response = {
statusCode: 200,
body: JSON.generate({ processing: !processing.nil?, queue_count: message_count }),
headers: {
"Access-Control-Allow-Headers" => "*",
"Access-Control-Allow-Origin" => "*",
"Access-Control-Allow-Methods" => "OPTIONS,POST,GET",
},
}
end

# Return the response
{
statusCode: response[:statusCode],
body: response[:body],
headers: {
"Access-Control-Allow-Headers" => "*",
"Access-Control-Allow-Origin" => "*",
"Access-Control-Allow-Methods" => "OPTIONS,POST,GET",
},
}
rescue StandardError => e
# Handle any errors
{
statusCode: 500,
body: JSON.generate({ error: e.message }),
headers: {
"Access-Control-Allow-Headers" => "*",
"Access-Control-Allow-Origin" => "*",
"Access-Control-Allow-Methods" => "OPTIONS,POST,GET",
},
}
end
2 changes: 1 addition & 1 deletion infra/wca_on_rails/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
version = ">= 5.72.1"
}
}

Expand Down
127 changes: 127 additions & 0 deletions infra/wca_on_rails/production/lambda.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
resource "aws_lambda_function" "registration_status_lambda" {
filename = "./lambda/processing_status.zip"
function_name = "${var.name_prefix}-poller-lambda"
role = aws_iam_role.lambda_role.arn
handler = "processing_status.lambda_handler"
runtime = "ruby3.3"
source_code_hash = filebase64sha256("./lambda/processing_status.zip")
vpc_config {
security_group_ids = [var.shared.cluster_security.id]
subnet_ids = var.shared.private_subnets[*].id
}
timeout = 10
environment {
variables = {
REDIS_URL = "redis://wca-main-cache.iebvzt.ng.0001.usw2.cache.amazonaws.com:6379"
QUEUE_URL = aws_sqs_queue.this.url
}
}
}

resource "aws_iam_role" "lambda_role" {
name = "${var.name_prefix}-lambda-execution-role"

assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = "sts:AssumeRole",
Effect = "Allow",
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}

resource "aws_iam_role_policy_attachment" "lambda_exec_policy_attach" {
role = aws_iam_role.lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}

resource "aws_lambda_permission" "this" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.registration_status_lambda.function_name
principal = "apigateway.amazonaws.com"
}

data "aws_iam_policy_document" "lambda_policy" {
statement {
effect = "Allow"
actions = [
"sqs:GetQueueAttributes",
"sqs:GetQueueUrl"
]
resources = [aws_sqs_queue.this.arn]
}
}

resource "aws_iam_role_policy" "lambda_policy_attachment" {
role = aws_iam_role.lambda_role.name
policy = data.aws_iam_policy_document.lambda_policy.json
}

resource "aws_api_gateway_resource" "prod" {
rest_api_id = var.shared.api_gateway.id
parent_id = var.shared.api_gateway.root_resource_id
path_part = "prod"
}

resource "aws_api_gateway_method" "poll_registration_status_method" {
rest_api_id = var.shared.api_gateway.id
resource_id = aws_api_gateway_resource.prod.id
http_method = "GET"
authorization = "NONE"
}

resource "aws_api_gateway_integration" "poll_registration_integration" {
rest_api_id = var.shared.api_gateway.id
resource_id = aws_api_gateway_resource.prod.id
http_method = aws_api_gateway_method.poll_registration_status_method.http_method

integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.registration_status_lambda.invoke_arn
}

resource "aws_api_gateway_method_response" "registration_status_method" {
rest_api_id = var.shared.api_gateway.id
resource_id = aws_api_gateway_resource.prod.id
http_method = aws_api_gateway_method.poll_registration_status_method.http_method
status_code = "200"

response_parameters = {
"method.response.header.Content-Type" = true
"method.response.header.Access-Control-Allow-Origin" = false
}
}

resource "aws_api_gateway_integration_response" "registration_status_integration_response" {
rest_api_id = var.shared.api_gateway.id
resource_id = aws_api_gateway_resource.prod.id
http_method = aws_api_gateway_method.poll_registration_status_method.http_method
status_code = aws_api_gateway_method_response.registration_status_method.status_code

response_templates = {
"application/json" = jsonencode({
processing = "Processing Status"
queue_count = "Queue Count"
})
}

response_parameters = {
"method.response.header.Access-Control-Allow-Origin" = "'*'"
}

depends_on = [aws_api_gateway_resource.prod, aws_api_gateway_method.poll_registration_status_method, aws_api_gateway_method_response.registration_status_method, aws_api_gateway_integration.poll_registration_integration]
}
resource "aws_api_gateway_deployment" "this" {
rest_api_id = var.shared.api_gateway.id

lifecycle {
create_before_destroy = true
}
depends_on = [aws_api_gateway_method.poll_registration_status_method, aws_api_gateway_integration.poll_registration_integration]
}
4 changes: 4 additions & 0 deletions infra/wca_on_rails/production/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ variable "shared" {
pma_production: object({
arn: string
})
api_gateway: object({
id: string,
root_resource_id: string
})
account_id: string
# These are booth arrays
private_subnets: any
Expand Down
11 changes: 11 additions & 0 deletions infra/wca_on_rails/shared/api_gateway.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
resource "aws_api_gateway_rest_api" "this" {
name = "wca-monolith-pollingco-api"
description = "The API to Poll for updates"
endpoint_configuration {
types = ["REGIONAL"]
}
}

output "api_gateway" {
value = aws_api_gateway_rest_api.this
}
Loading

0 comments on commit 4e06080

Please sign in to comment.