active_job_k8s 0.1.0 → 0.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47a17202dbfd72dab2e898ae6097df0d9f9191de31d05119e6ff4190f4038ea9
4
- data.tar.gz: 961f6548436a79621c58b8da0a5d078e14a0db4a2517ec701a43bbed3ea5da87
3
+ metadata.gz: 57feaec27f10c9503c7e9455b30526d080b446eb9194f93a7a20e8830b3f3ceb
4
+ data.tar.gz: a1447113bfcdae14dae4e870e859df4997ae1ab8d6a01dc3dd11351301a7a165
5
5
  SHA512:
6
- metadata.gz: aaad5ff2c9f345e177a7b0017d91fe68e6b064392ecdf7d4c174714256d0ba2cacfeae271102845265ecb9979aafb6495859d6831694d4de5f683ead8134dbc9
7
- data.tar.gz: 2e979a40fde5148b72fa00ebe1ab442b570b3ca9ff843a8b77f2f720ef1108423382f72a3c138caf30705f5ae58e149ca9d8543d48a65c4bfb133bdc1cbdc95c
6
+ metadata.gz: feb62f548aaeffade90f53014060463959c3d672d674f348a82474271291b2f7d00e3bb68a6398a26ff7a77de04b7d137df633ad42141bd1623fbc2b785024bf
7
+ data.tar.gz: 203f78073d6ce7667ab48facf91262b6cab587142ce8afe1b557220e64dcd574ff093ee160e38e6f1eccbe1b4d23177718b524b0bd13eb548ce75282f35f133b
data/.gitignore CHANGED
@@ -9,4 +9,5 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
- spec/dummy
12
+ spec/dummy
13
+ *.gem
data/README.md CHANGED
@@ -4,7 +4,7 @@ WIP gem to make active job work with kubernetes jobs.
4
4
 
5
5
  Roadmap for V1.0:
6
6
  - [x] ActiveJob.perform_later create a Job in k8s that will execute the job
7
- - [ ] ActiveJob.perform_later with delay create a Job in k8s in suspended mode,
7
+ - [x] ActiveJob.perform_later with delay create a Job in k8s in suspended mode,
8
8
  a task will enable it as soon as the time is reached
9
9
  - [ ] Limiting the number of concurrent jobs (if there are more jobs they will be created in suspended mode)
10
10
 
@@ -66,8 +66,33 @@ Rails.application.configure do
66
66
  namespace
67
67
  )
68
68
 
69
+ default_manifest = YAML.safe_load(
70
+ <<~MANIFEST
71
+ apiVersion: batch/v1
72
+ kind: Job
73
+ metadata:
74
+ name: scheduled-job-name
75
+ namespace: default
76
+ spec:
77
+ template:
78
+ spec:
79
+ restartPolicy: Never
80
+ containers:
81
+ - name: app-job
82
+ image: image_of_the_rails_application
83
+ imagePullPolicy: IfNotPresent
84
+ command:
85
+ - /bin/bash
86
+ args:
87
+ - '-c'
88
+ - bundle exec rails active_job_k8s:run_job
89
+
90
+ MANIFEST
91
+ )
92
+
69
93
  config.active_job.queue_adapter = ActiveJob::QueueAdapters::K8sAdapter.new(
70
- kubeclient_context: context
94
+ kubeclient_context: context,
95
+ default_manifest: default_manifest
71
96
  )
72
97
 
73
98
  end
@@ -123,27 +148,12 @@ class HelloWorldJob < ApplicationJob
123
148
  def perform(*args)
124
149
  Rails.logger.debug "Hello World"
125
150
  end
126
-
127
- def manifest
128
- YAML.safe_load(
129
- <<~MANIFEST
130
- apiVersion: batch/v1
131
- kind: Job
132
- metadata:
133
- name: scheduled-job-name
134
- namespace: default
135
- spec:
136
- template:
137
- spec:
138
- restartPolicy: Never
139
- containers:
140
- - name: app-job
141
- image: image_of_the_rails_application
142
- imagePullPolicy: IfNotPresent
143
-
144
- MANIFEST
145
- )
146
- end
151
+
152
+ #[Optional] if not present it will be taken from scheduler configuration
153
+ # def manifest
154
+ # .....
155
+ # end
156
+
147
157
  end
