tq 0.1.3
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 +7 -0
- data/.gems +3 -0
- data/lib/tq.rb +5 -0
- data/lib/tq/app.rb +167 -0
- data/lib/tq/logger.rb +93 -0
- data/lib/tq/queue.rb +138 -0
- data/lib/tq/shell.rb +85 -0
- data/lib/version.rb +4 -0
- data/test/helper.rb +120 -0
- data/test/suite.rb +4 -0
- data/test/test_auth.rb +63 -0
- data/test/test_logger.rb +184 -0
- data/test/test_run.rb +239 -0
- data/test/test_shell.rb +128 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fc0bd8304fbc66b4f921997f047c637f579250b9
|
4
|
+
data.tar.gz: d694a8a9b632e17715d78d32a7a2311176d4fed8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f0062de8772a7555928676469d0fa9191c9d36429f0b451d8cdf28ff0225e07d8eac29d69b826f74636110639936209dda94908e9df2d18ddaf9dafcc68076c7
|
7
|
+
data.tar.gz: 370c3621670e588217dd84fea9087d94fda54c040bc8776c55ea1f592383835f32c6d0baf366c3ce5052e0f655d729b59e88b0fe3af1069cdb5de706b4ac0983
|
data/.gems
ADDED
data/lib/tq.rb
ADDED
data/lib/tq/app.rb
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'google/api_client'
|
2
|
+
require 'google/api_client/client_secrets'
|
3
|
+
require 'google/api_client/auth/file_storage'
|
4
|
+
require 'google/api_client/auth/installed_app'
|
5
|
+
require 'parallel'
|
6
|
+
|
7
|
+
require_relative 'queue'
|
8
|
+
|
9
|
+
TASKQUEUE_API = 'taskqueue'
|
10
|
+
TASKQUEUE_API_VERSION = 'v1beta2'
|
11
|
+
TASKQUEUE_API_SCOPES = ['https://www.googleapis.com/auth/taskqueue']
|
12
|
+
|
13
|
+
module TQ
|
14
|
+
|
15
|
+
DEFAULT_OPTIONS = {
|
16
|
+
'concurrency' => 2,
|
17
|
+
'log' => {
|
18
|
+
'file' => $stderr
|
19
|
+
},
|
20
|
+
'env' => {}
|
21
|
+
}
|
22
|
+
|
23
|
+
class App
|
24
|
+
|
25
|
+
attr_reader :id, :worker
|
26
|
+
def initialize(id, worker, options={})
|
27
|
+
@id = id; @worker = worker
|
28
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
29
|
+
end
|
30
|
+
|
31
|
+
def options(_)
|
32
|
+
App.new @id, @worker, @options.merge(_)
|
33
|
+
end
|
34
|
+
|
35
|
+
def project(_)
|
36
|
+
options({'project' => _})
|
37
|
+
end
|
38
|
+
|
39
|
+
def log(_)
|
40
|
+
options({'log' => @options['log'].merge(_)})
|
41
|
+
end
|
42
|
+
|
43
|
+
def logger(_)
|
44
|
+
options({'logger' => _})
|
45
|
+
end
|
46
|
+
|
47
|
+
def env(_)
|
48
|
+
options({'env' => @options['env'].merge(_)})
|
49
|
+
end
|
50
|
+
|
51
|
+
def stdin(_)
|
52
|
+
return stdin({'name' => _}) if String === _
|
53
|
+
options({'stdin' => _})
|
54
|
+
end
|
55
|
+
|
56
|
+
def stdout(_)
|
57
|
+
return stdout({'name' => _}) if String === _
|
58
|
+
options({'stdout' => _})
|
59
|
+
end
|
60
|
+
|
61
|
+
def stderr(_)
|
62
|
+
return stderr({'name' => _}) if String === _
|
63
|
+
options({'stderr' => _})
|
64
|
+
end
|
65
|
+
|
66
|
+
def run!(secrets_file=nil, store_file=nil)
|
67
|
+
setup_logger!
|
68
|
+
_run *(_queues( TQ::Queue.new( *(auth!(secrets_file, store_file)) ).project(@options['project']) ) )
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
def service_auth!(issuer, p12_file)
|
73
|
+
key = Google::APIClient::KeyUtils.load_from_pkcs12(p12_file, 'notasecret')
|
74
|
+
client.authorization = Signet::OAuth2::Client.new(
|
75
|
+
:token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
|
76
|
+
:audience => 'https://accounts.google.com/o/oauth2/token',
|
77
|
+
:scope => TASKQUEUE_API_SCOPES,
|
78
|
+
:issuer => issuer,
|
79
|
+
:signing_key => key)
|
80
|
+
client.authorization.fetch_access_token!
|
81
|
+
|
82
|
+
api = client.discovered_api(TASKQUEUE_API, TASKQUEUE_API_VERSION)
|
83
|
+
|
84
|
+
return client, api
|
85
|
+
end
|
86
|
+
|
87
|
+
def auth!(secrets_file=nil, store_file=nil)
|
88
|
+
if store_file.nil? || (cred_store = credentials_store(store_file)).authorization.nil?
|
89
|
+
client_secrets = Google::APIClient::ClientSecrets.load(secrets_file)
|
90
|
+
flow = Google::APIClient::InstalledAppFlow.new(
|
91
|
+
:client_id => client_secrets.client_id,
|
92
|
+
:client_secret => client_secrets.client_secret,
|
93
|
+
:scope => TASKQUEUE_API_SCOPES
|
94
|
+
)
|
95
|
+
client.authorization = store_file.nil? ?
|
96
|
+
flow.authorize :
|
97
|
+
flow.authorize(cred_store)
|
98
|
+
else
|
99
|
+
client.authorization = cred_store.authorization
|
100
|
+
end
|
101
|
+
|
102
|
+
api = client.discovered_api(TASKQUEUE_API, TASKQUEUE_API_VERSION)
|
103
|
+
|
104
|
+
return client, api
|
105
|
+
end
|
106
|
+
|
107
|
+
def application_name
|
108
|
+
@id.split('/')[0]
|
109
|
+
end
|
110
|
+
|
111
|
+
def application_version
|
112
|
+
@id.split('/')[1] || '0.0.0'
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def setup_logger!
|
118
|
+
if logger = @options['logger']
|
119
|
+
else
|
120
|
+
if (log = @options['log']) && (file = log['file'])
|
121
|
+
logger = Logger.new(file)
|
122
|
+
if level = log['level']
|
123
|
+
logger.level = level
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
(Google::APIClient.logger = logger) if logger
|
128
|
+
end
|
129
|
+
|
130
|
+
def client
|
131
|
+
@client ||= Google::APIClient.new(
|
132
|
+
:application_name => application_name,
|
133
|
+
:application_version => application_version
|
134
|
+
)
|
135
|
+
end
|
136
|
+
|
137
|
+
def credentials_store(file)
|
138
|
+
Google::APIClient::FileStorage.new(file)
|
139
|
+
end
|
140
|
+
|
141
|
+
def _queues(q)
|
142
|
+
qin = @options['stdin'] && q.options(@options['stdin'])
|
143
|
+
qout = @options['stdout'] && q.options(@options['stdout'])
|
144
|
+
qerr = @options['stderr'] && q.options(@options['stderr'])
|
145
|
+
return qin, qout, qerr
|
146
|
+
end
|
147
|
+
|
148
|
+
# TODO handle uncaught worker errors by qerr.push!(err) and qin.finish!(task)
|
149
|
+
# TODO raise if not qin
|
150
|
+
def _run(qin, qout, qerr)
|
151
|
+
tasks = qin.lease!
|
152
|
+
Parallel.each(tasks, :in_threads => @options['concurrency']) do |task|
|
153
|
+
@worker.new(qin, qout, qerr, inherited_env).call(task)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# default log/logger options into env
|
158
|
+
def inherited_env
|
159
|
+
env = @options['env']
|
160
|
+
log = @options['log']
|
161
|
+
logger = @options['logger']
|
162
|
+
{'log' => log, 'logger' => logger}.merge(env)
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
data/lib/tq/logger.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module TQ
|
5
|
+
|
6
|
+
class Logger
|
7
|
+
|
8
|
+
DEFAULT_OPTIONS = {
|
9
|
+
'file' => $stderr,
|
10
|
+
'level' => ::Logger::WARN
|
11
|
+
}
|
12
|
+
|
13
|
+
def initialize(queue, options={})
|
14
|
+
@queue = queue
|
15
|
+
if Hash === options
|
16
|
+
@log = build_log( DEFAULT_OPTIONS.merge(options) )
|
17
|
+
else
|
18
|
+
@log = options
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def level
|
23
|
+
@log.level
|
24
|
+
end
|
25
|
+
|
26
|
+
def level=(severity)
|
27
|
+
@log.level = severity
|
28
|
+
end
|
29
|
+
|
30
|
+
def progname
|
31
|
+
@log.progname
|
32
|
+
end
|
33
|
+
|
34
|
+
def progname=(name)
|
35
|
+
@log.progname = name
|
36
|
+
end
|
37
|
+
|
38
|
+
def add(severity, message=nil, progname=nil, context=nil, &block)
|
39
|
+
t = Time.now
|
40
|
+
@log.add(severity, message, progname, &block)
|
41
|
+
@queue.push!(
|
42
|
+
queue_message(t, severity, message, progname, context, &block),
|
43
|
+
::Logger::SEV_LABEL[severity].to_s.downcase
|
44
|
+
) if (severity >= level)
|
45
|
+
end
|
46
|
+
|
47
|
+
alias log add
|
48
|
+
|
49
|
+
::Logger::SEV_LABEL.each_with_index do |label,level|
|
50
|
+
define_method(label == 'ANY' ? 'unknown' : label.to_s.downcase) do |progname=nil, context=nil, &block|
|
51
|
+
add(level, nil, progname, context, &block)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def build_log(options)
|
58
|
+
return options if options.respond_to?(:log)
|
59
|
+
logger = ::Logger.new(options['file'], options['shift_age'], options['shift_size'])
|
60
|
+
logger.level = options['level'] if options['level']
|
61
|
+
logger.progname = options['progname'] if options['progname']
|
62
|
+
return logger
|
63
|
+
end
|
64
|
+
|
65
|
+
# damn, the ruby logger interface is weird... this logic is copied almost verbatim
|
66
|
+
# from ::Logger.add
|
67
|
+
def queue_message(t, severity, message, progname, context, &block)
|
68
|
+
severity ||= ::Logger::UNKNOWN
|
69
|
+
progname ||= self.progname
|
70
|
+
if message.nil?
|
71
|
+
if block_given?
|
72
|
+
message = yield
|
73
|
+
else
|
74
|
+
message = progname
|
75
|
+
progname = self.progname
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
return {
|
80
|
+
time: t.iso8601,
|
81
|
+
timestamp: t.to_i,
|
82
|
+
level: severity,
|
83
|
+
label: ::Logger::SEV_LABEL[severity],
|
84
|
+
message: message,
|
85
|
+
progname: progname,
|
86
|
+
context: context
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
data/lib/tq/queue.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
|
2
|
+
module TQ
|
3
|
+
|
4
|
+
class Queue
|
5
|
+
|
6
|
+
DEFAULT_OPTIONS = { 'lease_secs' => 60, 'num_tasks' => 1 }
|
7
|
+
|
8
|
+
attr_reader :client, :api
|
9
|
+
def initialize(client, api, options={})
|
10
|
+
@client, @api = client, api
|
11
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def options(_)
|
15
|
+
Queue.new @client, @api, @options.merge(_)
|
16
|
+
end
|
17
|
+
|
18
|
+
def project(_)
|
19
|
+
options({'project' => _})
|
20
|
+
end
|
21
|
+
|
22
|
+
def name(_)
|
23
|
+
options({'name' => _})
|
24
|
+
end
|
25
|
+
|
26
|
+
def lease!(opts={})
|
27
|
+
opts = @options.merge(opts)
|
28
|
+
results = client.execute!(
|
29
|
+
:api_method => api.tasks.lease,
|
30
|
+
:parameters => { :leaseSecs => opts['lease_secs'],
|
31
|
+
:project => opts['project'],
|
32
|
+
:taskqueue => opts['name'],
|
33
|
+
:numTasks => opts['num_tasks']
|
34
|
+
}
|
35
|
+
)
|
36
|
+
items = (results.data && results.data['items']) || []
|
37
|
+
items.map {|t| new_task(t) }
|
38
|
+
end
|
39
|
+
|
40
|
+
# note: does not currently work; filed bug report https://code.google.com/p/googleappengine/issues/detail?id=11838
|
41
|
+
def extend!(task, secs=nil)
|
42
|
+
secs = secs.nil? ? @options['lease_secs'] : secs
|
43
|
+
opts = @options
|
44
|
+
results = client.execute!(
|
45
|
+
:api_method => api.tasks.update,
|
46
|
+
:parameters => { :newLeaseSeconds => secs,
|
47
|
+
:project => opts['project'],
|
48
|
+
:taskqueue => opts['name'],
|
49
|
+
:task => task.id
|
50
|
+
}
|
51
|
+
)
|
52
|
+
new_task(results.data)
|
53
|
+
end
|
54
|
+
|
55
|
+
def push!(payload, tag=nil)
|
56
|
+
opts = @options
|
57
|
+
body = { 'queueName' => opts['name'],
|
58
|
+
'payloadBase64' => encode(payload)
|
59
|
+
}
|
60
|
+
body['tag'] = tag if tag
|
61
|
+
|
62
|
+
results = client.execute!(
|
63
|
+
:api_method => api.tasks.insert,
|
64
|
+
:parameters => { :project => opts['project'],
|
65
|
+
:taskqueue => opts['name']
|
66
|
+
},
|
67
|
+
:body_object => body
|
68
|
+
)
|
69
|
+
new_task(results.data)
|
70
|
+
end
|
71
|
+
|
72
|
+
# note: you must have previously leased given task
|
73
|
+
def finish!(task)
|
74
|
+
opts = @options
|
75
|
+
client.execute!( :api_method => api.tasks.delete,
|
76
|
+
:parameters => { :project => opts['project'],
|
77
|
+
:taskqueue => opts['name'],
|
78
|
+
:task => task.id
|
79
|
+
}
|
80
|
+
)
|
81
|
+
return
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def new_task(t)
|
87
|
+
Task.new(self, t['id'], timestamp_time(t['leaseTimestamp']), decode(t.payloadBase64), t)
|
88
|
+
end
|
89
|
+
|
90
|
+
def timestamp_time(t)
|
91
|
+
Time.at( t / 1000000 )
|
92
|
+
end
|
93
|
+
|
94
|
+
def encode(obj)
|
95
|
+
Base64.urlsafe_encode64(JSON.dump(obj))
|
96
|
+
end
|
97
|
+
|
98
|
+
def decode(str)
|
99
|
+
JSON.load(Base64.urlsafe_decode64(str))
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
class Task < Struct.new(:queue, :id, :expires, :payload, :raw)
|
105
|
+
|
106
|
+
def initialize(*args)
|
107
|
+
super
|
108
|
+
@clock = Time
|
109
|
+
end
|
110
|
+
|
111
|
+
def finish!
|
112
|
+
self.queue.finish!(self)
|
113
|
+
end
|
114
|
+
|
115
|
+
def extend!(secs=nil)
|
116
|
+
self.queue.extend!(self, secs)
|
117
|
+
end
|
118
|
+
|
119
|
+
def clock!(_)
|
120
|
+
@clock = _; return self
|
121
|
+
end
|
122
|
+
|
123
|
+
def reset_clock!
|
124
|
+
@clock = Time; return self
|
125
|
+
end
|
126
|
+
|
127
|
+
def lease_remaining
|
128
|
+
self.expires - @clock.now
|
129
|
+
end
|
130
|
+
|
131
|
+
def lease_expired?
|
132
|
+
self.expires < @clock.now
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
data/lib/tq/shell.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'json'
|
3
|
+
require_relative '../version'
|
4
|
+
|
5
|
+
module TQ
|
6
|
+
|
7
|
+
class Shell
|
8
|
+
|
9
|
+
DEFAULT_OPTIONS = {
|
10
|
+
app: {},
|
11
|
+
auth_secrets_file: './tq-client.json',
|
12
|
+
auth_store_file: './tq-client-store.json',
|
13
|
+
config_file: './tq-app.json'
|
14
|
+
}
|
15
|
+
|
16
|
+
def initialize(app, logger=nil)
|
17
|
+
@app = app
|
18
|
+
@logger = logger
|
19
|
+
@summary = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def banner(_)
|
23
|
+
@banner = _; return self
|
24
|
+
end
|
25
|
+
|
26
|
+
def summary(*_)
|
27
|
+
@summary = _; return self
|
28
|
+
end
|
29
|
+
|
30
|
+
def call(argv=ARGV)
|
31
|
+
|
32
|
+
progname = File.basename(__FILE__,'.rb')
|
33
|
+
|
34
|
+
@logger.info(progname) { "Configuring #{@app.id}" } if @logger
|
35
|
+
opts = parse_args(argv)
|
36
|
+
@logger.debug(progname) { "Configuration: #{opts[:app].inspect}" } if @logger
|
37
|
+
|
38
|
+
@app = @app.options( opts[:app] ).logger(@logger)
|
39
|
+
|
40
|
+
@logger.info(progname) { "Running #{@app.id} using worker #{@app.worker}" } if @logger
|
41
|
+
@app.run!( opts[:auth_secrets_file], opts[:auth_store_file] )
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def parse_args(argv)
|
48
|
+
opts = {}.merge(DEFAULT_OPTIONS)
|
49
|
+
|
50
|
+
OptionParser.new do |shell|
|
51
|
+
|
52
|
+
(shell.banner = @banner) if @banner
|
53
|
+
@summary.each do |line|
|
54
|
+
shell.separator line
|
55
|
+
end
|
56
|
+
|
57
|
+
shell.on('-a', '--auth-secrets [FILE]', "Google OAuth2 secrets file") do |given|
|
58
|
+
opts[:auth_secrets_file] = given
|
59
|
+
end
|
60
|
+
|
61
|
+
shell.on('-s', '--auth-store [FILE]', "Google OAuth2 storage file") do |given|
|
62
|
+
opts[:auth_store_file] = given
|
63
|
+
end
|
64
|
+
|
65
|
+
shell.on('-c', '--config [FILE]', "Application config file (json)") do |given|
|
66
|
+
opts[:config_file] = given
|
67
|
+
opts[:app] = JSON.load( File.open(given, 'r') )
|
68
|
+
end
|
69
|
+
|
70
|
+
shell.on('-h', '--help', "Prints this help") do |given|
|
71
|
+
puts shell; exit
|
72
|
+
end
|
73
|
+
|
74
|
+
shell.on('-v', '--version', "Prints TQ version") do |given|
|
75
|
+
puts TQ::VERSION; exit
|
76
|
+
end
|
77
|
+
|
78
|
+
end.parse(argv)
|
79
|
+
|
80
|
+
return opts
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
data/lib/version.rb
ADDED
data/test/helper.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
gem 'minitest'
|
5
|
+
require 'minitest/autorun'
|
6
|
+
|
7
|
+
require 'google/api_client'
|
8
|
+
|
9
|
+
module TestUtils
|
10
|
+
extend self
|
11
|
+
|
12
|
+
def setup_logger(name="test")
|
13
|
+
file = File.expand_path("log/#{name}.log", File.dirname(__FILE__))
|
14
|
+
FileUtils.mkdir_p( File.dirname(file) )
|
15
|
+
logger = Logger.new( File.open(file, 'w' ) )
|
16
|
+
logger.level = ENV['DEBUG'] ? Logger::DEBUG : Logger::INFO
|
17
|
+
Google::APIClient.logger = logger
|
18
|
+
end
|
19
|
+
|
20
|
+
def current_logger
|
21
|
+
Google::APIClient.logger
|
22
|
+
end
|
23
|
+
|
24
|
+
class QueueHelper
|
25
|
+
|
26
|
+
attr_reader :project, :queue
|
27
|
+
def initialize(project,queue)
|
28
|
+
@project, @queue = project, queue
|
29
|
+
end
|
30
|
+
|
31
|
+
def auth_files(secrets,creds)
|
32
|
+
@secrets_file, @creds_file = secrets, creds
|
33
|
+
return self
|
34
|
+
end
|
35
|
+
|
36
|
+
def authorized_client
|
37
|
+
# @authorized_client ||= TQ::App.new('test_app',nil).service_auth!(File.read(SERVICE_ISSUER_FILE).chomp, SERVICE_P12_FILE)
|
38
|
+
@authorized_client ||= TQ::App.new('test_app',nil).auth!(@secrets_file, @creds_file)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Note: inaccurate, don't use
|
42
|
+
def peek()
|
43
|
+
client, api = authorized_client
|
44
|
+
results = client.execute!( :api_method => api.tasks.list,
|
45
|
+
:parameters => { :project => project, :taskqueue => queue }
|
46
|
+
)
|
47
|
+
items = results.data['items'] || []
|
48
|
+
end
|
49
|
+
|
50
|
+
def push!(payload)
|
51
|
+
client, api = authorized_client
|
52
|
+
client.execute!( :api_method => api.tasks.insert,
|
53
|
+
:parameters => { :project => project, :taskqueue => queue },
|
54
|
+
:body_object => {
|
55
|
+
'queueName' => queue,
|
56
|
+
'payloadBase64' => encode(payload)
|
57
|
+
}
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def pop!(n=1)
|
62
|
+
client, api = authorized_client
|
63
|
+
results = client.execute!( :api_method => api.tasks.lease,
|
64
|
+
:parameters => { :project => project, :taskqueue => queue,
|
65
|
+
:leaseSecs => 60, :numTasks => n
|
66
|
+
}
|
67
|
+
)
|
68
|
+
items = results.data['items'] || []
|
69
|
+
items.each do |item|
|
70
|
+
client.execute!( :api_method => api.tasks.delete,
|
71
|
+
:parameters => { :project => project, :taskqueue => queue, :task => item['id'] }
|
72
|
+
)
|
73
|
+
end
|
74
|
+
return items
|
75
|
+
end
|
76
|
+
|
77
|
+
def all_payloads!
|
78
|
+
map! { |t| decode(t['payloadBase64']) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def all_tags!
|
82
|
+
map! { |t| t['tag'] }
|
83
|
+
end
|
84
|
+
|
85
|
+
def all_payloads_and_tags!
|
86
|
+
map! { |t| {:payload => decode(t['payloadBase64']),
|
87
|
+
:tag => t['tag']
|
88
|
+
}
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
def map!(&b)
|
93
|
+
clear!.map(&b)
|
94
|
+
end
|
95
|
+
|
96
|
+
def clear!
|
97
|
+
client, api = authorized_client
|
98
|
+
done = false
|
99
|
+
all = []
|
100
|
+
while !done do
|
101
|
+
batch = pop!(10)
|
102
|
+
done = batch.empty? || batch.length < 10
|
103
|
+
all = all + batch
|
104
|
+
end
|
105
|
+
all
|
106
|
+
end
|
107
|
+
|
108
|
+
def encode(obj)
|
109
|
+
Base64.urlsafe_encode64(JSON.dump(obj))
|
110
|
+
end
|
111
|
+
|
112
|
+
def decode(str)
|
113
|
+
JSON.load(Base64.urlsafe_decode64(str))
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
end
|
120
|
+
|
data/test/suite.rb
ADDED
data/test/test_auth.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require_relative './helper'
|
3
|
+
require_relative '../lib/tq/app'
|
4
|
+
|
5
|
+
def setup_logger!(name)
|
6
|
+
TestUtils.setup_logger(name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def delete_credentials!
|
10
|
+
FileUtils.rm_f(CREDENTIALS_FILE)
|
11
|
+
end
|
12
|
+
|
13
|
+
# for installed app auth
|
14
|
+
CLIENT_SECRETS_FILE = File.expand_path('../config/secrets/test/client_secrets.json', File.dirname(__FILE__))
|
15
|
+
CREDENTIALS_FILE = File.expand_path("../config/secrets/test/#{File.basename(__FILE__,'.rb')}-oauth2.json", File.dirname(__FILE__))
|
16
|
+
|
17
|
+
# for service account auth -- not quite working
|
18
|
+
SERVICE_ISSUER_FILE = File.expand_path('../config/secrets/test/issuer', File.dirname(__FILE__))
|
19
|
+
SERVICE_P12_FILE = File.expand_path('../config/secrets/test/client.p12', File.dirname(__FILE__))
|
20
|
+
|
21
|
+
describe TQ::App do
|
22
|
+
|
23
|
+
describe "auth" do
|
24
|
+
|
25
|
+
before do
|
26
|
+
setup_logger!('auth')
|
27
|
+
delete_credentials!
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should authorize without cached credentials" do
|
31
|
+
app = TQ::App.new('test_app/0.0.0', nil)
|
32
|
+
app.auth! CLIENT_SECRETS_FILE
|
33
|
+
assert true
|
34
|
+
end
|
35
|
+
|
36
|
+
# Note: browser window should only appear once for this test
|
37
|
+
# Not sure if it's possible to assert this
|
38
|
+
it "should authorize with cached credentials not existing and existing" do
|
39
|
+
app = TQ::App.new('test_app/0.0.0', nil)
|
40
|
+
app.auth! CLIENT_SECRETS_FILE, CREDENTIALS_FILE
|
41
|
+
assert File.exists?(CREDENTIALS_FILE)
|
42
|
+
app.auth! CLIENT_SECRETS_FILE, CREDENTIALS_FILE
|
43
|
+
assert File.exists?(CREDENTIALS_FILE)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "service auth" do
|
49
|
+
|
50
|
+
before do
|
51
|
+
setup_logger!('auth')
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should authorize service account" do
|
55
|
+
app = TQ::App.new('test_app/0.0.0', nil)
|
56
|
+
app.service_auth!(File.read(SERVICE_ISSUER_FILE).chomp, SERVICE_P12_FILE)
|
57
|
+
assert true
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
data/test/test_logger.rb
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
# Functional tests of TQ::Logger
|
2
|
+
# Please note that these require a deployed GAE app
|
3
|
+
# with two queues: 'test' and 'log'.
|
4
|
+
# The GAE project_id is defined in ../config/secrets/test/project_id,
|
5
|
+
# along with other secrets files (see below).
|
6
|
+
|
7
|
+
require 'logger'
|
8
|
+
|
9
|
+
require_relative './helper'
|
10
|
+
require_relative '../lib/tq/app'
|
11
|
+
require_relative '../lib/tq/queue'
|
12
|
+
require_relative '../lib/tq/logger'
|
13
|
+
|
14
|
+
def setup_test_logger!
|
15
|
+
TestUtils.setup_logger( File.basename(__FILE__,'.rb') )
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
class LoggerTests < Minitest::Spec
|
20
|
+
|
21
|
+
# for installed app auth
|
22
|
+
CLIENT_SECRETS_FILE = File.expand_path(
|
23
|
+
'../config/secrets/test/client_secrets.json', File.dirname(__FILE__)
|
24
|
+
)
|
25
|
+
CREDENTIALS_FILE = File.expand_path(
|
26
|
+
"../config/secrets/test/#{File.basename(__FILE__,'.rb')}-oauth2.json",
|
27
|
+
File.dirname(__FILE__)
|
28
|
+
)
|
29
|
+
|
30
|
+
# task queue constants
|
31
|
+
TASKQUEUE_PROJECT_ID = File.read(
|
32
|
+
File.expand_path('../config/secrets/test/project_id', File.dirname(__FILE__))
|
33
|
+
).chomp
|
34
|
+
|
35
|
+
TASKQUEUE_TEST_QUEUE = 'log'
|
36
|
+
|
37
|
+
def test_logger
|
38
|
+
@test_logger ||= TestUtils.current_logger
|
39
|
+
end
|
40
|
+
|
41
|
+
def assert_logged( exps, acts )
|
42
|
+
errs = []
|
43
|
+
unless (a = exps.length) == (b = acts.length)
|
44
|
+
errs.push( "Expected #{a} messages, #{b < 2 ? 'were' : 'was'} #{b}" )
|
45
|
+
end
|
46
|
+
exps.each_with_index do |exp,i|
|
47
|
+
act = acts[i]
|
48
|
+
next if act.nil?
|
49
|
+
[:level, :message, :progname].each do |elem|
|
50
|
+
unless (a = exp[elem]) == (b = act[elem.to_s])
|
51
|
+
errs.push( "[#{i}] Expected #{elem} to be #{a}, was #{b}" )
|
52
|
+
end
|
53
|
+
end
|
54
|
+
unless (a = ::Logger::SEV_LABEL[exp[:level]]) == (b = act['label'])
|
55
|
+
errs.push( "[#{i}] Expected label to be #{a}, was #{b}" )
|
56
|
+
end
|
57
|
+
end
|
58
|
+
assert errs.length == 0, errs.join("\n")
|
59
|
+
end
|
60
|
+
|
61
|
+
def assert_logged_tags( exps, acts )
|
62
|
+
errs = []
|
63
|
+
unless (a = exps.length) == (b = acts.length)
|
64
|
+
errs.push( "Expected #{a} messages, #{b < 2 ? 'were' : 'was'} #{b}" )
|
65
|
+
end
|
66
|
+
exps.each_with_index do |exp,i|
|
67
|
+
act = acts[i]
|
68
|
+
next if act.nil?
|
69
|
+
unless (a = exp[:method].to_s) == (b = act)
|
70
|
+
errs.push( "[#{i}] Expected tag to be #{a}, was #{b}" )
|
71
|
+
end
|
72
|
+
end
|
73
|
+
assert errs.length == 0, errs.join("\n")
|
74
|
+
end
|
75
|
+
|
76
|
+
def send_messages_to!(logger, msgs)
|
77
|
+
msgs.each do |msg|
|
78
|
+
logger.__send__(msg[:method], msg[:progname], msg[:context]) { msg[:message] }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def queue_helper(project,queue)
|
83
|
+
TestUtils::QueueHelper.new(project,queue).auth_files(CLIENT_SECRETS_FILE, CREDENTIALS_FILE)
|
84
|
+
end
|
85
|
+
|
86
|
+
def clear_queue!(project,queue)
|
87
|
+
queue_helper(project,queue).clear!
|
88
|
+
end
|
89
|
+
|
90
|
+
def verify_logged_messages_to_level!(expected_messages, minlevel)
|
91
|
+
actual_messages = queue_helper(TASKQUEUE_PROJECT_ID, TASKQUEUE_TEST_QUEUE).all_payloads_and_tags!
|
92
|
+
$stderr.puts actual_messages.inspect
|
93
|
+
|
94
|
+
selected_messages = expected_messages.select { |msg| msg[:level] >= minlevel }
|
95
|
+
|
96
|
+
assert_logged( selected_messages,
|
97
|
+
actual_messages.map { |msg| msg[:payload] }
|
98
|
+
)
|
99
|
+
|
100
|
+
assert_logged_tags( selected_messages,
|
101
|
+
actual_messages.map { |msg| msg[:tag] }
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Note: ideally these tests would also verify the (file) logger output as well
|
106
|
+
# and the timestamps, mocking the clock, etc. But that's a mess of work, so for now
|
107
|
+
# you should just eyeball it in the $stderr output.
|
108
|
+
|
109
|
+
def setup
|
110
|
+
clear_queue!(TASKQUEUE_PROJECT_ID, TASKQUEUE_TEST_QUEUE)
|
111
|
+
|
112
|
+
app = TQ::App.new('test_app/0.0.0', nil)
|
113
|
+
@queue = TQ::Queue.new( *app.auth!(CLIENT_SECRETS_FILE, CREDENTIALS_FILE) )
|
114
|
+
.options({ 'project' => TASKQUEUE_PROJECT_ID, 'name' => TASKQUEUE_TEST_QUEUE })
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'default logger should log to queue at warn level' do
|
118
|
+
subject = TQ::Logger.new(@queue)
|
119
|
+
|
120
|
+
expected_messages = [
|
121
|
+
{ method: :debug, level: ::Logger::DEBUG, message: 'debug message', progname: 'prog1', context: { key: 1 } },
|
122
|
+
{ method: :info, level: ::Logger::INFO, message: 'info message', progname: 'prog2', context: { key: 2 } },
|
123
|
+
{ method: :warn, level: ::Logger::WARN, message: 'warn message', progname: 'prog3', context: { key: 3 } },
|
124
|
+
{ method: :error, level: ::Logger::ERROR, message: 'error message', progname: 'prog4', context: { key: 4 } }
|
125
|
+
]
|
126
|
+
|
127
|
+
send_messages_to! subject, expected_messages
|
128
|
+
|
129
|
+
verify_logged_messages_to_level! expected_messages, ::Logger::WARN
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'after setting level to debug, logger should log to queue at debug level' do
|
134
|
+
subject = TQ::Logger.new(@queue)
|
135
|
+
subject.level = ::Logger::DEBUG
|
136
|
+
|
137
|
+
expected_messages = [
|
138
|
+
{ method: :debug, level: ::Logger::DEBUG, message: 'debug message', progname: 'prog1', context: { key: 1 } },
|
139
|
+
{ method: :info, level: ::Logger::INFO, message: 'info message', progname: 'prog2', context: { key: 2 } },
|
140
|
+
{ method: :warn, level: ::Logger::WARN, message: 'warn message', progname: 'prog3', context: { key: 3 } },
|
141
|
+
{ method: :error, level: ::Logger::ERROR, message: 'error message', progname: 'prog4', context: { key: 4 } }
|
142
|
+
]
|
143
|
+
|
144
|
+
send_messages_to!(subject, expected_messages)
|
145
|
+
|
146
|
+
verify_logged_messages_to_level! expected_messages, ::Logger::DEBUG
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'when setting level to debug in config, logger should log to queue at debug level' do
|
151
|
+
subject = TQ::Logger.new(@queue, {'level' => ::Logger::DEBUG } )
|
152
|
+
|
153
|
+
expected_messages = [
|
154
|
+
{ method: :debug, level: ::Logger::DEBUG, message: 'debug message', progname: 'prog1', context: { key: 1 } },
|
155
|
+
{ method: :info, level: ::Logger::INFO, message: 'info message', progname: 'prog2', context: { key: 2 } },
|
156
|
+
{ method: :warn, level: ::Logger::WARN, message: 'warn message', progname: 'prog3', context: { key: 3 } },
|
157
|
+
{ method: :error, level: ::Logger::ERROR, message: 'error message', progname: 'prog4', context: { key: 4 } }
|
158
|
+
]
|
159
|
+
|
160
|
+
send_messages_to!(subject, expected_messages)
|
161
|
+
|
162
|
+
verify_logged_messages_to_level! expected_messages, ::Logger::DEBUG
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'when setting external logger, logger should log to queue at logger\'s level' do
|
167
|
+
subject = TQ::Logger.new(@queue, test_logger )
|
168
|
+
|
169
|
+
expected_messages = [
|
170
|
+
{ method: :debug, level: ::Logger::DEBUG, message: 'debug message', progname: 'prog1', context: { key: 1 } },
|
171
|
+
{ method: :info, level: ::Logger::INFO, message: 'info message', progname: 'prog2', context: { key: 2 } },
|
172
|
+
{ method: :warn, level: ::Logger::WARN, message: 'warn message', progname: 'prog3', context: { key: 3 } },
|
173
|
+
{ method: :error, level: ::Logger::ERROR, message: 'error message', progname: 'prog4', context: { key: 4 } }
|
174
|
+
]
|
175
|
+
|
176
|
+
send_messages_to!(subject, expected_messages)
|
177
|
+
|
178
|
+
verify_logged_messages_to_level! expected_messages, test_logger.level
|
179
|
+
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
setup_test_logger!
|
data/test/test_run.rb
ADDED
@@ -0,0 +1,239 @@
|
|
1
|
+
# Functional tests of TQ::App#run!
|
2
|
+
# Please note that these require a deployed GAE app
|
3
|
+
# with two queues: 'test' and 'log'.
|
4
|
+
# The GAE project_id is defined in ../config/secrets/test/project_id,
|
5
|
+
# along with other secrets files (see below).
|
6
|
+
|
7
|
+
require_relative './helper'
|
8
|
+
require_relative '../lib/tq/app'
|
9
|
+
|
10
|
+
def setup_test_logger!
|
11
|
+
TestUtils.setup_logger( File.basename(__FILE__,'.rb') )
|
12
|
+
end
|
13
|
+
|
14
|
+
class AppRunTests < Minitest::Spec
|
15
|
+
|
16
|
+
# for installed app auth
|
17
|
+
CLIENT_SECRETS_FILE = File.expand_path(
|
18
|
+
'../config/secrets/test/client_secrets.json', File.dirname(__FILE__)
|
19
|
+
)
|
20
|
+
CREDENTIALS_FILE = File.expand_path(
|
21
|
+
"../config/secrets/test/#{File.basename(__FILE__,'.rb')}-oauth2.json",
|
22
|
+
File.dirname(__FILE__)
|
23
|
+
)
|
24
|
+
|
25
|
+
# for service account auth -- not quite working
|
26
|
+
SERVICE_ISSUER_FILE = File.expand_path(
|
27
|
+
'../config/secrets/test/issuer', File.dirname(__FILE__)
|
28
|
+
)
|
29
|
+
SERVICE_P12_FILE = File.expand_path(
|
30
|
+
'../config/secrets/test/client.p12', File.dirname(__FILE__)
|
31
|
+
)
|
32
|
+
|
33
|
+
# task queue constants
|
34
|
+
TASKQUEUE_PROJECT_ID = File.read(
|
35
|
+
File.expand_path('../config/secrets/test/project_id', File.dirname(__FILE__))
|
36
|
+
).chomp
|
37
|
+
|
38
|
+
TASKQUEUE_LEASE_SECS = 2
|
39
|
+
|
40
|
+
|
41
|
+
def queue_helper(project,queue)
|
42
|
+
TestUtils::QueueHelper.new(project,queue).auth_files(CLIENT_SECRETS_FILE, CREDENTIALS_FILE)
|
43
|
+
end
|
44
|
+
|
45
|
+
def tasks_on_queue(project,queue)
|
46
|
+
queue_helper(project,queue).list
|
47
|
+
end
|
48
|
+
|
49
|
+
def clear_queue!(project,queue)
|
50
|
+
queue_helper(project,queue).clear!
|
51
|
+
end
|
52
|
+
|
53
|
+
def push_tasks!(project,queue,tasks)
|
54
|
+
q = queue_helper(project,queue)
|
55
|
+
tasks.each do |task| q.push!(task) end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
describe "run!" do
|
60
|
+
|
61
|
+
before do
|
62
|
+
sleep TASKQUEUE_LEASE_SECS + 1 # to wait for lease expiry from previous test
|
63
|
+
clear_queue!(TASKQUEUE_PROJECT_ID,'test')
|
64
|
+
clear_queue!(TASKQUEUE_PROJECT_ID,'log')
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should setup clearing the queue" do
|
68
|
+
cleared = clear_queue!(TASKQUEUE_PROJECT_ID,'test')
|
69
|
+
assert_equal 0, n = cleared.length,
|
70
|
+
"Expected no tasks on queue, #{n < 2 ? 'was' : 'were'} #{n}"
|
71
|
+
|
72
|
+
# unfortunately, queue peeks are horribly inaccurate right now.
|
73
|
+
# assert_equal 0, n = tasks_on_queue(TASKQUEUE_PROJECT_ID,'test').length,
|
74
|
+
# "Expected no tasks on queue, #{n < 2 ? 'was' : 'were'} #{n}"
|
75
|
+
end
|
76
|
+
|
77
|
+
it "worker should receive input queue and :call with each task on the queue up to the specified number" do
|
78
|
+
|
79
|
+
# setup
|
80
|
+
expected_tasks = [
|
81
|
+
{ 'What is your name?' => 'Sir Lancelot', 'What is your quest?' => 'To seek the holy grail', 'What is your favorite color?' => 'blue' },
|
82
|
+
{ 'What is your name?' => 'Sir Robin', 'What is your quest?' => 'To seek the holy grail', 'What is the capital of Assyria?' => nil },
|
83
|
+
{ 'What is your name?' => 'Galahad', 'What is your quest?' => 'To seek the grail', 'What is your favorite color?' => ['blue','yellow'] },
|
84
|
+
{ 'What is your name?' => 'Arthur', 'What is your quest?' => 'To seek the holy grail', 'What is the air-speed velocity of an unladen swallow?' => 'African or European swallow?' }
|
85
|
+
]
|
86
|
+
push_tasks!(TASKQUEUE_PROJECT_ID,'test', expected_tasks)
|
87
|
+
|
88
|
+
# expectations
|
89
|
+
mock_handler_class = MiniTest::Mock.new
|
90
|
+
mock_handler = MiniTest::Mock.new
|
91
|
+
|
92
|
+
## expect constructor receives task queue as first param -- for each queued task up to expected_instances
|
93
|
+
expected_calls = 3
|
94
|
+
(0...expected_calls).each do
|
95
|
+
mock_handler_class.expect(:new, mock_handler) do |*args|
|
96
|
+
args.first.respond_to?('finish!')
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
## expect :call for each queued task up to expected_calls
|
101
|
+
actual_calls = 0;
|
102
|
+
(0...expected_calls).each do
|
103
|
+
mock_handler.expect(:call, true) do |actual_task|
|
104
|
+
actual_calls += 1
|
105
|
+
valid = !!actual_task && actual_task.payload.has_key?('What is your name?')
|
106
|
+
valid.tap do |yes|
|
107
|
+
$stderr.puts("mock_handler.call - received: #{ actual_task.payload }") if yes
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# execution
|
113
|
+
app = TQ::App.new('test_app/0.0.0', mock_handler_class)
|
114
|
+
.project(TASKQUEUE_PROJECT_ID)
|
115
|
+
.stdin({ 'name' => 'test', 'num_tasks' => expected_calls, 'lease_secs' => TASKQUEUE_LEASE_SECS })
|
116
|
+
app.run! CLIENT_SECRETS_FILE, CREDENTIALS_FILE
|
117
|
+
|
118
|
+
# assertions
|
119
|
+
assert_equal expected_calls, actual_calls,
|
120
|
+
"Expected #{expected_calls} worker calls, was #{actual_calls}"
|
121
|
+
|
122
|
+
mock_handler_class.verify
|
123
|
+
mock_handler.verify
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'should put task back on input queue after lease_secs, if not finished' do
|
128
|
+
|
129
|
+
# setup
|
130
|
+
expected_tasks = [
|
131
|
+
{ 'What is your name?' => 'Sir Lancelot', 'What is your quest?' => 'To seek the holy grail', 'What is your favorite color?' => 'blue' }
|
132
|
+
]
|
133
|
+
push_tasks!(TASKQUEUE_PROJECT_ID,'test', expected_tasks)
|
134
|
+
|
135
|
+
class DoNothingWorker
|
136
|
+
def initialize(*args); end
|
137
|
+
def call(task); end
|
138
|
+
end
|
139
|
+
|
140
|
+
# execution
|
141
|
+
app = TQ::App.new('test_app/0.0.0', DoNothingWorker)
|
142
|
+
.project(TASKQUEUE_PROJECT_ID)
|
143
|
+
.stdin({ 'name' => 'test', 'num_tasks' => 1, 'lease_secs' => TASKQUEUE_LEASE_SECS })
|
144
|
+
app.run! CLIENT_SECRETS_FILE, CREDENTIALS_FILE
|
145
|
+
|
146
|
+
sleep TASKQUEUE_LEASE_SECS + 1
|
147
|
+
actual_tasks = clear_queue!(TASKQUEUE_PROJECT_ID,'test')
|
148
|
+
|
149
|
+
## queue peeks are inaccurate right now...
|
150
|
+
# actual_tasks = tasks_on_queue(TASKQUEUE_PROJECT_ID,'test')
|
151
|
+
|
152
|
+
## assertion
|
153
|
+
assert_equal a = expected_tasks.length, b = actual_tasks.length,
|
154
|
+
"Expected #{a} #{a == 1 ? 'task' : 'tasks'} on queue, #{b < 2 ? 'was' : 'were'} #{b}"
|
155
|
+
|
156
|
+
assert (n = actual_tasks.first['retry_count']) > 0,
|
157
|
+
"Expected >0 lease retry count, #{n < 2 ? 'was' : 'were'} #{n}"
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'should be able to push to queue from within worker' do
|
162
|
+
|
163
|
+
# setup
|
164
|
+
expected_tasks = [
|
165
|
+
{ 'What is your name?' => 'Sir Lancelot', 'What is your quest?' => 'To seek the holy grail', 'What is your favorite color?' => 'blue' }
|
166
|
+
]
|
167
|
+
push_tasks!(TASKQUEUE_PROJECT_ID,'test', expected_tasks)
|
168
|
+
|
169
|
+
class RelayWorker
|
170
|
+
def initialize(*args)
|
171
|
+
@stdin = args.first; @stdout = args[1]
|
172
|
+
end
|
173
|
+
|
174
|
+
def call(task)
|
175
|
+
@stdout.push!(task.payload)
|
176
|
+
task.finish!
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# execution
|
181
|
+
app = TQ::App.new('test_app/0.0.0', RelayWorker)
|
182
|
+
.project(TASKQUEUE_PROJECT_ID)
|
183
|
+
.stdin({ 'name' => 'test', 'num_tasks' => 1, 'lease_secs' => TASKQUEUE_LEASE_SECS })
|
184
|
+
.stdout({ 'name' => 'log' })
|
185
|
+
app.run! CLIENT_SECRETS_FILE, CREDENTIALS_FILE
|
186
|
+
|
187
|
+
sleep TASKQUEUE_LEASE_SECS + 1
|
188
|
+
actual_output_tasks = clear_queue!(TASKQUEUE_PROJECT_ID,'log')
|
189
|
+
actual_input_tasks = clear_queue!(TASKQUEUE_PROJECT_ID,'test')
|
190
|
+
|
191
|
+
# assertion
|
192
|
+
|
193
|
+
assert_equal 0, b = actual_input_tasks.length,
|
194
|
+
"Expected no tasks on input queue, #{b < 2 ? 'was' : 'were'} #{b}"
|
195
|
+
|
196
|
+
assert_equal a = expected_tasks.length, b = actual_output_tasks.length,
|
197
|
+
"Expected #{a} #{a == 1 ? 'task' : 'tasks'} on output queue, #{b < 2 ? 'was' : 'were'} #{b}"
|
198
|
+
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'should extend a task lease if extended before lease expires' do
|
202
|
+
|
203
|
+
refute true, "Extending task leases does not currently work"
|
204
|
+
|
205
|
+
# setup
|
206
|
+
expected_tasks = [
|
207
|
+
{ 'What is your name?' => 'Sir Lancelot', 'What is your quest?' => 'To seek the holy grail', 'What is your favorite color?' => 'blue' }
|
208
|
+
]
|
209
|
+
push_tasks!(TASKQUEUE_PROJECT_ID,'test', expected_tasks)
|
210
|
+
|
211
|
+
class ExtendWorker
|
212
|
+
def initialize(*args)
|
213
|
+
@stdin = args.first
|
214
|
+
end
|
215
|
+
|
216
|
+
def call(task)
|
217
|
+
$stderr.puts "ExtendWorker - task #{ task.raw.inspect }"
|
218
|
+
ttl = task.lease_remaining
|
219
|
+
# sleep( ttl - 0.5 )
|
220
|
+
task = @stdin.extend!(task)
|
221
|
+
ttl2 = task.lease_remaining
|
222
|
+
$stderr.puts "ExtendWorker - ttl before extend: #{ttl}"
|
223
|
+
$stderr.puts "ExtendWorker - ttl after extend: #{ttl2}"
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# execution
|
228
|
+
app = TQ::App.new('test_app/0.0.0', ExtendWorker)
|
229
|
+
.project(TASKQUEUE_PROJECT_ID)
|
230
|
+
.stdin({ 'name' => 'test', 'num_tasks' => 1, 'lease_secs' => TASKQUEUE_LEASE_SECS + 2 })
|
231
|
+
app.run! CLIENT_SECRETS_FILE, CREDENTIALS_FILE
|
232
|
+
|
233
|
+
end
|
234
|
+
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
|
239
|
+
setup_test_logger!
|
data/test/test_shell.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
require_relative './helper'
|
2
|
+
require_relative '../lib/tq/app'
|
3
|
+
require_relative '../lib/tq/shell'
|
4
|
+
|
5
|
+
def setup_test_logger!
|
6
|
+
TestUtils.setup_logger( File.basename(__FILE__,'.rb') )
|
7
|
+
end
|
8
|
+
|
9
|
+
class ShellTests < Minitest::Spec
|
10
|
+
|
11
|
+
include
|
12
|
+
# for installed app auth
|
13
|
+
CLIENT_SECRETS_FILE = File.expand_path(
|
14
|
+
'../config/secrets/test/client_secrets.json', File.dirname(__FILE__)
|
15
|
+
)
|
16
|
+
CREDENTIALS_FILE = File.expand_path(
|
17
|
+
"../config/secrets/test/#{File.basename(__FILE__,'.rb')}-oauth2.json",
|
18
|
+
File.dirname(__FILE__)
|
19
|
+
)
|
20
|
+
|
21
|
+
# task queue constants
|
22
|
+
TASKQUEUE_APP_CONFIG =
|
23
|
+
File.expand_path("../config/secrets/test/#{File.basename(__FILE__,'.rb')}" +
|
24
|
+
"-config.json", File.dirname(__FILE__))
|
25
|
+
|
26
|
+
TASKQUEUE_LEASE_SECS = 2
|
27
|
+
|
28
|
+
class EchoWorker
|
29
|
+
|
30
|
+
def initialize(stdin, stdout, stderr, env)
|
31
|
+
@stdin = stdin
|
32
|
+
@env = env
|
33
|
+
@logger = env['logger']
|
34
|
+
end
|
35
|
+
|
36
|
+
def call(task)
|
37
|
+
@logger.info("EchoWorker") { "Received task #{task.id}" }
|
38
|
+
@logger.debug("EchoWorker") { "Task payload: #{task.payload.inspect}" }
|
39
|
+
@logger.debug("EchoWorker") { "Env: #{@env.inspect}" }
|
40
|
+
task.finish!
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
def logger
|
46
|
+
@logger ||= TestUtils.current_logger
|
47
|
+
end
|
48
|
+
|
49
|
+
def queue_helper(project,queue)
|
50
|
+
TestUtils::QueueHelper.new(project,queue).auth_files(CLIENT_SECRETS_FILE, CREDENTIALS_FILE)
|
51
|
+
end
|
52
|
+
|
53
|
+
def app_config
|
54
|
+
@app_config ||= JSON.load( File.open(TASKQUEUE_APP_CONFIG) )
|
55
|
+
end
|
56
|
+
|
57
|
+
def app_project_id
|
58
|
+
app_config['project']
|
59
|
+
end
|
60
|
+
|
61
|
+
def app_stdin_name
|
62
|
+
app_config['stdin']['name']
|
63
|
+
end
|
64
|
+
|
65
|
+
def app_stdin_num_tasks
|
66
|
+
app_config['stdin']['num_tasks']
|
67
|
+
end
|
68
|
+
|
69
|
+
def populate_queue!(tasks)
|
70
|
+
q = queue_helper(app_project_id, app_stdin_name)
|
71
|
+
tasks.each do |task| q.push!(task) end
|
72
|
+
end
|
73
|
+
|
74
|
+
def clear_queue!
|
75
|
+
queue_helper(app_project_id, app_stdin_name).clear!
|
76
|
+
end
|
77
|
+
|
78
|
+
def tasks_on_queue
|
79
|
+
clear_queue!
|
80
|
+
end
|
81
|
+
|
82
|
+
def assert_tasks_on_queue(exp)
|
83
|
+
assert_equal exp, n = tasks_on_queue.length,
|
84
|
+
"Expected #{exp} tasks on input queue, was #{n}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def shell_args
|
88
|
+
[ "--auth-secrets", CLIENT_SECRETS_FILE,
|
89
|
+
"--auth-store", CREDENTIALS_FILE,
|
90
|
+
"--config", TASKQUEUE_APP_CONFIG
|
91
|
+
]
|
92
|
+
end
|
93
|
+
|
94
|
+
def setup
|
95
|
+
sleep TASKQUEUE_LEASE_SECS+1
|
96
|
+
clear_queue!
|
97
|
+
@app = TQ::App.new('test_app_shell/0.0.0', EchoWorker)
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should execute and complete app-specified number of tasks on input queue' do
|
101
|
+
exps = [
|
102
|
+
{ 'What is your name?' => 'Sir Lancelot',
|
103
|
+
'What is your quest?' => 'To seek the holy grail',
|
104
|
+
'What is your favorite color?' => 'blue' },
|
105
|
+
{ 'What is your name?' => 'Sir Robin',
|
106
|
+
'What is your quest?' => 'To seek the holy grail',
|
107
|
+
'What is the capital of Assyria?' => nil },
|
108
|
+
{ 'What is your name?' => 'Galahad',
|
109
|
+
'What is your quest?' => 'To seek the grail',
|
110
|
+
'What is your favorite color?' => ['blue','yellow'] },
|
111
|
+
{ 'What is your name?' => 'Arthur',
|
112
|
+
'What is your quest?' => 'To seek the holy grail',
|
113
|
+
'What is the air-speed velocity of an unladen swallow?' => 'African or European swallow?' }
|
114
|
+
]
|
115
|
+
|
116
|
+
populate_queue! exps
|
117
|
+
|
118
|
+
TQ::Shell.new(@app, logger).call( shell_args )
|
119
|
+
|
120
|
+
assert_tasks_on_queue(exps.length - app_stdin_num_tasks)
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
setup_test_logger!
|
128
|
+
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tq
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Eric Gjertsen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-04-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: google-api-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.8.4
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.8.4
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 5.5.1
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 5.5.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: parallel
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.4.1
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.4.1
|
55
|
+
description: Provides a simple framework for writing task worker processes
|
56
|
+
email: ericgj72@gmail.com
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- ".gems"
|
62
|
+
- lib/tq.rb
|
63
|
+
- lib/tq/app.rb
|
64
|
+
- lib/tq/logger.rb
|
65
|
+
- lib/tq/queue.rb
|
66
|
+
- lib/tq/shell.rb
|
67
|
+
- lib/version.rb
|
68
|
+
- test/helper.rb
|
69
|
+
- test/suite.rb
|
70
|
+
- test/test_auth.rb
|
71
|
+
- test/test_logger.rb
|
72
|
+
- test/test_run.rb
|
73
|
+
- test/test_shell.rb
|
74
|
+
homepage: https://github.com/ericgj/tq
|
75
|
+
licenses:
|
76
|
+
- MIT
|
77
|
+
metadata: {}
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
requirements: []
|
93
|
+
rubyforge_project:
|
94
|
+
rubygems_version: 2.4.5
|
95
|
+
signing_key:
|
96
|
+
specification_version: 4
|
97
|
+
summary: Ruby client for Google App Engine TaskQueue (REST API)
|
98
|
+
test_files: []
|