Skip to content

Commit a1b61ea

Browse files
authored
Big v1 Release (#1)
1 parent 629b920 commit a1b61ea

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1590
-75
lines changed

Gemfile.lock

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,25 @@ PATH
44
lambdakiq (1.0.0)
55
activejob
66
aws-sdk-sqs
7+
concurrent-ruby
8+
railties
79

810
GEM
911
remote: https://rubygems.org/
1012
specs:
13+
actionpack (6.0.3.4)
14+
actionview (= 6.0.3.4)
15+
activesupport (= 6.0.3.4)
16+
rack (~> 2.0, >= 2.0.8)
17+
rack-test (>= 0.6.3)
18+
rails-dom-testing (~> 2.0)
19+
rails-html-sanitizer (~> 1.0, >= 1.2.0)
20+
actionview (6.0.3.4)
21+
activesupport (= 6.0.3.4)
22+
builder (~> 3.1)
23+
erubi (~> 1.4)
24+
rails-dom-testing (~> 2.0)
25+
rails-html-sanitizer (~> 1.1, >= 1.2.0)
1126
activejob (6.0.3.4)
1227
activesupport (= 6.0.3.4)
1328
globalid (>= 0.3.6)
@@ -29,25 +44,54 @@ GEM
2944
aws-sigv4 (~> 1.1)
3045
aws-sigv4 (1.2.2)
3146
aws-eventstream (~> 1, >= 1.0.2)
47+
builder (3.2.4)
3248
coderay (1.1.3)
3349
concurrent-ruby (1.1.7)
50+
crass (1.0.6)
51+
erubi (1.10.0)
3452
globalid (0.4.2)
3553
activesupport (>= 4.2.0)
3654
i18n (1.8.5)
3755
concurrent-ruby (~> 1.0)
3856
jmespath (1.4.0)
57+
loofah (2.8.0)
58+
crass (~> 1.0.2)
59+
nokogiri (>= 1.5.9)
60+
macaddr (1.7.2)
61+
systemu (~> 2.6.5)
3962
method_source (1.0.0)
63+
mini_portile2 (2.4.0)
4064
minitest (5.14.2)
4165
minitest-focus (1.2.1)
4266
minitest (>= 4, < 6)
4367
mocha (1.11.2)
68+
nokogiri (1.10.10)
69+
mini_portile2 (~> 2.4.0)
4470
pry (0.13.1)
4571
coderay (~> 1.1)
4672
method_source (~> 1.0)
73+
rack (2.2.3)
74+
rack-test (1.1.0)
75+
rack (>= 1.0, < 3)
76+
rails-dom-testing (2.0.3)
77+
activesupport (>= 4.2.0)
78+
nokogiri (>= 1.6)
79+
rails-html-sanitizer (1.3.0)
80+
loofah (~> 2.3)
81+
railties (6.0.3.4)
82+
actionpack (= 6.0.3.4)
83+
activesupport (= 6.0.3.4)
84+
method_source
85+
rake (>= 0.8.7)
86+
thor (>= 0.20.3, < 2.0)
4787
rake (13.0.1)
88+
systemu (2.6.5)
89+
thor (1.0.1)
4890
thread_safe (0.3.6)
4991
tzinfo (1.2.8)
5092
thread_safe (~> 0.1)
93+
uuid (2.3.9)
94+
macaddr (~> 1.0)
5195
zeitwerk (2.4.2)
5296

5397
PLATFORMS
@@ -61,6 +105,7 @@ DEPENDENCIES
61105
mocha
62106
pry
63107
rake
108+
uuid
64109

65110
BUNDLED WITH
66111
2.1.4

README.md

Lines changed: 237 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,247 @@
11

2+
![Lambdakiq: ActiveJob on SQS & Lambda](images/Lambdakiq.png)
3+
24
# Lambdakiq
35

4-
TODO ...
6+
<a href="https://lamby.custominktech.com"><img src="https://user-images.githubusercontent.com/2381/59363668-89edeb80-8d03-11e9-9985-2ce14361b7e3.png" alt="Lamby: Simple Rails & AWS Lambda Integration using Rack." align="right" width="300" /></a>A drop-in replacement for [Sidekiq](https://github.com/mperham/sidekiq) when running Rails in AWS Lambda using the [Lamby](https://lamby.custominktech.com) gem.
7+
8+
Lambdakiq allows you to leverage AWS' managed infrastructure to the fullest extent. Gone are the days of managing pods and long polling processes. Instead AWS delivers messages directly to your Rails' job functions and scales it up and down as needed. Observability is built in using AWS CloudWatch Metrics, Dashboards, and Alarms. Learn more about [Using AWS Lambda with Amazon SQS](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html) or get started now.
9+
10+
## Key Features
11+
12+
* Distinct web & jobs Lambda functions.
13+
* AWS fully managed polling. Event-driven.
14+
* Maximum 12 retries. Per job configurable.
15+
* Mirror Sidekiq's retry [backoff](https://github.com/mperham/sidekiq/wiki/Error-Handling#automatic-job-retry) timing.
16+
* Last retry is at 11 hours 30 minutes.
17+
* Supports ActiveJob's wait/delay. Up to 15 minutes.
18+
* Dead messages are stored for up to 14 days.
19+
20+
## Project Setup
21+
22+
This gem assumes your Rails application is on AWS Lambda, ideally with our [Lamby](https://lamby.custominktech.com) gem. It could be using Lambda's traditional zip package type or the newer [container](https://dev.to/aws-heroes/lambda-containers-with-rails-a-perfect-match-4lgb) format. If Rails on Lambda is new to you, consider following our [quick start](https://lamby.custominktech.com/docs/quick_start) guide to get your first application up and running. From there, to use Lambdakiq, here are steps to setup your project
23+
24+
25+
### Bundle & Config
26+
27+
Add the Lambdakiq gem to your `Gemfile`.
28+
29+
```ruby
30+
gem 'lambdakiq'
31+
```
32+
33+
Open `config/initializers/production.rb` and set Lambdakiq as your ActiveJob queue adapter.
34+
35+
```ruby
36+
config.active_job.queue_adapter = :lambdakiq
37+
```
38+
39+
Open `app/jobs/application_job.rb` and add our worker module. The queue name will be set by an environment using CloudFormation further down.
40+
41+
```ruby
42+
class ApplicationJob < ActiveJob::Base
43+
include Lambdakiq::Worker
44+
queue_as ENV['JOBS_QUEUE_NAME']
45+
end
46+
```
47+
48+
### SQS Resources
49+
50+
Open up your project's SAM [`template.yaml`](https://lamby.custominktech.com/docs/anatomy#file-template-yaml) file and make the following additions and changes. First, we need to create your [SQS queues](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html) under the `Resources` section.
51+
52+
```yaml
53+
JobsQueue:
54+
Type: AWS::SQS::Queue
55+
Properties:
56+
ReceiveMessageWaitTimeSeconds: 10
57+
RedrivePolicy:
58+
deadLetterTargetArn: !GetAtt JobsDLQueue.Arn
59+
maxReceiveCount: 13
60+
VisibilityTimeout: 301
61+
62+
JobsDLQueue:
63+
Type: AWS::SQS::Queue
64+
Properties:
65+
MessageRetentionPeriod: 1209600
66+
```
67+
68+
In this example above we are also creating a queue to automatically handle our redrives and storage for any dead messages. We use [long polling](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-short-and-long-polling.html#sqs-long-polling) to receive messages for lower costs. In most cases your message is consumed almost immediately. Sidekiq polling is around 10s too.
69+
70+
The max receive count is 13 which means you get 12 retries. This is done so we can mimic Sidekiq's [automatic retry and backoff](https://github.com/mperham/sidekiq/wiki/Error-Handling#automatic-job-retry). The dead letter queue retains messages for the maximum of 14 days. This can be changed as needed. We also make no assumptions on how you want to handle dead jobs.
71+
72+
### Queue Name Environment Variable
73+
74+
We need to pass the newly created queue's name as an environment variable to your soon to be created jobs function. Since it is common for your Rails web and jobs functions to share these, we can leverage [SAM's Globals](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification-template-anatomy-globals.html) section.
75+
76+
```yaml
77+
Globals:
78+
Function:
79+
Environment:
80+
Variables:
81+
RAILS_ENV: !Ref RailsEnv
82+
JOBS_QUEUE_NAME: !GetAtt JobsQueue.QueueName
83+
```
84+
85+
We can remove the `Environment` section from our web function and all functions in this stack will now use the globals. Here we are using an [intrinsic function](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html) to pass the queue's name as the `JOBS_QUEUE_NAME` environment variable.
86+
87+
### IAM Permissions
88+
89+
Both functions will need capabilities to access the SQS jobs queue. We can add or extend the [SAM Policies](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html#sam-function-policies) section of our `RailsLambda` web function so it (and our soon to be created jobs function) have full capabilities to this new queue.
90+
91+
```yaml
92+
Policies:
93+
- Version: '2012-10-17'
94+
Statement:
95+
- Effect: Allow
96+
Action:
97+
- sqs:*
98+
Resource:
99+
- !Sub arn:aws:sqs:${AWS::Region}:${AWS::AccountId}:${JobsQueue.QueueName}
100+
```
101+
102+
Now we can duplicate our `RailsLambda` resource YAML (except for the `Events` property) to a new `JobsLambda` one. This gives us a distinct Lambda function to process jobs whose events, memory, timeout, and more can be independently tuned. However, both the `web` and `jobs` functions will use the same ECR container image!
103+
104+
```yaml
105+
JobsLambda:
106+
Type: AWS::Serverless::Function
107+
Metadata:
108+
DockerContext: ./.lamby/RailsLambda
109+
Dockerfile: Dockerfile
110+
DockerTag: jobs
111+
Properties:
112+
Events:
113+
SQSJobs:
114+
Type: SQS
115+
Properties:
116+
Queue: !GetAtt JobsQueue.Arn
117+
BatchSize: 1
118+
MemorySize: 1792
119+
PackageType: Image
120+
Policies:
121+
- Version: '2012-10-17'
122+
Statement:
123+
- Effect: Allow
124+
Action:
125+
- sqs:*
126+
Resource:
127+
- !Sub arn:aws:sqs:${AWS::Region}:${AWS::AccountId}:${JobsQueue.QueueName}
128+
Timeout: 300
129+
```
130+
131+
Here are some key aspects of our `JobsLambda` resource above:
132+
133+
* The `Events` property uses the [SQS Type](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-sqs.html).
134+
* Our [BatchSize](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-sqs.html#sam-function-sqs-batchsize) is set to one so we can handle retrys more easily without worrying about idempotency in larger batches.
135+
* The `Metadata`'s Docker properties must be the same as our web function except for the `DockerTag`. This is needed for the image to be shared. This works around a known [SAM issue](https://github.com/aws/aws-sam-cli/issues/2466) vs using the `ImageConfig` property.
136+
* The jobs function `Timeout` must be lower than the `JobsQueue`'s `VisibilityTimeout` property. When the batch size is one, the queue's visibility is generally one second more.
137+
138+
🎉 Deploy your application and have fun with ActiveJob on SQS & Lambda.
139+
140+
## Configuration
141+
142+
Most general Lambdakiq configuration options are exposed via the Rails standard configuration method.
143+
144+
### Rails Configs
145+
146+
```ruby
147+
config.lambdakiq
148+
```
149+
150+
* `max_retries=` - Retries for all jobs. Default is the Lambdakiq maximum of `12`.
151+
* `metrics_namespace=` - The CloudWatch Embedded Metrics namespace. Default is `Lambdakiq`.
152+
* `metrics_logger=` - Set to the Rails logger which is STDOUT via Lamby/Lambda.
153+
154+
### ActiveJob Configs
155+
156+
You can also set configuration options on a per job basis using the `lambdakiq_options` method.
157+
158+
```ruby
159+
class OrderProcessorJob < ApplicationJob
160+
lambdakiq_options retry: 2
161+
end
162+
```
163+
164+
* `retry` - Overrides the default Lambdakiq `max_retries` for this one job.
165+
166+
## Concurrency & Limits
167+
168+
AWS SQS is highly scalable with [few limits](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-quotas.html). As your jobs in SQS increases so should your concurrent functions to process that work. However, as this article, ["Why isn't my Lambda function with an Amazon SQS event source scaling optimally?"](https://aws.amazon.com/premiumsupport/knowledge-center/lambda-sqs-scaling/) describes it is possible that errors will effect your concurrency.
169+
170+
To help keep your queue and workers scalable, reduce the errors raised by your jobs. You an also reduce the retry count.
171+
172+
## Observability with CloudWatch
173+
174+
Get ready to gain way more insights into your ActiveJobs using AWS' [CloudWatch](https://aws.amazon.com/cloudwatch/) service. Every AWS service, including SQS & Lambda, publishes detailed [CloudWatch Metrics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/working_with_metrics.html). This gem leverages [CloudWatch Embedded Metrics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format.html) to add detailed ActiveJob metrics to that system. You can mix and match these data points to build your own [CloudWatch Dashboards](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Dashboards.html). If needed, any combination can be used to trigger [CloudWatch Alarms](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html). Much like Sumo Logic, you can search & query for data using [CloudWatch Logs Insights](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AnalyzingLogData.html).
175+
176+
![CloudWatch Dashboard](https://user-images.githubusercontent.com/2381/106465990-be7a6200-6468-11eb-8461-93db0046cda5.png)
177+
178+
Metrics are published under the `Lambdakiq` namespace. This is configurable using `config.lambdakiq.metrics_namespace` but should not be needed since all metrics are published using these three dimensions which allow you to easily segment metrics/dashboards to a specific application.
179+
180+
### Metric Dimensions
181+
182+
* `AppName` - This is the name of your Rails application. Ex: `MyApp`
183+
* `JobEvent` - Name of the ActiveSupport Notification. Ex: `*.active_job`.
184+
* `JobName` - The class name of the ActiveSupport job. Ex: `NotificationJob`
185+
186+
### ActiveJob Event Names
187+
For reference, here are the `JobEvent` names published by ActiveSupport. A few of these are instrumented by Lambdakiq since we use custom retry logic like Sidekiq. These event/metrics are found in the Rails application CloudWatch logs because they publish/enqueue jobs.
188+
189+
* `enqueue.active_job`
190+
* `enqueue_at.active_job`
191+
192+
While these event/metrics can be found in the jobs function's log.
193+
194+
* `perform_start.active_job`
195+
* `perform.active_job`
196+
* `enqueue_retry.active_job`
197+
* `retry_stopped.active_job`
198+
199+
### Metric Properties
200+
201+
These are the properties published with each metric. Remember, properties can not be used as metric data in charts but can be searched using [CloudWatch Logs Insights](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AnalyzingLogData.html).
202+
203+
* `JobId` - ActiveJob Unique ID. Ex: `9f3b6977-6afc-4769-aed6-bab1ad9a0df5`
204+
* `QueueName` - SQS Queue Name. Ex: `myapp-JobsQueue-14F18LG6XFUW5.fifo`
205+
* `MessageId` - SQS Message ID. Ex: `5653246d-dc5e-4c95-9583-b6b83ec78602`
206+
* `ExceptionName` - Class name of error raised. Present in perform and retry events.
207+
* `EnqueuedAt` - When ActiveJob enqueued the message. Ex: `2021-01-14T01:43:38Z`
208+
* `Executions` - The number of current executions. Counts from `1` and up.
209+
* `JobArg#{n}` - Enumerated serialized arguments.
210+
211+
### Metric Data
212+
213+
And finally, here are the metrics which each dimension can chart using [CloudWatch Metrics & Dashboards](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Dashboards.html).
214+
215+
* `Duration` - Of the job event in milliseconds.
216+
* `Count` - Of the event.
217+
* `ExceptionCount` - Of the event. Useful with `ExceptionName`.
218+
219+
### CloudWatch Dashboard Examples
220+
221+
Please share how you are using CloudWatch to monitor and/or alert on your ActiveJobs with Lambdakiq!
222+
223+
💬 https://github.com/customink/lambdakiq/discussions/3
224+
225+
226+
## Common Questions
227+
228+
**Are Scheduled Jobs Supported?** - No. If you need a scheduled job please use the [SAM Schedule](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-schedule.html) event source which invokes your function with an [Eventbridege AWS::Events::Rule](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-rule.html).
229+
230+
**Are FIFO Queues Supported?** - Yes. When you create your [AWS::SQS::Queue](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html) resources you can set the [FifoQueue](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-fifoqueue) property to `true`. Remember that both your jobs queue and the redrive queue must be the same. When using FIFO we:
231+
232+
* Simulate `delay_seconds` for ActiveJob's wait by using visibility timeouts under the hood. We still cap it to non-FIFO's 15 minutes.
233+
* Set both the messages `message_group_id` and `message_deduplication_id` to the unique job id provided by ActiveJob.
234+
235+
**Can I Use Multiple Queues?** - Yes. Nothing is stopping you from creating any number of queues and/or functions to process them. Your subclasses can use ActiveJob's `queue_as` method as needed. This is an easy way to handle job priorities too.
5236

6237
```ruby
7-
# TODO ...
238+
class SomeLowPriorityJob < ApplicationJob
239+
queue_as ENV['BULK_QUEUE_NAME']
240+
end
8241
```
9242

243+
**What Is The Max Message Size?** - 256KB. ActiveJob messages should be small however since Rails uses the [GlobalID](https://github.com/rails/globalid) gem to avoid marshaling large data structures to jobs.
244+
10245
## Contributing
11246

12247
After checking out the repo, run:

Rakefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ require "rake/testtask"
55
Rake::TestTask.new(:test) do |t|
66
t.libs << "test"
77
t.libs << "lib"
8-
t.test_files = FileList["test/**/*_test.rb"]
8+
t.test_files = FileList["test/cases/**/*_test.rb"]
99
t.verbose = false
1010
t.warning = false
1111
end

bin/_console

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env ruby
2+
3+
require "bundler/setup"
4+
require "lambdakiq"
5+
6+
# You can add fixtures and/or initialization code here to make experimenting
7+
# with your gem easier. You can also use a different console, if you like.
8+
9+
# (If you use this, don't forget to add pry to your Gemfile!)
10+
# require "pry"
11+
# Pry.start
12+
13+
require "irb"
14+
IRB.start(__FILE__)

bin/console

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
1-
#!/usr/bin/env ruby
1+
#!/bin/bash
2+
set -e
23

3-
require "bundler/setup"
4-
require "lambdakiq"
5-
6-
# You can add fixtures and/or initialization code here to make experimenting
7-
# with your gem easier. You can also use a different console, if you like.
8-
9-
# (If you use this, don't forget to add pry to your Gemfile!)
10-
# require "pry"
11-
# Pry.start
12-
13-
require "irb"
14-
IRB.start(__FILE__)
4+
docker-compose run \
5+
lambdakiqgem \
6+
./bin/_console

images/Lambdakiq.png

28.6 KB
Loading

images/Lambdakiq.sketch

200 KB
Binary file not shown.

0 commit comments

Comments
 (0)