jobbie_oci 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 75785a941b0a3e17af5387fbd61e775a881018a4
4
+ data.tar.gz: f9d3b9416587e292b184efb0015a6a2d5e8ccf41
5
+ SHA512:
6
+ metadata.gz: 19e62e7a8ef1808507143e01cbbb97524b65b34e32485490a204c7a54eef733e1584ecb7f013f029b10e66998bd4d4e233f02a01231835af4e03c908cb06667f
7
+ data.tar.gz: 7b736413ac7b793f5111a95df6cf9d5d6a10b4625699c158b6a810bfd127a5259a46e59d7d0ec26a3c9d0d05871463e79b20080d2e3d4bb52408bf55e651bb84
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ *.swp
data/.rubocop.yml ADDED
@@ -0,0 +1,10 @@
1
+ Metrics/AbcSize:
2
+ Enabled: false
3
+ Metrics/PerceivedComplexity:
4
+ Enabled: false
5
+ Metrics/CyclomaticComplexity:
6
+ Enabled: false
7
+ Metrics/MethodLength:
8
+ Max: 1000
9
+ Metrics/ClassLength:
10
+ Max: 1000
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in jobbie_oci.gemspec
8
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,20 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ jobbie_oci (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ rake (10.5.0)
10
+
11
+ PLATFORMS
12
+ ruby
13
+
14
+ DEPENDENCIES
15
+ bundler (~> 1.16)
16
+ jobbie_oci!
17
+ rake (~> 10.0)
18
+
19
+ BUNDLED WITH
20
+ 1.16.1
data/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # JobbieOci
2
+
3
+ A very simple job queuing system that uses OCI Casper buckets as a backend.
4
+
5
+ Jobbie is intended to fill a gap in Oracle's OCI cloud whilst there is a lack
6
+ of a queueing service similar to Amazon's SNS/SQS. As soon as such a service
7
+ is introduced, it's unlikely that Jobbie will serve any further purpose.
8
+
9
+ Jobs are added to an 'execution group', which is used to group related
10
+ tasks together for consumption by a daemon. Multiple daemons can run in the
11
+ same execution group, but only one daemon will be able to claim the job. If
12
+ a daemon is unable to complete the job then it will eventually time out and
13
+ become claimable again.
14
+
15
+ ## Installation
16
+
17
+ ```
18
+ gem install jobbie_oci
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Ensure that you have a bucket in Casper and that you have permission to create
24
+ objects in it.
25
+
26
+ Jobbie can be run in one of two modes: Either client mode which can submit jobs
27
+ or list them, or daemon mode which takes jobs from the queue and executes them.
28
+
29
+ To run jobs in daemon mode, you will use:
30
+ ```
31
+ jobbie daemon --exec-group=EXEC_GROUP --script-dir=SCRIPT_DIR
32
+ ```
33
+
34
+ An `execution group` is used to to group related tasks together. Jobs
35
+ submitted in the same execution group will be executed in the same way. For
36
+ example, you might create a job in the "cleanup\_users" exec group, and a
37
+ jobbie daemon with a matching exec-group will be used to execute a script
38
+ to cleanup the user.
39
+
40
+ The script dir is the only directory in which scripts triggered by the
41
+ daemon are allowed to run. No fully qualified paths are allowed in the script
42
+ name.
43
+
44
+ ## Example
45
+
46
+ Suppose we have a script directory called `scripts` and that we add to it a
47
+ script called `cleanup_users`.
48
+
49
+ We could add a job to the queue as follows:
50
+ ```
51
+ jobbie add --bucket my-bucket --exec-group=USERMGMT --script=cleanup_users user123
52
+ ```
53
+
54
+ Meanwhile we could write start a daemon as follows:
55
+ ```
56
+ jobbie daemon --bucket my-bucket --exec-group=USERMGMT --script-dir=scripts
57
+ ```
58
+ .. and the daemon would pick up the job and execute the script as if
59
+ someone were to manually run:
60
+ ```
61
+ ./scripts/cleanup_users user123
62
+ ```
63
+
64
+ ## Contributing
65
+
66
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/jobbie_oci.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ task default: :spec
data/exe/jobbie ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'jobbie_oci/cli'
5
+
6
+ Jobbie::CLI::Main.start(ARGV)
@@ -0,0 +1,26 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ lib = File.expand_path('lib', __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require 'jobbie_oci/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'jobbie_oci'
10
+ spec.version = JobbieOci::VERSION
11
+ spec.authors = ['Stephen Pearson']
12
+ spec.email = ['stephen.pearson@oracle.com']
13
+
14
+ spec.summary = 'Simple noddy job queuer that uses OCI Casper backend'
15
+ spec.homepage = 'http://github.com/stephenpearson/jobbie_oci'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_development_dependency 'bundler', '~> 1.16'
25
+ spec.add_development_dependency 'rake', '~> 10.0'
26
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'oci'
4
+ require 'thor'
5
+
6
+ require 'jobbie_oci/jobs'
7
+
8
+ module Jobbie
9
+ module CLI
10
+ # This is the Main entry point for the CLI
11
+ class Main < Thor
12
+ class_option :region,
13
+ type: :string,
14
+ default: 'phx',
15
+ desc: 'Region in which to store jobs'
16
+ class_option :profile,
17
+ type: :string,
18
+ default: 'DEFAULT',
19
+ desc: 'OCI config profile name'
20
+ class_option :config,
21
+ type: :string,
22
+ default: OCI::ConfigFileLoader::DEFAULT_CONFIG_FILE,
23
+ desc: 'OCI config file location'
24
+ class_option :bucket,
25
+ type: :string,
26
+ required: true
27
+ class_option :verbose,
28
+ type: :boolean,
29
+ aliases: :v,
30
+ default: false
31
+
32
+ desc 'list', 'List queued jobs'
33
+ option :exec_group, type: :string, default: nil
34
+ def list
35
+ job_api = Jobbie::Jobs.new(options)
36
+ job_api.show_job_list(exec_group: options[:exec_group])
37
+ end
38
+
39
+ desc 'add', 'Add job onto the queue'
40
+ option :exec_group, type: :string, required: true
41
+ option :script, type: :string, required: true
42
+ def add(*params)
43
+ job_api = Jobbie::Jobs.new(options)
44
+ job_api.put_job(options[:exec_group], options[:script], params)
45
+ end
46
+
47
+ desc 'daemon', 'Daemon mode: Poll queue for jobs'
48
+ option :exec_group, type: :string, required: true
49
+ option :script_dir, type: :string, required: true
50
+ def daemon
51
+ job_api = Jobbie::Jobs.new(options)
52
+ job_api.daemon(options[:exec_group], options[:script_dir])
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'socket'
5
+ require 'terminal-table'
6
+
7
+ require 'jobbie_oci/oci'
8
+
9
+ module Jobbie
10
+ # Process jobs from the queue
11
+ class Jobs
12
+ attr_reader :options, :oci
13
+
14
+ def initialize(options)
15
+ @options = options
16
+ @oci = Jobbie::OciUtil.new(options)
17
+ end
18
+
19
+ def obj_api
20
+ oci.obj_api
21
+ end
22
+
23
+ def job_list(exec_group: nil)
24
+ unordered_job_list(exec_group: exec_group).sort_by(&:first)
25
+ end
26
+
27
+ def fqdn
28
+ Socket.gethostname
29
+ end
30
+
31
+ def job_details(job)
32
+ job_id = job.first.split('_')
33
+ params = job[1]['params']
34
+ params = params.join(' ') if params.is_a? Array
35
+ {
36
+ group: job_id.first,
37
+ id: job_id.last,
38
+ script: job[1]['script'],
39
+ params: params,
40
+ claim: job[1]['claim'],
41
+ time: Time.at(job[1]['epoch'].to_i)
42
+ }
43
+ end
44
+
45
+ def show_job_list(exec_group: nil)
46
+ rows = []
47
+ headings = if options[:verbose]
48
+ ['Exec Group', 'ID', 'Command', 'Date Added', 'Claimed']
49
+ else
50
+ ['Exec Group', 'Command']
51
+ end
52
+ job_list(exec_group: exec_group).each do |job|
53
+ details = job_details(job)
54
+ rows << if options[:verbose]
55
+ [details[:group], details[:id],
56
+ "#{details[:script]} #{details[:params]}",
57
+ details[:time],
58
+ details[:claim].nil? ? 'UNCLAIMED' : details[:claim]]
59
+ else
60
+ [details[:group], "#{details[:script]} #{details[:params]}"]
61
+ end
62
+ end
63
+ puts Terminal::Table.new headings: headings, rows: rows
64
+ end
65
+
66
+ def put_job(exec_group, script, params)
67
+ obj_name = "#{exec_group}_#{Time.now.to_f}"
68
+ epoch = Time.now.to_i
69
+ obj = { script: script, params: params, epoch: epoch }
70
+ puts "Adding job #{obj_name}"
71
+ oci.add_obj(obj_name, obj.to_json)
72
+ end
73
+
74
+ def claimable_jobs?(exec_group)
75
+ job_list(exec_group: exec_group).each do |job|
76
+ obj = JSON.parse(oci.get_obj(job.first))
77
+ return true if obj['claim'].nil?
78
+ end
79
+ false
80
+ end
81
+
82
+ def unclaimed?(obj)
83
+ if obj['claim']
84
+ # Claim expires after 10 mins
85
+ (Time.now.to_i - obj['claimed_at'].to_i) > 300
86
+ else
87
+ true
88
+ end
89
+ end
90
+
91
+ def claim_job(job_id, obj)
92
+ my_ref = "#{fqdn}.#{Process.pid}"
93
+ obj[:claim] = my_ref
94
+ obj[:claimed_at] = Time.now.to_i
95
+ puts "claiming #{job_id}"
96
+ begin
97
+ oci.add_obj(job_id, obj.to_json)
98
+ rescue OCI::Errors::ServiceError
99
+ puts 'did not get claim'
100
+ return false
101
+ end
102
+ sleep 0.3
103
+ begin
104
+ obj = JSON.parse(oci.get_obj(job_id))
105
+ rescue TypeError
106
+ puts 'Could not read back job contents, skipping ..'
107
+ return false
108
+ end
109
+ return true if obj['claim'] == my_ref
110
+ puts 'did not get claim'
111
+ false
112
+ end
113
+
114
+ def invalid_script?(script)
115
+ (script =~ /\A[a-zA-Z0-9\._-]+\Z/).nil?
116
+ end
117
+
118
+ def daemon(exec_group, script_dir)
119
+ dir = File.expand_path(script_dir)
120
+ loop do
121
+ puts 'Looking for jobs ..'
122
+ job_obj_list(exec_group: exec_group).map(&:name).each do |job_id|
123
+ raw = oci.get_obj(job_id)
124
+ next if raw.nil?
125
+ obj = JSON.parse(raw)
126
+ if unclaimed?(obj) && claim_job(job_id, obj)
127
+ if invalid_script?(obj['script'])
128
+ puts 'Invalid script'
129
+ else
130
+ params = obj['params']
131
+ params = params.join(' ') if params.is_a? Array
132
+ if system(File.join(dir, obj['script']), params)
133
+ puts 'Success. Removing job from queue'
134
+ oci.del_obj(job_id)
135
+ else
136
+ puts "Job #{job_id} returned non-zero exit code"
137
+ end
138
+ end
139
+ end
140
+ end
141
+ puts 'Waiting 60 seconds ..'
142
+ sleep 60
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def job_obj_list(exec_group: nil)
149
+ if exec_group
150
+ oci.objects.select { |o| o.name.start_with? "#{exec_group}_" }
151
+ else
152
+ oci.objects
153
+ end
154
+ end
155
+
156
+ def unordered_job_list(exec_group: nil)
157
+ job_obj_list(exec_group: exec_group).map do |job|
158
+ [job.name, JSON.parse(oci.get_obj(job.name))]
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'oci'
4
+
5
+ module Jobbie
6
+ # Utility class for interacting with OCI object store
7
+ class OciUtil
8
+ attr_reader :options
9
+
10
+ def initialize(options)
11
+ @options = options
12
+ end
13
+
14
+ def region
15
+ OCI::Regions::REGION_SHORT_NAMES_TO_LONG_NAMES[options[:region].to_sym]
16
+ end
17
+
18
+ def client_conf
19
+ OCI::ConfigFileLoader.load_config(profile_name: options[:profile])
20
+ end
21
+
22
+ def client_args
23
+ { config: client_conf, region: region }
24
+ end
25
+
26
+ def obj_api
27
+ OCI::ObjectStorage::ObjectStorageClient.new(client_args)
28
+ end
29
+
30
+ def namespace
31
+ @namespace ||= obj_api.get_namespace.data
32
+ end
33
+
34
+ def bucket
35
+ options[:bucket]
36
+ end
37
+
38
+ def objects
39
+ obj_api.list_objects(namespace, bucket).flat_map do |resp|
40
+ resp.data.objects.map do |obj|
41
+ obj
42
+ end
43
+ end
44
+ end
45
+
46
+ def add_obj(obj_name, content)
47
+ obj_api.put_object(namespace, bucket, obj_name, content)
48
+ end
49
+
50
+ def get_obj(obj_name)
51
+ obj_api.get_object(namespace, bucket, obj_name).data
52
+ rescue OCI::Errors::ServiceError
53
+ nil
54
+ end
55
+
56
+ def del_obj(obj_name)
57
+ obj_api.delete_object(namespace, bucket, obj_name)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobbieOci
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jobbie_oci
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Stephen Pearson
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-07-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ description:
42
+ email:
43
+ - stephen.pearson@oracle.com
44
+ executables:
45
+ - jobbie
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".gitignore"
50
+ - ".rubocop.yml"
51
+ - Gemfile
52
+ - Gemfile.lock
53
+ - README.md
54
+ - Rakefile
55
+ - exe/jobbie
56
+ - jobbie_oci.gemspec
57
+ - lib/jobbie_oci/cli.rb
58
+ - lib/jobbie_oci/jobs.rb
59
+ - lib/jobbie_oci/oci.rb
60
+ - lib/jobbie_oci/version.rb
61
+ homepage: http://github.com/stephenpearson/jobbie_oci
62
+ licenses: []
63
+ metadata: {}
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 2.6.13
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Simple noddy job queuer that uses OCI Casper backend
84
+ test_files: []