148
158
  ```
149
159
 
@@ -9,13 +9,13 @@ Gem::Specification.new do |spec|
9
9
  spec.summary = "ActiveJob adapter for kubernetes job"
10
10
  spec.homepage = "https://github.com/oniram88/active_job_k8s"
11
11
  spec.license = "MIT"
12
- spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
12
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
13
13
 
14
14
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
15
15
 
16
16
  spec.metadata["homepage_uri"] = spec.homepage
17
17
  spec.metadata["source_code_uri"] = spec.homepage
18
- spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
18
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
19
19
 
20
20
  # Specify which files should be added to the gem when it is released.
21
21
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
data/bin/setup CHANGED
@@ -11,21 +11,35 @@ bundle exec rails new spec/dummy --skip-git --skip-javascript --skip-hotwire --s
11
11
 
12
12
  echo "Changes to application"
13
13
 
14
- sed -i 's/"2.7.4"/">= 2.7.4"/' spec/dummy/Gemfile
14
+ sed -i 's/2.7.4/>= 2.7.4/' spec/dummy/Gemfile
15
15
  sed -i '/end/i root "main#index"' spec/dummy/config/routes.rb
16
16
  sed -i '/root "main#index"/i get "create_job", to: "main#create_job", as: "create_job"' spec/dummy/config/routes.rb
17
+ sed -i '/root "main#index"/i get "send_email", to: "main#send_email", as: "send_email"' spec/dummy/config/routes.rb
17
18
 
18
19
  echo '
19
20
  class MainController < ApplicationController
20
21
  include ActionView::Helpers::UrlHelper
21
- def index
22
- render inline: "#{link_to HelloWorldJob.name,create_job_path}"
23
- end
24
22
 
25
- def create_job
26
- HelloWorldJob.perform_later
27
- redirect_to root_path
28
- end
23
+ def index
24
+ render inline: "#{link_to HelloWorldJob.name, create_job_path}<br>
25
+ #{link_to HelloWorldJob.name, create_job_path(wait: 1)}<br>
26
+ #{link_to SubscriptionMailer, send_email_path}"
27
+ end
28
+
29
+ def create_job
30
+ j = HelloWorldJob
31
+ if params[:wait]
32
+ j = j.set(wait: params[:wait].to_i.minutes)
33
+ end
34
+ j.perform_later
35
+
36
+ redirect_to root_path
37
+ end
38
+
39
+ def send_email
40
+ SubscriptionMailer.notify.deliver_later
41
+ redirect_to root_path
42
+ end
29
43
 
30
44
  end
31
45
  ' > spec/dummy/app/controllers/main_controller.rb
@@ -52,8 +66,46 @@ Rails.application.configure do
52
66
  namespace
53
67
  )
54
68
 
69
+ default_manifest = YAML.safe_load(
70
+ <<~MANIFEST
71
+ apiVersion: batch/v1
72
+ kind: Job
73
+ metadata:
74
+ name: scheduled-job-name
75
+ namespace: default
76
+ spec:
77
+ template:
78
+ spec:
79
+ restartPolicy: Never
80
+ containers:
81
+ - name: app-job
82
+ image: activejobk8s:0.1.0
83
+ imagePullPolicy: IfNotPresent
84
+ env:
85
+ - name: RAILS_SERVE_STATIC_FILES
86
+ value: "1"
87
+ - name: RAILS_LOG_TO_STDOUT
88
+ value: "1"
89
+ - name: RAILS_ENV
90
+ value: "development"
91
+ command: ["/bin/sh"]
92
+ args:
93
+ - "-ec"
94
+ - "bundle install && bundle exec rails active_job_k8s:run_job"
95
+ volumeMounts:
96
+ - mountPath: /usr/src/app
97
+ name: application-volume
98
+ volumes:
99
+ - name: application-volume
100
+ hostPath:
101
+ # directory location on host
102
+ path: /application
103
+ MANIFEST
104
+ )
105
+
55
106
  config.active_job.queue_adapter = ActiveJob::QueueAdapters::K8sAdapter.new(
56
- kubeclient_context: context
107
+ kubeclient_context: context,
108
+ default_manifest: default_manifest
57
109
  )
58
110
 
59
111
  end
@@ -67,48 +119,16 @@ class HelloWorldJob < ApplicationJob
67
119
  Rails.logger.debug "Hello World"
68
120
  FileUtils.touch(Rails.root.join("tmp","#{SecureRandom.hex}.test"))
69
121
  end
122
+ end
123
+ ' > spec/dummy/app/jobs/hello_world_job.rb
70
124
 
71
- def manifest
72
- YAML.safe_load(
73
- <<~MANIFEST
74
- apiVersion: batch/v1
75
- kind: Job
76
- metadata:
77
- name: scheduled-job-name
78
- namespace: default
79
- spec:
80
- template:
81
- spec:
82
- restartPolicy: Never
83
- containers:
84
- - name: app-job
85
- image: activejobk8s:0.1.0
86
- imagePullPolicy: IfNotPresent
87
- env:
88
- - name: RAILS_SERVE_STATIC_FILES
89
- value: "1"
90
- - name: RAILS_LOG_TO_STDOUT
91
- value: "1"
92
- - name: RAILS_ENV
93
- value: "development"
94
- command: ["/bin/sh"]
95
- args:
96
- - "-ec"
97
- - "bundle install && bundle exec rails active_job_k8s:run_job"
98
- volumeMounts:
99
- - mountPath: /usr/src/app
100
- name: application-volume
101
- volumes:
102
- - name: application-volume
103
- hostPath:
104
- # directory location on host
105
- path: /application
106
-
107
- MANIFEST
108
- )
125
+ echo '
126
+ class SubscriptionMailer < ApplicationMailer
127
+ def notify
128
+ mail(to: "test@domain.tld", subject: "test", body: "message")
109
129
  end
110
130
  end
111
- ' > spec/dummy/app/jobs/hello_world_job.rb
131
+ ' > spec/dummy/app/mailers/subscription_mailer.rb
112
132
 
113
133
  echo 'FROM ruby:3.0.4-alpine3.16
114
134
 
@@ -16,12 +16,10 @@ module ActiveJob
16
16
  scheduler.create_job(job)
17
17
  end
18
18
 
19
- def enqueue_at(*)
20
- # :nodoc:
21
- raise NotImplementedError, "It will be implemented in the future. Read the roadmap"
19
+ def enqueue_at(job,scheduled_at)
20
+ scheduler.create_job(job,scheduled_at:scheduled_at)
22
21
  end
23
22
 
24
-
25
23
  end
26
24
  end
27
25
  end
@@ -5,5 +5,17 @@ module ActiveJobK8s
5
5
  rake_tasks do
6
6
  require 'active_job_k8s/tasks'
7
7
  end
8
+
9
+ server do
10
+ puts "=> START ActiveJobK8s - ControlLoop sleep [5]"
11
+ Thread.new do
12
+ Rails.application.reloader.wrap do
13
+ loop do
14
+ Rails.application.config.active_job.queue_adapter.scheduler.un_suspend_jobs
15
+ sleep 5
16
+ end
17
+ end
18
+ end
19
+ end
8
20
  end
9
21
  end
@@ -5,26 +5,32 @@ require "json"
5
5
  module ActiveJobK8s
6
6
  class Scheduler
7
7
 
8
+ #@return [Kubeclient::Config::Context]
8
9
  attr_reader :kubeclient_context
9
10
 
10
- # @param [Hash{ kubeclient_context: [Kubeclient::Config::Context] }] opts
11
+ #@return [Hash]
12
+ attr_reader :default_manifest
13
+
14
+ # @param [Hash{ kubeclient_context: Kubeclient::Config::Context, default_manifest: Hash }] opts
11
15
  def initialize(**opts)
12
16
  raise "No KubeClientContext given" if opts[:kubeclient_context].nil?
13
17
  # or to use a specific context, by name:
14
18
  @kubeclient_context = opts[:kubeclient_context]
19
+ @default_manifest = opts[:default_manifest] || {}
15
20
 
16
21
  end
17
22
 
18
- def create_job(job)
23
+ def create_job(job, scheduled_at: nil)
19
24
 
20
25
  serialized_job = JSON.dump(job.serialize)
21
26
 
22
- kube_job = Kubeclient::Resource.new(job.manifest)
27
+ manifest = (job.respond_to?(:manifest) and job.manifest.is_a?(Hash) and !job.manifest.empty?) ? job.manifest : default_manifest
28
+ kube_job = Kubeclient::Resource.new(manifest)
23
29
 
24
- # kube_job.spec.suspend = false FIXME complete for delayed jobs
25
30
  kube_job.metadata.name = "#{kube_job.metadata.name}-#{job.job_id}"
26
- kube_job.metadata.job_id = job.job_id
27
- kube_job.metadata.queue_name = job.queue_name
31
+ kube_job.metadata.annotations ||= {}
32
+ kube_job.metadata.annotations.job_id = job.job_id
33
+ kube_job.metadata.annotations.queue_name = job.queue_name
28
34
  kube_job.metadata.namespace = kube_job.metadata.namespace || kubeclient_context.namespace
29
35
  kube_job.spec.template.spec.containers.map do |container|
30
36
  container.env ||= []
@@ -40,6 +46,14 @@ module ActiveJobK8s
40
46
  end
41
47
  kube_job.spec.ttlSecondsAfterFinished = 300 #number of seconds the job will be erased
42
48
 
49
+ kube_job.metadata.labels ||= {}
50
+ if scheduled_at
51
+ kube_job.spec.suspend = true
52
+ kube_job.metadata.annotations.scheduled_at = scheduled_at.to_s
53
+ kube_job.metadata.labels.activeJobK8s = "scheduled" # job to be execute when time comes
54
+ else
55
+ kube_job.metadata.labels.activeJobK8s = "delayed" # job to be execute when possible
56
+ end
43
57
  client.create_job(kube_job)
44
58
  end
45
59
 
@@ -47,6 +61,17 @@ module ActiveJobK8s
47
61
  ActiveJob::Base.execute(JSON.parse(ENV['SERIALIZED_JOB']))
48
62
  end
49
63
 
64
+ ##
65
+ # Un-suspend jobs if the scheduled at is outdated
66
+ def un_suspend_jobs
67
+ suspended_jobs.each do |sj|
68
+ scheduled_at = Time.at(sj.metadata.annotations.scheduled_at.to_f)
69
+ if Time.now > scheduled_at and sj.spec.suspend
70
+ client.patch_job(sj.metadata.name, { spec: { suspend: false } }, sj.metadata.namespace).inspect
71
+ end
72
+ end
73
+ end
74
+
50
75
  protected
51
76
 
52
77
  def client
@@ -55,6 +80,16 @@ module ActiveJobK8s
55
80
  ssl_options: @kubeclient_context.ssl_options,
56
81
  auth_options: @kubeclient_context.auth_options)
57
82
  end
83
+
84
+ private
85
+
86
+ ##
87
+ # Internal list of all suspended jobs
88
+ def suspended_jobs
89
+ client.get_jobs(namespace: kubeclient_context.namespace,
90
+ label_selector: "activeJobK8s=scheduled",
91
+ field_selector: 'status.successful!=1')
92
+ end
58
93
  end
59
94
 
60
95
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveJobK8s
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_job_k8s
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marino Bonetti
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-10-30 00:00:00.000000000 Z
11
+ date: 2022-11-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: kubeclient
@@ -79,7 +79,7 @@ metadata:
79
79
  allowed_push_host: https://rubygems.org
80
80
  homepage_uri: https://github.com/oniram88/active_job_k8s
81
81
  source_code_uri: https://github.com/oniram88/active_job_k8s
82
- changelog_uri: https://github.com/oniram88/active_job_k8s/CHANGELOG.md
82
+ changelog_uri: https://github.com/oniram88/active_job_k8s/blob/master/CHANGELOG.md
83
83
  post_install_message:
84
84
  rdoc_options: []
85
85
  require_paths:
@@ -88,7 +88,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
88
88
  requirements:
89
89
  - - ">="
90
90
  - !ruby/object:Gem::Version
91
- version: 2.3.0
91
+ version: 2.6.0
92
92
  required_rubygems_version: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - ">="