qm 1.1.11 → 1.1.12
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 +4 -4
- data/README.md +5 -3
- data/lib/defs-mongo.rb +84 -0
- data/lib/qm.rb +37 -22
- data/lib/service.rb +133 -0
- metadata +52 -9
- data/lib/api-server.rb +0 -198
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c404fd6af1628ffecc1b74c37bb6ea79510a0838
|
4
|
+
data.tar.gz: a5ce742f60e5b7234af3019321d8472a1babdeed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e808c306e2e806c964c5425eeb1925ad9795ab4afaa52b0d54c4528c62353daf506c9637b5f329d9559fc0e6b6aab322ece6b0d2901dc6105c4b0c39ebbe88c2
|
7
|
+
data.tar.gz: 1ad66076c0d3b6b516fb71ae9ab15918562add2176eb941d29cd3a981801e900995aac6299b201698d9000efb844b3da84101a5a7f63c456e5e9447178c4fad4
|
data/README.md
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
# qm
|
2
2
|
|
3
3
|
The `qm` gem for [Ruby](http://www.ruby-lang.org/) implements both the API
|
4
|
-
server and web server components of [QMachine](https://www.qmachine.org) (QM)
|
5
|
-
|
4
|
+
server and web server components of [QMachine](https://www.qmachine.org) (QM).
|
5
|
+
The gem currently only supports [MongoDB](http://www.mongodb.org/) for
|
6
|
+
persistent storage, although the repository contains working definitions for
|
7
|
+
[SQLite](https://www.sqlite.org/).
|
6
8
|
|
7
9
|
Install
|
8
10
|
-------
|
9
11
|
|
10
12
|
To install the latest release, run
|
11
13
|
|
12
|
-
gem install qm
|
14
|
+
$ gem install qm
|
13
15
|
|
14
16
|
===
|
15
17
|
|
data/lib/defs-mongo.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
#- Ruby source code
|
2
|
+
|
3
|
+
#- defs-mongo.rb ~~
|
4
|
+
# ~~ (c) SRW, 16 Jul 2014
|
5
|
+
# ~~ last updated 18 Jul 2014
|
6
|
+
|
7
|
+
require 'bson'
|
8
|
+
require 'json'
|
9
|
+
require 'mongo'
|
10
|
+
require 'sinatra/base'
|
11
|
+
require 'uri'
|
12
|
+
|
13
|
+
module Sinatra
|
14
|
+
|
15
|
+
module MongoConnect
|
16
|
+
|
17
|
+
def mongo_connect()
|
18
|
+
# This function needs documentation.
|
19
|
+
db = URI.parse(settings.persistent_storage[:mongo])
|
20
|
+
db_name = db.path.gsub(/^\//, '')
|
21
|
+
conn = Mongo::Connection.new(db.host, db.port).db(db_name)
|
22
|
+
unless db.user.nil? or db.password.nil?
|
23
|
+
conn.authenticate(db.user, db.password)
|
24
|
+
end
|
25
|
+
set db: conn
|
26
|
+
settings.db['avars'].ensure_index('box_status', {
|
27
|
+
background: true,
|
28
|
+
sparse: true
|
29
|
+
})
|
30
|
+
settings.db['avars'].ensure_index('exp_date', {
|
31
|
+
expireAfterSeconds: settings.avar_ttl
|
32
|
+
})
|
33
|
+
return
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
module MongoDefs
|
39
|
+
|
40
|
+
def get_avar(params)
|
41
|
+
# This helper function needs documentation.
|
42
|
+
bk, db = "#{params[0]}&#{params[1]}", settings.db
|
43
|
+
x = db['avars'].find_one({_id: bk})
|
44
|
+
y = (x.nil?) ? '{}' : x['body']
|
45
|
+
return y
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_list(params)
|
49
|
+
# This helper function needs documentation.
|
50
|
+
bs, db, x = "#{params[0]}&#{params[1]}", settings.db, []
|
51
|
+
db['avars'].find({box_status: bs}).each do |doc|
|
52
|
+
# This block needs documentation.
|
53
|
+
x.push(doc['key'])
|
54
|
+
end
|
55
|
+
y = (x.length == 0) ? '[]' : x.to_json
|
56
|
+
return y
|
57
|
+
end
|
58
|
+
|
59
|
+
def set_avar(params)
|
60
|
+
# This helper function needs documentation.
|
61
|
+
db = settings.db
|
62
|
+
doc = {
|
63
|
+
_id: "#{params[0]}&#{params[1]}",
|
64
|
+
body: params[2],
|
65
|
+
exp_date: Time.now,
|
66
|
+
key: params[1]
|
67
|
+
}
|
68
|
+
options = {upsert: true}#, w: 1}
|
69
|
+
if (params.length == 4) then
|
70
|
+
doc['body'] = params[3]
|
71
|
+
doc['box_status'] = "#{params[0]}&#{params[2]}"
|
72
|
+
end
|
73
|
+
db['avars'].update({_id: doc[:_id]}, doc, options)
|
74
|
+
return
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
helpers MongoDefs
|
80
|
+
register MongoConnect
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
#- vim:set syntax=ruby:
|
data/lib/qm.rb
CHANGED
@@ -2,34 +2,49 @@
|
|
2
2
|
|
3
3
|
#- qm.rb ~~
|
4
4
|
# ~~ (c) SRW, 12 Apr 2013
|
5
|
-
# ~~ last updated
|
5
|
+
# ~~ last updated 18 Jul 2014
|
6
6
|
|
7
7
|
module QM
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
class QM_Client
|
12
|
-
|
13
|
-
def initialize
|
14
|
-
# This function needs documentation.
|
15
|
-
# ...
|
16
|
-
end
|
17
|
-
|
18
|
-
end
|
19
|
-
|
20
|
-
public
|
21
|
-
|
22
|
-
def self::launch_client()
|
9
|
+
def self::launch_client(options = {})
|
23
10
|
# This function needs documentation.
|
24
|
-
puts '(placeholder: `launch_client`)'
|
25
|
-
return
|
11
|
+
puts '(placeholder: `launch_client`)'
|
12
|
+
return
|
26
13
|
end
|
27
14
|
|
28
|
-
def self::launch_service(
|
29
|
-
# This function
|
30
|
-
|
31
|
-
|
32
|
-
|
15
|
+
def self::launch_service(options = {})
|
16
|
+
# This function creates, configures, and launches a fresh Sinatra app
|
17
|
+
# that inherits from the original "teaching version".
|
18
|
+
require 'service.rb'
|
19
|
+
require 'defs-mongo'
|
20
|
+
#require 'defs-sqlite'
|
21
|
+
app = Sinatra.new(QMachineService) do
|
22
|
+
register Sinatra::MongoConnect
|
23
|
+
#register Sinatra::SQLiteConnect
|
24
|
+
configure do
|
25
|
+
convert = lambda do |x|
|
26
|
+
# This converts all keys in a hash to symbols recursively.
|
27
|
+
if (x.is_a?(Hash)) then
|
28
|
+
x = x.inject({}) do |memo, (k, v)|
|
29
|
+
memo[k.to_sym] = convert.call(v)
|
30
|
+
memo
|
31
|
+
end
|
32
|
+
end
|
33
|
+
return x
|
34
|
+
end
|
35
|
+
options = convert.call(options)
|
36
|
+
set options
|
37
|
+
set bind: :hostname, run: false, static: :enable_web_server
|
38
|
+
if (settings.persistent_storage.has_key?(:mongo)) then
|
39
|
+
helpers Sinatra::MongoDefs
|
40
|
+
mongo_connect
|
41
|
+
#elsif (settings.persistent_storage.has_key?(:sqlite)) then
|
42
|
+
# helpers Sinatra::SQLiteDefs
|
43
|
+
# sqlite_connect
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
return app.run!
|
33
48
|
end
|
34
49
|
|
35
50
|
end
|
data/lib/service.rb
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
#- Ruby source code
|
2
|
+
|
3
|
+
#- service.rb ~~
|
4
|
+
#
|
5
|
+
# This file is derived from the original "teaching version" of QMachine,
|
6
|
+
# which used Sinatra and SQLite in a self-contained way. Where that version
|
7
|
+
# sought to abbreviate the original Node.js codebase as succinctly as
|
8
|
+
# possible, this version attempts to provide a more similar interface and
|
9
|
+
# level of configurability. Performance will *never* be a priority in the
|
10
|
+
# Ruby port.
|
11
|
+
#
|
12
|
+
# NOTE: Using a "%" character incorrectly in a URL will cause you great
|
13
|
+
# anguish, and there isn't a good way for me to handle this problem "softly"
|
14
|
+
# because it is the expected behavior (http://git.io/bmKr2w). Thus, you will
|
15
|
+
# tend to see "Bad Request" on your screen if you insist on using "%" as part
|
16
|
+
# of a 'box', 'key', or 'status' value.
|
17
|
+
#
|
18
|
+
# ~~ (c) SRW, 24 Apr 2013
|
19
|
+
# ~~ last updated 17 Jul 2014
|
20
|
+
|
21
|
+
require 'sinatra'
|
22
|
+
require 'sinatra/cross_origin'
|
23
|
+
|
24
|
+
class QMachineService < Sinatra::Base
|
25
|
+
|
26
|
+
register Sinatra::CrossOrigin
|
27
|
+
|
28
|
+
configure do
|
29
|
+
|
30
|
+
# QMachine options
|
31
|
+
|
32
|
+
set avar_ttl: 86400, # seconds
|
33
|
+
enable_api_server: false,
|
34
|
+
enable_CORS: false,
|
35
|
+
enable_web_server: false,
|
36
|
+
hostname: '0.0.0.0',
|
37
|
+
persistent_storage: {},
|
38
|
+
port: 8177,
|
39
|
+
public_folder: 'public'
|
40
|
+
|
41
|
+
# Sinatra mappings and options needed by QMachine.
|
42
|
+
|
43
|
+
mime_type webapp: 'application/x-web-app-manifest+json'
|
44
|
+
set bind: :hostname, run: false, static: :enable_web_server
|
45
|
+
|
46
|
+
# See also: http://www.sinatrarb.com/configuration.html
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
error do
|
51
|
+
# This "route" handles errors that occur as part of the server-side code.
|
52
|
+
hang_up
|
53
|
+
end
|
54
|
+
|
55
|
+
helpers do
|
56
|
+
# This block defines "subfunctions" for use inside the route definitions.
|
57
|
+
# The most important ones are the three functions for interacting with
|
58
|
+
# persistent storage: `get_avar`, `get_list`, and `set_avar`. Those three
|
59
|
+
# functions are not defined here -- they are defined separately in modules
|
60
|
+
# that are loaded at runtime by `QM::launch_service`.
|
61
|
+
|
62
|
+
def hang_up
|
63
|
+
# This helper method "hangs up" on a request by sending a nondescript
|
64
|
+
# 444 response back to the client, a convention taken from nginx.
|
65
|
+
halt [444, {'Content-Type' => 'text/plain'}, ['']]
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
not_found do
|
71
|
+
# This "route" handles requests that didn't match.
|
72
|
+
hang_up
|
73
|
+
end
|
74
|
+
|
75
|
+
# Route definitions
|
76
|
+
|
77
|
+
before '/*/*' do |version, box|
|
78
|
+
# When any request matches the pattern given, this block will execute
|
79
|
+
# before the route that corresponds to its HTTP method. The code here
|
80
|
+
# will validate the request's parameters and store them as instance
|
81
|
+
# variables that will be available to the corresponding route's code.
|
82
|
+
@box, @key, @status = box, params[:key], params[:status]
|
83
|
+
hang_up unless (settings.enable_api_server?) and
|
84
|
+
((version == 'box') or (version == 'v1')) and
|
85
|
+
(@box.match(/^[\w\-]+$/)) and
|
86
|
+
((@key.is_a?(String) and @key.match(/^[A-Za-z0-9]+$/)) or
|
87
|
+
(@status.is_a?(String) and @status.match(/^[A-Za-z0-9]+$/)))
|
88
|
+
cross_origin if settings.enable_CORS?
|
89
|
+
end
|
90
|
+
|
91
|
+
get '/:version/:box' do
|
92
|
+
# This route responds to API calls that "read" from persistent storage,
|
93
|
+
# such as when checking for new tasks to run or downloading results.
|
94
|
+
hang_up unless (@key.is_a?(String) ^ @status.is_a?(String))
|
95
|
+
if @key.is_a?(String) then
|
96
|
+
# This arm runs when a client requests the value of a specific avar.
|
97
|
+
y = get_avar([@box, @key])
|
98
|
+
else
|
99
|
+
# This arm runs when a client requests a task queue.
|
100
|
+
y = get_list([@box, @status])
|
101
|
+
end
|
102
|
+
return [200, {'Content-Type' => 'application/json'}, [y]]
|
103
|
+
end
|
104
|
+
|
105
|
+
post '/:version/:box' do
|
106
|
+
# This route responds to API calls that "write" to persistent storage,
|
107
|
+
# such as when uploading results or submitting new tasks.
|
108
|
+
hang_up unless @key.is_a?(String) and not @status.is_a?(String)
|
109
|
+
body = request.body.read
|
110
|
+
x = JSON.parse(body)
|
111
|
+
hang_up unless (@box == x['box']) and (@key == x['key'])
|
112
|
+
if x['status'].is_a?(String) then
|
113
|
+
# This arm runs only when a client writes an avar which represents a
|
114
|
+
# task description.
|
115
|
+
hang_up unless x['status'].match(/^[A-Za-z0-9]+$/)
|
116
|
+
set_avar([@box, @key, x['status'], body])
|
117
|
+
else
|
118
|
+
# This arm runs when a client is writing a "regular avar".
|
119
|
+
set_avar([@box, @key, body])
|
120
|
+
end
|
121
|
+
return [201, {'Content-Type' => 'text/plain'}, ['']]
|
122
|
+
end
|
123
|
+
|
124
|
+
get '/' do
|
125
|
+
# This route enables a static index page to be served from the public
|
126
|
+
# folder, if and only if QM's web server has been enabled.
|
127
|
+
hang_up unless settings.enable_web_server?
|
128
|
+
send_file(File.join(settings.public_folder, 'index.html'))
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
#- vim:set syntax=ruby:
|
metadata
CHANGED
@@ -1,15 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: qm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sean Wilkinson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-07-
|
11
|
+
date: 2014-07-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bson
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.10.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.10.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bson_ext
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.10.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.10.2
|
13
41
|
- !ruby/object:Gem::Dependency
|
14
42
|
name: json
|
15
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -24,6 +52,20 @@ dependencies:
|
|
24
52
|
- - '='
|
25
53
|
- !ruby/object:Gem::Version
|
26
54
|
version: 1.8.1
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: mongo
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.10.2
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.10.2
|
27
69
|
- !ruby/object:Gem::Dependency
|
28
70
|
name: sinatra
|
29
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -39,19 +81,19 @@ dependencies:
|
|
39
81
|
- !ruby/object:Gem::Version
|
40
82
|
version: 1.4.5
|
41
83
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
84
|
+
name: sinatra-cross_origin
|
43
85
|
requirement: !ruby/object:Gem::Requirement
|
44
86
|
requirements:
|
45
87
|
- - '='
|
46
88
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
89
|
+
version: 0.3.2
|
48
90
|
type: :runtime
|
49
91
|
prerelease: false
|
50
92
|
version_requirements: !ruby/object:Gem::Requirement
|
51
93
|
requirements:
|
52
94
|
- - '='
|
53
95
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
96
|
+
version: 0.3.2
|
55
97
|
- !ruby/object:Gem::Dependency
|
56
98
|
name: thin
|
57
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,7 +108,7 @@ dependencies:
|
|
66
108
|
- - '='
|
67
109
|
- !ruby/object:Gem::Version
|
68
110
|
version: 1.6.2
|
69
|
-
description: This is
|
111
|
+
description: This is a port of the QMachine web service.
|
70
112
|
email: sean@mathbiol.org
|
71
113
|
executables: []
|
72
114
|
extensions: []
|
@@ -76,11 +118,12 @@ extra_rdoc_files:
|
|
76
118
|
files:
|
77
119
|
- LICENSE
|
78
120
|
- README.md
|
79
|
-
- lib/
|
121
|
+
- lib/defs-mongo.rb
|
80
122
|
- lib/qm.rb
|
123
|
+
- lib/service.rb
|
81
124
|
homepage: https://www.qmachine.org
|
82
125
|
licenses:
|
83
|
-
- Apache
|
126
|
+
- Apache-2.0
|
84
127
|
metadata:
|
85
128
|
issue_tracker: https://github.com/qmachine/qm-ruby/issues
|
86
129
|
post_install_message:
|
@@ -99,7 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
99
142
|
version: '0'
|
100
143
|
requirements: []
|
101
144
|
rubyforge_project:
|
102
|
-
rubygems_version: 2.
|
145
|
+
rubygems_version: 2.4.1
|
103
146
|
signing_key:
|
104
147
|
specification_version: 4
|
105
148
|
summary: A platform for World Wide Computing
|
data/lib/api-server.rb
DELETED
@@ -1,198 +0,0 @@
|
|
1
|
-
#- Ruby source code / Rack configuration file
|
2
|
-
|
3
|
-
#- api-server.rb ~~
|
4
|
-
#
|
5
|
-
# This file is a hacked-up version of the "teaching version" of QMachine,
|
6
|
-
# and the rest of this introduction reflects that.
|
7
|
-
#
|
8
|
-
# This is a self-contained Rack app that uses Sinatra's domain-specific
|
9
|
-
# language (DSL) in tandem with SQLite to implement a teaching version of
|
10
|
-
# QMachine. The idea here is to pack most of the functionality of the
|
11
|
-
# original Node.js codebase into a single file that reads like pseudo-code.
|
12
|
-
#
|
13
|
-
# Of course, there are some caveats. This version succeeds in abbreviating
|
14
|
-
# the original codebase, but it doesn't support all of the original options
|
15
|
-
# yet. The code can also be hard to modify if you're unfamiliar with Sinatra,
|
16
|
-
# because Ruby's scoping rules are very different from JavaScript's, and
|
17
|
-
# Sinatra's DSL makes things even "worse", to be honest. My advice here is,
|
18
|
-
# don't think too hard about it. Just enjoy it.
|
19
|
-
#
|
20
|
-
# I do plan to merge this program with the Ruby gem in the future, which is
|
21
|
-
# why the database schema matches the Node.js implementation's (which is not
|
22
|
-
# as straight-forward as it could be). For now, it serves its purpose, and it
|
23
|
-
# does so with just 95 lines of source code ;-)
|
24
|
-
#
|
25
|
-
# NOTE: Using a "%" character incorrectly in a URL will cause you great
|
26
|
-
# anguish, and there isn't a good way for me to handle this problem "softly"
|
27
|
-
# because it is the expected behavior (http://git.io/bmKr2w). Thus, you will
|
28
|
-
# tend to see "Bad Request" on your screen if you insist on using "%" as part
|
29
|
-
# of a 'box', 'key', or 'status' value.
|
30
|
-
#
|
31
|
-
# ~~ (c) SRW, 24 Apr 2013
|
32
|
-
# ~~ last updated 13 Jul 2014
|
33
|
-
|
34
|
-
require 'json'
|
35
|
-
require 'sinatra'
|
36
|
-
#require 'sinatra/cross_origin'
|
37
|
-
require 'sqlite3'
|
38
|
-
|
39
|
-
class QMachineService < Sinatra::Base
|
40
|
-
|
41
|
-
configure do
|
42
|
-
|
43
|
-
# QMachine options
|
44
|
-
|
45
|
-
set avar_ttl: 86400, # seconds
|
46
|
-
enable_api_server: true,
|
47
|
-
enable_CORS: true,
|
48
|
-
enable_web_server: true,
|
49
|
-
hostname: '0.0.0.0',
|
50
|
-
persistent_storage: 'qm.db',
|
51
|
-
port: ENV['PORT'] || 8177,
|
52
|
-
public_folder: 'public'
|
53
|
-
|
54
|
-
# Sinatra mappings and options needed by QMachine -- leave these alone ;-)
|
55
|
-
|
56
|
-
mime_type webapp: 'application/x-web-app-manifest+json'
|
57
|
-
set bind: :hostname, run: false, static: :enable_web_server
|
58
|
-
|
59
|
-
# See also: http://www.sinatrarb.com/configuration.html
|
60
|
-
|
61
|
-
end
|
62
|
-
|
63
|
-
error do
|
64
|
-
# This "route" handles errors that occur as part of the server-side code.
|
65
|
-
hang_up
|
66
|
-
end
|
67
|
-
|
68
|
-
helpers do
|
69
|
-
# This block defines subfunctions for use inside the route definitions.
|
70
|
-
|
71
|
-
def sqlite(query)
|
72
|
-
# This helper method helps DRY out the code for database queries, and
|
73
|
-
# it does so in an incredibly robust and inefficient way -- by
|
74
|
-
# creating the table and evicting expired rows before every single
|
75
|
-
# query. A caveat, of course, is that the special ":memory:" database
|
76
|
-
# doesn't work correctly, but ":memory:" isn't *persistent* storage
|
77
|
-
# anyway. Also, I have omitted indexing on `box_status` for obvious
|
78
|
-
# reasons :-P
|
79
|
-
begin
|
80
|
-
db = SQLite3::Database.open(settings.persistent_storage)
|
81
|
-
db.execute_batch <<-sql
|
82
|
-
CREATE TABLE IF NOT EXISTS avars (
|
83
|
-
body TEXT NOT NULL,
|
84
|
-
box_key TEXT NOT NULL PRIMARY KEY,
|
85
|
-
box_status TEXT,
|
86
|
-
exp_date INTEGER NOT NULL,
|
87
|
-
key TEXT
|
88
|
-
);
|
89
|
-
DELETE FROM avars WHERE (exp_date < #{now_plus(0)})
|
90
|
-
sql
|
91
|
-
# We have to execute the query code `query` separately because
|
92
|
-
# the `db.execute_batch` function always returns `nil`, which
|
93
|
-
# prevents us from being able to retrieve the results of the
|
94
|
-
# query.
|
95
|
-
x = db.execute(query)
|
96
|
-
rescue SQLite3::Exception => err
|
97
|
-
puts "Exception occurred: #{err}"
|
98
|
-
ensure
|
99
|
-
db.close if db
|
100
|
-
end
|
101
|
-
return x
|
102
|
-
end
|
103
|
-
|
104
|
-
def hang_up
|
105
|
-
# This helper method "hangs up" on a request by sending a nondescript
|
106
|
-
# 444 response back to the client, a convention taken from nginx.
|
107
|
-
halt [444, {'Content-Type' => 'text/plain'}, ['']]
|
108
|
-
end
|
109
|
-
|
110
|
-
def now_plus(dt)
|
111
|
-
# This helper method computes a date (in milliseconds) that is
|
112
|
-
# specified by an offset `dt` (in seconds).
|
113
|
-
return (1000 * (Time.now.to_f + dt)).to_i
|
114
|
-
end
|
115
|
-
|
116
|
-
end
|
117
|
-
|
118
|
-
not_found do
|
119
|
-
# This "route" handles requests that didn't match.
|
120
|
-
hang_up
|
121
|
-
end
|
122
|
-
|
123
|
-
if settings.enable_api_server? then
|
124
|
-
|
125
|
-
# Here, we set up "routes" to handle incoming GET and POST requests.
|
126
|
-
|
127
|
-
before '/*/*' do |version, box|
|
128
|
-
# When any request matches the pattern given, this block will execute
|
129
|
-
# before the route that corresponds to its HTTP method. The code here
|
130
|
-
# will validate the request's parameters and store them as instance
|
131
|
-
# variables that will be available to the corresponding route's code.
|
132
|
-
@box, @key, @status = box, params[:key], params[:status]
|
133
|
-
hang_up unless ((version == 'box') or (version == 'v1')) and
|
134
|
-
(@box.match(/^[\w\-]+$/)) and
|
135
|
-
((@key.is_a?(String) and @key.match(/^[A-Za-z0-9]+$/)) or
|
136
|
-
(@status.is_a?(String) and @status.match(/^[A-Za-z0-9]+$/)))
|
137
|
-
#cross_origin if settings.enable_CORS?
|
138
|
-
end
|
139
|
-
|
140
|
-
get '/:version/:box' do
|
141
|
-
# This route responds to API calls that "read" from persistent
|
142
|
-
# storage, such as when checking for new tasks to run or downloading
|
143
|
-
# results.
|
144
|
-
hang_up unless (@key.is_a?(String) ^ @status.is_a?(String))
|
145
|
-
bk, bs = "#{@box}&#{@key}", "#{@box}&#{@status}"
|
146
|
-
if @key.is_a?(String) then
|
147
|
-
# This arm runs when a client requests the value of a specific
|
148
|
-
# avar.
|
149
|
-
x = sqlite("SELECT body FROM avars WHERE box_key = '#{bk}'")
|
150
|
-
y = (x.length == 0) ? '{}' : x[0][0]
|
151
|
-
else
|
152
|
-
# This arm runs when a client requests a task queue.
|
153
|
-
x = sqlite("SELECT key FROM avars WHERE box_status = '#{bs}'")
|
154
|
-
y = (x.length == 0) ? '[]' : (x.map {|row| row[0]}).to_json
|
155
|
-
end
|
156
|
-
return [200, {'Content-Type' => 'application/json'}, [y]]
|
157
|
-
end
|
158
|
-
|
159
|
-
post '/:version/:box' do
|
160
|
-
# This route responds to API calls that "write" to persistent
|
161
|
-
# storage, such as when uploading results or submitting new tasks.
|
162
|
-
hang_up unless @key.is_a?(String) and not @status.is_a?(String)
|
163
|
-
body, ed = request.body.read, now_plus(settings.avar_ttl)
|
164
|
-
x = JSON.parse(body)
|
165
|
-
hang_up unless (@box == x['box']) and (@key == x['key'])
|
166
|
-
bk, bs = "#{@box}&#{@key}", "#{@box}&#{x['status']}"
|
167
|
-
if x['status'].is_a?(String) then
|
168
|
-
# This arm runs only when a client writes an avar which
|
169
|
-
# represents a task description.
|
170
|
-
hang_up unless x['status'].match(/^[A-Za-z0-9]+$/)
|
171
|
-
sqlite("INSERT OR REPLACE INTO avars
|
172
|
-
(body, box_key, box_status, exp_date, key)
|
173
|
-
VALUES ('#{body}', '#{bk}', '#{bs}', #{ed}, '#{@key}')")
|
174
|
-
else
|
175
|
-
# This arm runs when a client is writing a "regular avar".
|
176
|
-
sqlite("INSERT OR REPLACE INTO avars (body, box_key, exp_date)
|
177
|
-
VALUES ('#{body}', '#{bk}', #{ed})")
|
178
|
-
end
|
179
|
-
return [201, {'Content-Type' => 'text/plain'}, ['']]
|
180
|
-
end
|
181
|
-
|
182
|
-
end
|
183
|
-
|
184
|
-
if settings.enable_web_server? then
|
185
|
-
|
186
|
-
get '/' do
|
187
|
-
# This route enables a static index page to be served from the public
|
188
|
-
# folder, if and only if QM's web server has been enabled.
|
189
|
-
send_file(File.join(settings.public_folder, 'index.html'))
|
190
|
-
end
|
191
|
-
|
192
|
-
end
|
193
|
-
|
194
|
-
end
|
195
|
-
|
196
|
-
QMachineService.run!
|
197
|
-
|
198
|
-
#- vim:set syntax=ruby:
|