funktor 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +6 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +7 -0
  7. data/Gemfile.lock +84 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +154 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/exe/funktor +13 -0
  14. data/exe/funktor-deploy +8 -0
  15. data/funktor.gemspec +38 -0
  16. data/lib/funktor.rb +63 -0
  17. data/lib/funktor/active_job_handler.rb +52 -0
  18. data/lib/funktor/aws/sqs/event.rb +20 -0
  19. data/lib/funktor/aws/sqs/record.rb +14 -0
  20. data/lib/funktor/cli/application.rb +23 -0
  21. data/lib/funktor/cli/bootstrap.rb +35 -0
  22. data/lib/funktor/cli/generate.rb +0 -0
  23. data/lib/funktor/cli/generate/base.rb +13 -0
  24. data/lib/funktor/cli/generate/work_queue.rb +25 -0
  25. data/lib/funktor/cli/init.rb +78 -0
  26. data/lib/funktor/cli/templates/Gemfile +9 -0
  27. data/lib/funktor/cli/templates/config/environment.yml +4 -0
  28. data/lib/funktor/cli/templates/config/funktor.yml +51 -0
  29. data/lib/funktor/cli/templates/config/package.yml +9 -0
  30. data/lib/funktor/cli/templates/config/ruby_layer.yml +11 -0
  31. data/lib/funktor/cli/templates/function_definitions/active_job_handler.yml +11 -0
  32. data/lib/funktor/cli/templates/function_definitions/incoming_job_handler.yml +11 -0
  33. data/lib/funktor/cli/templates/funktor.yml.tt +51 -0
  34. data/lib/funktor/cli/templates/gitignore +2 -0
  35. data/lib/funktor/cli/templates/handlers/active_job_handler.rb +17 -0
  36. data/lib/funktor/cli/templates/handlers/incoming_job_handler.rb +8 -0
  37. data/lib/funktor/cli/templates/iam_permissions/active_job_queue.yml +8 -0
  38. data/lib/funktor/cli/templates/iam_permissions/incoming_job_queue.yml +8 -0
  39. data/lib/funktor/cli/templates/iam_permissions/ssm.yml +5 -0
  40. data/lib/funktor/cli/templates/package.json +1 -0
  41. data/lib/funktor/cli/templates/resources/active_job_queue.yml +22 -0
  42. data/lib/funktor/cli/templates/resources/cloudwatch_dashboard.yml +518 -0
  43. data/lib/funktor/cli/templates/resources/incoming_job_queue.yml +22 -0
  44. data/lib/funktor/cli/templates/resources/incoming_job_queue_user.yml +26 -0
  45. data/lib/funktor/cli/templates/serverless.yml +54 -0
  46. data/lib/funktor/cli/templates/workers/hello_worker.rb +8 -0
  47. data/lib/funktor/deploy/cli.rb +42 -0
  48. data/lib/funktor/deploy/serverless.rb +60 -0
  49. data/lib/funktor/deploy/serverless_templates/serverless.yml +156 -0
  50. data/lib/funktor/fake_job_queue.rb +15 -0
  51. data/lib/funktor/incoming_job_handler.rb +39 -0
  52. data/lib/funktor/job.rb +76 -0
  53. data/lib/funktor/middleware/metrics.rb +51 -0
  54. data/lib/funktor/middleware_chain.rb +62 -0
  55. data/lib/funktor/testing.rb +69 -0
  56. data/lib/funktor/version.rb +3 -0
  57. data/lib/funktor/worker.rb +86 -0
  58. metadata +173 -0
