funktor 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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