@@ -0,0 +1,22 @@
1
+ Resources:
2
+ IncomingJobQueue:
3
+ Type: AWS::SQS::Queue
4
+ Properties:
5
+ QueueName: ${self:custom.funktor.incomingJobQueueName}
6
+ VisibilityTimeout: 300
7
+ RedrivePolicy:
8
+ deadLetterTargetArn:
9
+ "Fn::GetAtt": [ IncomingJobDeadLetterQueue, Arn ]
10
+ maxReceiveCount: 5
11
+ IncomingJobDeadLetterQueue:
12
+ Type: AWS::SQS::Queue
13
+ Properties:
14
+ QueueName: ${self:custom.funktor.incomingDeadJobQueueName}
15
+
16
+ Outputs:
17
+ IncomingJobQueueUrl:
18
+ Value:
19
+ Ref: IncomingJobQueue
20
+ IncomingJobDeadLetterQueueUrl:
21
+ Value:
22
+ Ref: IncomingJobDeadLetterQueue
@@ -0,0 +1,26 @@
1
+ Resources:
2
+ IncomingJobQueueUser:
3
+ Type: AWS::IAM::User
4
+ Properties:
5
+ Policies:
6
+ - PolicyName: incoming-job-queue-access
7
+ PolicyDocument:
8
+ Version: '2012-10-17'
9
+ Statement:
10
+ - Effect: Allow
11
+ Action:
12
+ - sqs:*
13
+ Resource:
14
+ - "Fn::GetAtt": [ IncomingJobQueue, Arn ]
15
+
16
+ IncomingJobQueueUserAccessKey:
17
+ Type: AWS::IAM::AccessKey
18
+ Properties:
19
+ UserName: !Ref IncomingJobQueueUser
20
+
21
+
22
+ Outputs:
23
+ AccessKeyID:
24
+ Value: !Ref IncomingJobQueueUserAccessKey
25
+ SecretAccessKey:
26
+ Value: !GetAtt IncomingJobQueueUserAccessKey.SecretAccessKey
@@ -0,0 +1,54 @@
1
+ # Welcome to Funktor & Serverless!
2
+ #
3
+ # This file is the main config file for your service.
4
+ # It's already configured to run Funktor, you just have to deploy it.
5
+ #
6
+ # For more info about Funktor:
7
+ # TODO
8
+ #
9
+ # For more about serverless, check their docs:
10
+ # docs.serverless.com
11
+ #
12
+ # Happy Coding!
13
+
14
+ # The name of your service. All your AWS resources will contain this name.
15
+ service: yourapp-funktor
16
+
17
+ # This causes serverless to throw an error early if the config is bad, instead of waiting for CloudFormation to try and fail to deploy it.
18
+ configValidationMode: error
19
+
20
+ # Pin the serverless framework to the 2.x line
21
+ frameworkVersion: '2'
22
+
23
+ provider:
24
+ name: aws
25
+ runtime: ruby2.7
26
+ lambdaHashingVersion: 20201221
27
+ environment: ${file(config/environment.yml)}
28
+ iamRoleStatements:
29
+ - ${file(iam_permissions/ssm.yml)}
30
+ - ${file(iam_permissions/active_job_queue.yml)}
31
+ - ${file(iam_permissions/incoming_job_queue.yml)}
32
+
33
+
34
+ custom:
35
+ # Our stage is based on what is passed in when running serverless
36
+ # commands. Or fallsback to what we have set in the provider section.
37
+ stage: ${self:provider.stage, 'dev'}
38
+ funktor: ${file(config/funktor.yml)}
39
+ rubyLayer: ${file(config/ruby_layer.yml)}
40
+
41
+ package: ${file(config/package.yml)}
42
+
43
+ functions:
44
+ incomingJobHandler: ${file(function_definitions/incoming_job_handler.yml)}
45
+ activeJobHandler: ${file(function_definitions/active_job_handler.yml)}
46
+
47
+ resources:
48
+ - ${file(resources/active_job_queue.yml)}
49
+ - ${file(resources/incoming_job_queue.yml)}
50
+ - ${file(resources/incoming_job_queue_user.yml)}
51
+ - ${file(resources/cloudwatch_dashboard.yml)}
52
+
53
+ plugins:
54
+ - /Users/jgreen/projects/serverless-ruby-layer
@@ -0,0 +1,8 @@
1
+ class HelloWorker
2
+ include Funktor::Worker
3
+
4
+ def perform(*args)
5
+ puts "Greetings from the HelloWorker!"
6
+ end
7
+ end
8
+
@@ -0,0 +1,42 @@
1
+ require 'optparse'
2
+ require 'funktor/deploy/serverless'
3
+
4
+ module Funktor
5
+ module Deploy
6
+ class CLI
7
+ attr_reader :options
8
+ def initialize
9
+ @options = {
10
+ verbose: false,
11
+ file: 'funktor.yml',
12
+ tmp_dir_prefix: '.funktor',
13
+ stage: 'dev'
14
+ }
15
+ end
16
+
17
+ def parse(argv = ARGV)
18
+ OptionParser.new do |opts|
19
+ opts.on('-v', '--verbose', 'Display verbose output') do |verbose|
20
+ options[:verbose] = verbose
21
+ end
22
+ opts.on('-f', '--file=FILE', 'The path to the funktor.yml file to deploy') do |file|
23
+ options[:file] = file
24
+ end
25
+ opts.on('-t', '--tmp_dir_prefix=TMP_DIR_PREFIX', 'The prefix for the tmp dir. The stage will be appended.') do |tmp_dir_prefix|
26
+ options[:tmp_dir_prefix] = tmp_dir_prefix
27
+ end
28
+ opts.on('-s', '--stage=STAGE', 'The stage to deploy to. Defaults to "dev"') do |stage|
29
+ options[:stage] = stage
30
+ end
31
+ opts.on('-h') { puts opts; exit }
32
+ opts.parse!(argv)
33
+ end
34
+ end
35
+
36
+ def run
37
+ Funktor::Deploy::Serverless.new(**options).call
38
+ end
39
+ end
40
+ end
41
+ end
42
+
@@ -0,0 +1,60 @@
1
+ require 'yaml'
2
+
3
+ module Funktor
4
+ module Deploy
5
+ class Serverless
6
+ attr_accessor :file, :tmp_dir_prefix, :verbose, :stage
7
+ def initialize(file:, tmp_dir_prefix:, verbose:, stage:)
8
+ @file = file
9
+ @tmp_dir_prefix = tmp_dir_prefix
10
+ @verbose = verbose
11
+ @stage = stage
12
+ end
13
+
14
+ def call
15
+ #puts "deploying file #{file} via tmp_dir_prefix #{tmp_dir_prefix} for stage #{stage}"
16
+ make_tmp_dir
17
+ create_serverless_file
18
+ end
19
+
20
+ def funktor_data
21
+ @funktor_data ||= squash_hash(YAML.load_file(file))
22
+ end
23
+
24
+ def squash_hash(hsh, stack=[])
25
+ hsh.reduce({}) do |res, (key, val)|
26
+ next_stack = [ *stack, key ]
27
+ if val.is_a?(Hash)
28
+ next res.merge(squash_hash(val, next_stack))
29
+ end
30
+ res.merge(next_stack.join(".").to_sym => val)
31
+ end
32
+ end
33
+
34
+ def make_tmp_dir
35
+ FileUtils.mkdir_p tmp_dir
36
+ end
37
+
38
+ def tmp_dir
39
+ "#{tmp_dir_prefix}_#{stage}"
40
+ end
41
+
42
+ def create_serverless_file
43
+ #puts "funktor_data = "
44
+ #puts funktor_data
45
+ template_source = File.open(serverless_file_source).read
46
+ file_content = template_source % funktor_data
47
+ File.open(serverless_file_destination, 'w') { |file| file.write(file_content) }
48
+ end
49
+
50
+ def serverless_file_source
51
+ File.expand_path("../serverless_templates/serverless.yml", __FILE__)
52
+ end
53
+
54
+ def serverless_file_destination
55
+ File.join tmp_dir, 'serverless.yml'
56
+ end
57
+ end
58
+ end
59
+ end
60
+
@@ -0,0 +1,156 @@
1
+ # WARNING : You probably don't want to mess with this file directly.
2
+ service: %{stack_name}
3
+ # app and org for use with dashboard.serverless.com
4
+ #app: your-app-name
5
+ #org: your-org-name
6
+
7
+ frameworkVersion: '2'
8
+
9
+ provider:
10
+ name: aws
11
+ runtime: %{runtime}
12
+ lambdaHashingVersion: 20201221
13
+ ecr:
14
+ images:
15
+ funktorimage:
16
+ path: ./
17
+ # TODO : Expose buildArgs via funktor.yml?
18
+ buildArgs:
19
+ BUNDLE_GEM__FURY__IO: ${env:BUNDLE_GEM__FURY__IO}
20
+ # TODO : Expose environment to funktor.yml?
21
+ environment:
22
+ FUNKTOR_INCOMING_JOB_QUEUE:
23
+ Ref: IncomingJobQueue
24
+ FUNKTOR_ACTIVE_JOB_QUEUE:
25
+ Ref: ActiveJobQueue
26
+ FUNKTOR_DELAYED_JOB_TABLE:
27
+ Ref: DelayedJobTable
28
+ delayedJobTable:
29
+ Ref: DelayedJobTable
30
+ iamRoleStatements:
31
+ - Effect: Allow
32
+ Action:
33
+ - ssm:Get*
34
+ Resource:
35
+ - '*' # TODO : This should probably be more selective...
36
+ - Effect: Allow
37
+ Action:
38
+ - sqs:ReceiveMessage
39
+ - sqs:DeleteMessage
40
+ - sqs:SendMessage
41
+ - sqs:GetQueueAttributes
42
+ Resource:
43
+ - "Fn::GetAtt": [ ActiveJobQueue, Arn ]
44
+ - Effect: Allow
45
+ Action:
46
+ - sqs:ReceiveMessage
47
+ - sqs:DeleteMessage
48
+ - sqs:SendMessage
49
+ - sqs:GetQueueAttributes
50
+ Resource:
51
+ - "Fn::GetAtt": [ IncomingJobQueue, Arn ]
52
+ - Effect: Allow
53
+ Action:
54
+ - sqs:ReceiveMessage
55
+ - sqs:DeleteMessage
56
+ - sqs:SendMessage
57
+ - sqs:GetQueueAttributes
58
+ Resource:
59
+ - "Fn::GetAtt": [ ActivityQueue, Arn ]
60
+ - Effect: Allow
61
+ Action:
62
+ - dynamodb:PutItem
63
+ - dynamodb:DeleteItem
64
+ Resource:
65
+ - "Fn::GetAtt": [ DelayedJobTable, Arn ]
66
+ - Effect: Allow
67
+ Action:
68
+ - dynamodb:*
69
+ Resource:
70
+ - "Fn::GetAtt": [ StatsTable, Arn ]
71
+ - Effect: Allow
72
+ Action:
73
+ - dynamodb:Query
74
+ Resource:
75
+ Fn::Join:
76
+ - ""
77
+ - - "Fn::GetAtt": [ DelayedJobTable, Arn ]
78
+ - "/index/performAtIndex"
79
+
80
+ custom:
81
+ # Our stage is based on what is passed in when running serverless
82
+ # commands. Or fallsback to what we have set in the provider section.
83
+ stage: ${opt:stage, 'dev'}
84
+ incomingJobQueueName: ${self:service}-${self:custom.stage}-incoming-jobs
85
+ incomingJobQueueAccessPolicyName: ${self:service}-${self:custom.stage}-incoming-job-queue-access
86
+ incomingDeadJobQueueName: ${self:service}-${self:custom.stage}-incoming-dead-jobs
87
+ activeJobQueueName: ${self:service}-${self:custom.stage}-active-jobs
88
+ activityQueueName: ${self:service}-${self:custom.stage}-activity
89
+ activityDeadQueueName: ${self:service}-${self:custom.stage}-activity-dead
90
+ deadJobQueueName: ${self:service}-${self:custom.stage}-dead-jobs
91
+ delayedJobTableName: ${self:service}-${self:custom.stage}-delayed-jobs
92
+ statsTableName: ${self:service}-${self:custom.stage}-stats
93
+ dashboardName: ${self:service}-${self:custom.stage}-dashboard
94
+
95
+ # you can define service wide environment variables here
96
+ # environment:
97
+ # variable1: value1
98
+
99
+ functions:
100
+ # TODO - How could other functions be passed in from funktor.yml?
101
+ #random_job_generator:
102
+ ##handler: lambda_handlers/random_job_generator.RandomJobGenerator.call
103
+ #image:
104
+ #name: funktorimage
105
+ #command:
106
+ #- lambda_handlers/random_job_generator.RandomJobGenerator.call
107
+ #timeout: 170
108
+ #reservedConcurrency: 0
109
+ #events:
110
+ #- schedule: rate(1 minute)
111
+
112
+ delayed_job_activator:
113
+ image:
114
+ name: funktorimage
115
+ command:
116
+ - lambda_handlers/delayed_job_activator.call
117
+ timeout: %{delayed_job_activator.timeout_in_seconds}
118
+ # TODO - handle memory and concurrency
119
+ #reservedConcurrency: 1
120
+ events:
121
+ - schedule: %{delayed_job_activator.execution_schedule}
122
+
123
+ incoming_job_handler:
124
+ image:
125
+ name: funktorimage
126
+ command:
127
+ - lambda_handlers/incoming_job_handler.call
128
+ timeout: %{incoming_job_handler.timeout_in_seconds}
129
+ events:
130
+ - sqs:
131
+ arn:
132
+ Fn::GetAtt:
133
+ - IncomingJobQueue
134
+ - Arn
135
+
136
+ # TODO - We need one of these per work queue
137
+ active_job_handler:
138
+ image:
139
+ name: funktorimage
140
+ command:
141
+ - lambda_handlers/active_job_handler.call
142
+ timeout: 300
143
+ events:
144
+ - sqs:
145
+ arn:
146
+ Fn::GetAtt:
147
+ - ActiveJobQueue
148
+ - Arn
149
+
150
+ # you can add CloudFormation resource templates here
151
+ resources:
152
+ - ${file(resources/sqs-queue.yml)}
153
+ - ${file(resources/sqs-incoming-user.yml)}
154
+ - ${file(resources/dynamodb-table.yml)}
155
+ - ${file(resources/cloudfront-dashboard.yml)}
156
+
@@ -0,0 +1,15 @@
1
+ module Funktor
2
+ module FakeJobQueue
3
+ def self.push(worker, payload)
4
+ jobs[worker.name].push({worker: worker, payload: payload})
5
+ end
6
+
7
+ def self.jobs
8
+ @jobs ||= Hash.new { |hash, key| hash[key] = [] }
9
+ end
10
+
11
+ def self.clear_all
12
+ jobs.clear
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,39 @@
1
+ require 'aws-sdk-sqs'
2
+
3
+ module Funktor
4
+ class IncomingJobHandler
5
+
6
+ def call(event:, context:)
7
+ event = Funktor::Aws::Sqs::Event.new(event)
8
+ puts "event.jobs.count = #{event.jobs.count}"
9
+ event.jobs.each do |job|
10
+ dispatch(job)
11
+ end
12
+ end
13
+
14
+ def sqs_client
15
+ @sqs_client ||= ::Aws::SQS::Client.new
16
+ end
17
+
18
+ def dispatch(job)
19
+ Funktor.incoming_job_handler_middleware.invoke(job) do
20
+ puts "pushing to active_job_queue for delay = #{job.delay}"
21
+ push_to_active_job_queue(job)
22
+ end
23
+ end
24
+
25
+ def active_job_queue
26
+ ENV['FUNKTOR_ACTIVE_JOB_QUEUE']
27
+ end
28
+
29
+ def push_to_active_job_queue(job)
30
+ sqs_client.send_message({
31
+ # TODO : How to get this URL...
32
+ queue_url: active_job_queue,
33
+ message_body: job.to_json,
34
+ delay_seconds: job.delay
35
+ })
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,76 @@
1
+ module Funktor
2
+ class Job
3
+ attr_accessor :job_string
4
+ attr_accessor :job_data
5
+ def initialize(job_string)
6
+ @job_string = job_string
7
+ end
8
+
9
+ def job_data
10
+ @job_data ||= Funktor.parse_json(job_string)
11
+ end
12
+
13
+ def worker_class_name
14
+ job_data["worker"]
15
+ end
16
+
17
+ def job_id
18
+ job_data["job_id"]
19
+ end
20
+
21
+ def worker_params
22
+ job_data["worker_params"]
23
+ end
24
+
25
+ def retries
26
+ job_data["retries"] || 0
27
+ end
28
+
29
+ def retries=(retries)
30
+ job_data["retries"] = retries
31
+ end
32
+
33
+ def delay
34
+ job_data["delay"]
35
+ end
36
+
37
+ def delay=(delay)
38
+ job_data["delay"] = delay
39
+ end
40
+
41
+ def execute
42
+ worker_class.new.perform(worker_params)
43
+ end
44
+
45
+ def worker_class
46
+ @klass ||= Object.const_get worker_class_name
47
+ end
48
+
49
+ def increment_retries
50
+ self.retries ||= 0
51
+ self.retries += 1
52
+ self.delay = seconds_to_delay(retries)
53
+ end
54
+
55
+ # delayed_job and sidekiq use the same basic formula
56
+ def seconds_to_delay(count)
57
+ (count**4) + 15 + (rand(30) * (count + 1))
58
+ end
59
+
60
+ def to_json(arg = nil)
61
+ Funktor.dump_json(job_data)
62
+ end
63
+
64
+ def retry_limit
65
+ 25
66
+ end
67
+
68
+ def can_retry
69
+ self.retries < retry_limit
70
+ end
71
+
72
+ def retry_queue_url
73
+ worker_class&.custom_queue_url || ENV['FUNKTOR_INCOMING_JOB_QUEUE']
74
+ end
75
+ end
76
+ end