tomdoc 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.md +104 -0
- data/Rakefile +80 -0
- data/bin/tomdoc +6 -0
- data/lib/tomdoc.rb +25 -0
- data/lib/tomdoc/arg.rb +14 -0
- data/lib/tomdoc/cli.rb +150 -0
- data/lib/tomdoc/generator.rb +138 -0
- data/lib/tomdoc/generators/console.rb +68 -0
- data/lib/tomdoc/generators/html.rb +42 -0
- data/lib/tomdoc/method.rb +21 -0
- data/lib/tomdoc/scope.rb +46 -0
- data/lib/tomdoc/source_parser.rb +145 -0
- data/lib/tomdoc/tomdoc.rb +133 -0
- data/lib/tomdoc/version.rb +3 -0
- data/man/tomdoc.5 +320 -0
- data/man/tomdoc.5.html +285 -0
- data/man/tomdoc.5.ronn +203 -0
- data/test/console_generator_test.rb +20 -0
- data/test/fixtures/chimney.rb +711 -0
- data/test/fixtures/multiplex.rb +47 -0
- data/test/fixtures/simple.rb +10 -0
- data/test/generator_test.rb +47 -0
- data/test/helper.rb +21 -0
- data/test/html_generator_test.rb +18 -0
- data/test/source_parser_test.rb +66 -0
- data/test/tomdoc_parser_test.rb +127 -0
- metadata +143 -0
data/man/tomdoc.5.ronn
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
TomDoc for Ruby - Version 0.9.0
|
2
|
+
===============================
|
3
|
+
|
4
|
+
Purpose
|
5
|
+
-------
|
6
|
+
|
7
|
+
TomDoc is a code documentation specification that helps you write precise
|
8
|
+
documentation that is nice to read in plain text, yet structured enough to be
|
9
|
+
automatically extracted and processed by a machine.
|
10
|
+
|
11
|
+
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD",
|
12
|
+
"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be
|
13
|
+
interpreted as described in RFC 2119.
|
14
|
+
|
15
|
+
|
16
|
+
Class/Module Documentation
|
17
|
+
--------------------------
|
18
|
+
|
19
|
+
TomDoc for classes and modules consists of a block of single comment markers
|
20
|
+
(#) that appear directly above the class/module definition. Lines SHOULD be
|
21
|
+
wrapped at 80 characters. Lines that contain text MUST be separated from the
|
22
|
+
comment marker by a single space. Lines that do not contain text SHOULD
|
23
|
+
consist of just a comment marker (no trailing spaces).
|
24
|
+
|
25
|
+
Code examples SHOULD be indented two spaces (three spaces from the comment
|
26
|
+
marker).
|
27
|
+
|
28
|
+
# Various methods useful for performing mathematical operations. All
|
29
|
+
# methods are module methods and should be called on the Math module.
|
30
|
+
# For example:
|
31
|
+
#
|
32
|
+
# Math.square_root(9)
|
33
|
+
# # => 3
|
34
|
+
#
|
35
|
+
module Math
|
36
|
+
...
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
Method Documentation
|
41
|
+
--------------------
|
42
|
+
|
43
|
+
A quick example will serve to best illustrate the TomDoc method documentation
|
44
|
+
format:
|
45
|
+
|
46
|
+
# Duplicate some text an abitrary number of times.
|
47
|
+
#
|
48
|
+
# text - The String to be duplicated.
|
49
|
+
# count - The Integer number of times to duplicate the text.
|
50
|
+
#
|
51
|
+
# Examples
|
52
|
+
#
|
53
|
+
# multiplex('Tom', 4)
|
54
|
+
# # => 'TomTomTomTom'
|
55
|
+
#
|
56
|
+
# Returns the duplicated String.
|
57
|
+
def multiplex(text, count)
|
58
|
+
text * count
|
59
|
+
end
|
60
|
+
|
61
|
+
TomDoc for a specific method consists of a block of single comment markers (#)
|
62
|
+
that appears directly above the method. There MUST NOT be a blank line between
|
63
|
+
the comment block and the method definition. A TomDoc method block consists of
|
64
|
+
a description section (required), an arguments section (required if the method
|
65
|
+
takes any arguments), an examples section (optional), and a returns section
|
66
|
+
(required). Lines that contain text MUST be separated from the comment
|
67
|
+
marker by a single space. Lines that do not contain text SHOULD consist of
|
68
|
+
just a comment marker (no trailing spaces).
|
69
|
+
|
70
|
+
### The Description Section
|
71
|
+
|
72
|
+
The description section SHOULD be in plain sentences. Each sentence SHOULD end
|
73
|
+
with a period. Good descriptions explain what the code does at a high level.
|
74
|
+
Make sure to explain any unexpected behavior that the method may have, or any
|
75
|
+
pitfalls that the user may experience. Lines SHOULD be wrapped at 80
|
76
|
+
characters.
|
77
|
+
|
78
|
+
If a method's description begins with "Public:" then that method will be
|
79
|
+
considered part of the project's public API. For example:
|
80
|
+
|
81
|
+
# Public: Initialize a new Widget.
|
82
|
+
|
83
|
+
This annotation is designed to let developers know which methods are
|
84
|
+
considered stable. You SHOULD use this to document the public API of your
|
85
|
+
project. This information can then be used along with [Semantic
|
86
|
+
Versioning](http://semver.org) to inform decisions on when major, minor, and
|
87
|
+
patch versions should be incremented.
|
88
|
+
|
89
|
+
If a method's description begins with "Deprecated:" then that method will be
|
90
|
+
considered as deprecated and users will know that it will be removed in a
|
91
|
+
future version.
|
92
|
+
|
93
|
+
### The Arguments Section
|
94
|
+
|
95
|
+
The arguments section consists of a list of arguments. Each list item MUST be
|
96
|
+
comprised of the name of the argument, a dash, and an explanation of the
|
97
|
+
argument in plain sentences. The expected type (or types) of each argument
|
98
|
+
SHOULD be clearly indicated in the explanation. When you specify a type, use
|
99
|
+
the proper classname of the type (for instance, use 'String' instead of
|
100
|
+
'string' to refer to a String type). The dashes following each argument name
|
101
|
+
should be lined up in a single column. Lines SHOULD be wrapped at 80 columns.
|
102
|
+
If an explanation is longer than that, additional lines MUST be indented at
|
103
|
+
least two spaces but SHOULD be indented to match the indentation of the
|
104
|
+
explanation. For example:
|
105
|
+
|
106
|
+
# element - The Symbol representation of the element. The Symbol should
|
107
|
+
# contain only lowercase ASCII alpha characters.
|
108
|
+
|
109
|
+
All arguments are assumed to be required. If an argument is optional, you MUST
|
110
|
+
specify the default value:
|
111
|
+
|
112
|
+
# host - The String hostname to bind (default: '0.0.0.0').
|
113
|
+
|
114
|
+
For hash arguments, you SHOULD enumerate each valid option in a way similar
|
115
|
+
to how normal arguments are defined:
|
116
|
+
|
117
|
+
# options - The Hash options used to refine the selection (default: {}):
|
118
|
+
# :color - The String color to restrict by (optional).
|
119
|
+
# :weight - The Float weight to restrict by. The weight should
|
120
|
+
# be specified in grams (optional).
|
121
|
+
|
122
|
+
### The Examples Section
|
123
|
+
|
124
|
+
The examples section MUST start with the word "Examples" on a line by
|
125
|
+
itself. The next line SHOULD be blank. The following lines SHOULD be indented
|
126
|
+
by two spaces (three spaces from the initial comment marker) and contain code
|
127
|
+
that shows off how to call the method and (optional) examples of what it
|
128
|
+
returns. Everything under the "Examples" line should be considered code, so
|
129
|
+
make sure you comment out lines that show return values. Separate examples
|
130
|
+
should be separated by a blank line. For example:
|
131
|
+
|
132
|
+
# Examples
|
133
|
+
#
|
134
|
+
# multiplex('x', 4)
|
135
|
+
# # => 'xxxx'
|
136
|
+
#
|
137
|
+
# multiplex('apple', 2)
|
138
|
+
# # => 'appleapple'
|
139
|
+
|
140
|
+
### The Returns Section
|
141
|
+
|
142
|
+
The returns section should explain in plain sentences what is returned from
|
143
|
+
the method. The line MUST begin with "Returns". If only a single thing is
|
144
|
+
returned, state the nature and type of the value. For example:
|
145
|
+
|
146
|
+
# Returns the duplicated String.
|
147
|
+
|
148
|
+
If several different types may be returned, list all of them. For example:
|
149
|
+
|
150
|
+
# Returns the given element Symbol or nil if none was found.
|
151
|
+
|
152
|
+
If the return value of the method is not intended to be used, then you should
|
153
|
+
simply state:
|
154
|
+
|
155
|
+
# Returns nothing.
|
156
|
+
|
157
|
+
If the method raises exceptions that the caller may be interested in, add
|
158
|
+
additional lines that explain each exception and under what conditions it may
|
159
|
+
be encountered. The lines MUST begin with "Raises". For example:
|
160
|
+
|
161
|
+
# Returns nothing.
|
162
|
+
# Raises Errno::ENOENT if the file cannot be found.
|
163
|
+
# Raises Errno::EACCES if the file cannot be accessed.
|
164
|
+
|
165
|
+
Lines SHOULD be wrapped at 80 columns. Wrapped lines MUST be indented under
|
166
|
+
the above line by at least two spaces. For example:
|
167
|
+
|
168
|
+
# Returns the atomic mass of the element as a Float. The value is in
|
169
|
+
# unified atomic mass units.
|
170
|
+
|
171
|
+
|
172
|
+
Special Considerations
|
173
|
+
----------------------
|
174
|
+
|
175
|
+
### Attributes
|
176
|
+
|
177
|
+
Ruby's built in `attr_reader`, `attr_writer`, and `attr_accessor` require a
|
178
|
+
bit more consideration. With TomDoc you SHOULD NOT use `attr_access` since it
|
179
|
+
represents two methods with different signatures. Restricting yourself in this
|
180
|
+
way also makes you think more carefully about the read vs. write behavior and
|
181
|
+
whether each should be part of the Public API.
|
182
|
+
|
183
|
+
Here is an example TomDoc for `attr_reader`.
|
184
|
+
|
185
|
+
# Public: Get the user's name.
|
186
|
+
#
|
187
|
+
# Returns the String name of the user.
|
188
|
+
attr_reader :name
|
189
|
+
|
190
|
+
Here is an example TomDoc for `attr_writer`. The parameter name should be the
|
191
|
+
same as the attribute name.
|
192
|
+
|
193
|
+
# Set the user's name.
|
194
|
+
#
|
195
|
+
# name - The String name of the user.
|
196
|
+
#
|
197
|
+
# Returns nothing.
|
198
|
+
attr_writer :name
|
199
|
+
|
200
|
+
While this approach certainly takes up more space than listing dozens of
|
201
|
+
attributes on a single line, it allows for individual documentation of each
|
202
|
+
attribute. Attributes are an extremely important part of a class and should be
|
203
|
+
treated with the same care as any other methods.
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'test/helper'
|
2
|
+
|
3
|
+
class ConsoleGeneratorTest < TomDoc::Test
|
4
|
+
def setup
|
5
|
+
@text = TomDoc::Generators::Console.generate(fixture(:simple))
|
6
|
+
end
|
7
|
+
|
8
|
+
test "works" do
|
9
|
+
assert_equal <<text, @text
|
10
|
+
--------------------------------------------------------------------------------
|
11
|
+
\e[1mSimple#string(text)\e[0m
|
12
|
+
|
13
|
+
Just a simple method.
|
14
|
+
|
15
|
+
\e[32mtext\e[0m - The \e[36mString\e[0m to return.
|
16
|
+
|
17
|
+
Returns a \e[36mString\e[0m.
|
18
|
+
text
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,711 @@
|
|
1
|
+
module Butter
|
2
|
+
class Something
|
3
|
+
end
|
4
|
+
end
|
5
|
+
|
6
|
+
module GitHub
|
7
|
+
# Sings you a poem.
|
8
|
+
#
|
9
|
+
# name - Your name as a String.
|
10
|
+
#
|
11
|
+
# Returns a String poem.
|
12
|
+
def self.poem(name)
|
13
|
+
"Roses are red, " +
|
14
|
+
"violets are blue, " +
|
15
|
+
"#{name}'s a sucker, " +
|
16
|
+
"and now you are, too."
|
17
|
+
end
|
18
|
+
|
19
|
+
# Chimney is the API for getting and setting Smoke routes.
|
20
|
+
#
|
21
|
+
# Setup
|
22
|
+
# -----
|
23
|
+
#
|
24
|
+
# In order for Chimney to function, some setup keys are required to exist in the
|
25
|
+
# routing Redis. This sections shows you how to enter the required
|
26
|
+
# information. Start by connecting to the routing Redis:
|
27
|
+
#
|
28
|
+
# require 'chimney'
|
29
|
+
# chimney = Chimney.new('router.example.com:21201')
|
30
|
+
#
|
31
|
+
# The routing Redis must contain one or more storage host values.
|
32
|
+
#
|
33
|
+
# chimney.add_storage_server('s1.example.com')
|
34
|
+
# chimney.add_storage_server('s2.example.com')
|
35
|
+
#
|
36
|
+
# Each storage host is expected to have disk usage information (percent of disk
|
37
|
+
# used) that is kept up to date (via cron or similar). If these are not set, the
|
38
|
+
# host that will be chosen for new routes is arbitrary, but will always be the
|
39
|
+
# same. This is a simple example of a cron script that is responsible for
|
40
|
+
# updating the usage keys:
|
41
|
+
#
|
42
|
+
# (0..15).map { |num| num.to_s(16) }.each do |part|
|
43
|
+
# host = get_current_host # => 's1.example.com'
|
44
|
+
# percent_used = get_partition_usage(part) # => 17.23
|
45
|
+
# chimney.set_partition_usage(host, part, percent_used)
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# Usage
|
49
|
+
# -----
|
50
|
+
#
|
51
|
+
# Make sure you require this sucker.
|
52
|
+
#
|
53
|
+
# require 'chimney'
|
54
|
+
#
|
55
|
+
# Chimney must be initialized with the host:port of the routing Redis server.
|
56
|
+
#
|
57
|
+
# chimney = Chimney.new('router.example.com:21201')
|
58
|
+
#
|
59
|
+
# Looking up a route for a user is simple. This command simply finds the host
|
60
|
+
# upon which the user is stored. If the router Redis is unreachable, Chimney
|
61
|
+
# will check its internal cache. If that is a miss, it will try to reconnect to
|
62
|
+
# the router. If that fails, it will fallback on making calls to Smoke and
|
63
|
+
# checking each storage server for the user. Subsequent lookups will then be
|
64
|
+
# able to find the route in the cache. This mechanism should ensure high
|
65
|
+
# tolerance to failures of the routing server.
|
66
|
+
#
|
67
|
+
# chimney.get_user_route('mojombo')
|
68
|
+
# # => 'domU-12-31-38-01-C8-F1.compute-1.internal'
|
69
|
+
#
|
70
|
+
# Setting a route for a new user is also a simple call. This command will first
|
71
|
+
# refresh the cached list of available storage hosts, then figure out which one
|
72
|
+
# of them is least loaded. This host will be set as the route for the user and
|
73
|
+
# returned. If the user already exists in the routing table, the host is
|
74
|
+
# returned and the routing table is unaffected.
|
75
|
+
#
|
76
|
+
# chimney.set_user_route('franko')
|
77
|
+
# # => domU-12-31-38-01-C8-F1.compute-1.internal
|
78
|
+
#
|
79
|
+
# If you need to change the name of the user, but keep the host the same:
|
80
|
+
#
|
81
|
+
# chimney.rename_user_route('oldname', 'newname')
|
82
|
+
#
|
83
|
+
# If you need to remove a route for a user:
|
84
|
+
#
|
85
|
+
# chimney.delete_user_route('mojombo')
|
86
|
+
#
|
87
|
+
# If you need the absolute path to a user on disk (class or instance method):
|
88
|
+
#
|
89
|
+
# Chimney.shard_user_path('mojombo')
|
90
|
+
# chimney.shard_user_path('mojombo')
|
91
|
+
# # => "/data/repositories/2/a8/e2/95/mojombo"
|
92
|
+
#
|
93
|
+
# If you need the absolute path to a repo on disk (class or instance method):
|
94
|
+
#
|
95
|
+
# Chimney.shard_repo_path('mojombo', 'god')
|
96
|
+
# chimney.shard_repo_path('mojombo', 'god')
|
97
|
+
# # => "/data/repositories/2/a8/e2/95/mojombo/god.git"
|
98
|
+
#
|
99
|
+
# Getting and setting routes for gists is similar to that for users:
|
100
|
+
#
|
101
|
+
# chimney.get_gist_route('1234')
|
102
|
+
# # => 'domU-12-31-38-01-C8-F1.compute-1.internal'
|
103
|
+
#
|
104
|
+
# chimney.set_gist_route('4e460bfd6c184058c7a3')
|
105
|
+
# # => 'domU-12-31-38-01-C8-F1.compute-1.internal'
|
106
|
+
#
|
107
|
+
# If you need the absolute path to a gist on disk (class or instance method):
|
108
|
+
#
|
109
|
+
# Chimney.shard_gist_path('1234')
|
110
|
+
# chimney.shard_gist_path('1234')
|
111
|
+
# # => "/data/repositories/0/81/dc/9b/gist/1234.git"
|
112
|
+
#
|
113
|
+
# If you need the unix user that has access to the repository data (class or
|
114
|
+
# instance method):
|
115
|
+
#
|
116
|
+
# Chimney.unix_user
|
117
|
+
# chimney.unix_user
|
118
|
+
# # => 'root'
|
119
|
+
#
|
120
|
+
# That's it!
|
121
|
+
class Chimney
|
122
|
+
SMOKE_HOSTS_FILE = '/tmp/smoke_hosts'
|
123
|
+
REPO_DIR = ENV['REPO_ROOT'] || '/data/repositories'
|
124
|
+
UNIX_USER = 'git'
|
125
|
+
|
126
|
+
attr_accessor :host, :port
|
127
|
+
attr_accessor :client, :hosts, :cache, :verbose, :logger
|
128
|
+
|
129
|
+
# Instantiate a new Chimney object.
|
130
|
+
#
|
131
|
+
# server - The host:port of the routing redis instance.
|
132
|
+
# logger - An optional Logger object. If none is given, Chimney
|
133
|
+
# writes to /dev/null.
|
134
|
+
#
|
135
|
+
# Returns a configured Chimney instance.
|
136
|
+
def initialize(server, logger = nil)
|
137
|
+
self.cache = {}
|
138
|
+
self.hosts = []
|
139
|
+
self.logger = logger || Logger.new('/dev/null')
|
140
|
+
|
141
|
+
self.host = server.split(':').first
|
142
|
+
self.port = server.split(':').last.to_i
|
143
|
+
ensure_client_connection
|
144
|
+
end
|
145
|
+
|
146
|
+
# Add a storage server to the list.
|
147
|
+
#
|
148
|
+
# host - The String hostname to add.
|
149
|
+
#
|
150
|
+
# Returns the Array of String hostnames after the addition.
|
151
|
+
def self.add_storage_server(host)
|
152
|
+
if current_servers = self.client.get('gh.storage.servers')
|
153
|
+
new_servers = [current_servers, host].join(',')
|
154
|
+
else
|
155
|
+
new_servers = host
|
156
|
+
end
|
157
|
+
self.client.set('gh.storage.servers', new_servers)
|
158
|
+
new_servers.split(',')
|
159
|
+
end
|
160
|
+
|
161
|
+
# Remove a storage server from the list.
|
162
|
+
#
|
163
|
+
# host - The String hostname to remove.
|
164
|
+
#
|
165
|
+
# Returns the Array of String hostnames after the removal.
|
166
|
+
# Raises Chimney::NoSuchStorageServer if the storage server is not currently
|
167
|
+
# in the list.
|
168
|
+
def remove_storage_server(host)
|
169
|
+
if current_servers = self.client.get('gh.storage.servers')
|
170
|
+
servers = current_servers.split(',')
|
171
|
+
if servers.delete(host)
|
172
|
+
self.client.set('gh.storage.servers', servers.join(','))
|
173
|
+
return servers
|
174
|
+
else
|
175
|
+
raise NoSuchStorageServer.new(host)
|
176
|
+
end
|
177
|
+
else
|
178
|
+
raise NoSuchStorageServer.new(host)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# The list of storage server hostnames.
|
183
|
+
#
|
184
|
+
# Returns an Array of String hostnames.
|
185
|
+
def storage_servers
|
186
|
+
self.client.get('gh.storage.servers').split(',')
|
187
|
+
end
|
188
|
+
|
189
|
+
# Checks if the storage server is currently online.
|
190
|
+
#
|
191
|
+
# host - The String hostname to check.
|
192
|
+
#
|
193
|
+
# Returns true if the server is online, false if not.
|
194
|
+
def storage_server_online?(host)
|
195
|
+
!self.client.exists("gh.storage.server.offline.#{host}")
|
196
|
+
rescue Errno::ECONNREFUSED
|
197
|
+
# If we can't connect to Redis, check to see if the BERTRPC
|
198
|
+
# server is alive manually.
|
199
|
+
begin
|
200
|
+
smoke(host).alive?
|
201
|
+
rescue BERTRPC::ReadTimeoutError
|
202
|
+
false
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Sets a storage server as being online.
|
207
|
+
#
|
208
|
+
# host - The String hostname to set.
|
209
|
+
#
|
210
|
+
# Returns nothing.
|
211
|
+
def set_storage_server_online(host)
|
212
|
+
self.client.delete("gh.storage.server.offline.#{host}")
|
213
|
+
end
|
214
|
+
|
215
|
+
# Sets a storage server as being offline.
|
216
|
+
#
|
217
|
+
# host - The String hostname to set.
|
218
|
+
# duration - An optional number of seconds after which the
|
219
|
+
# server will no longer be considered offline; with
|
220
|
+
# no duration, servers are kept offline until marked
|
221
|
+
# online manually.
|
222
|
+
#
|
223
|
+
# Returns true if the server was not previously offline, nil otherwise.
|
224
|
+
def set_storage_server_offline(host, duration=nil)
|
225
|
+
key = "gh.storage.server.offline.#{host}"
|
226
|
+
if self.client.set_unless_exists(key, Time.now.to_i)
|
227
|
+
self.client.expire(key, duration) if duration
|
228
|
+
true
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# If a server is offline, tells us when we first noticed.
|
233
|
+
#
|
234
|
+
# host - The String hostname to check.
|
235
|
+
#
|
236
|
+
# Returns nothing if the storage server is online.
|
237
|
+
# Returns an instance of Time representing the moment we set the
|
238
|
+
# server as offline if it is offline.
|
239
|
+
def self.storage_server_offline_since(host)
|
240
|
+
if time = self.client.get("gh.storage.server.offline.#{host}")
|
241
|
+
Time.at(time.to_i)
|
242
|
+
end
|
243
|
+
rescue Errno::ECONNREFUSED
|
244
|
+
# If we can't connect to Redis and we're wondering when the
|
245
|
+
# storage server went offline, return whatever.
|
246
|
+
Time.now
|
247
|
+
end
|
248
|
+
|
249
|
+
# Maximum number of network failures that can occur with a file server
|
250
|
+
# before it's marked offline.
|
251
|
+
DISRUPTION_THRESHOLD = 10
|
252
|
+
|
253
|
+
# The window of time, in seconds, under which no more than
|
254
|
+
# DISRUPTION_THRESHOLD failures may occur.
|
255
|
+
DISRUPTION_WINDOW = 5
|
256
|
+
|
257
|
+
# Called when some kind of network disruption occurs when communicating
|
258
|
+
# with a file server. When more than DISRUPTION_THRESHOLD failures are
|
259
|
+
# reported within DISRUPTION_WINDOW seconds, the server is marked offline
|
260
|
+
# for two minutes.
|
261
|
+
#
|
262
|
+
# The return value can be used to determine the action taken:
|
263
|
+
# nil when the storage server is already marked offline.
|
264
|
+
# > 0 when the number of disruptions is under the threshold.
|
265
|
+
# -1 when the server has been marked offline due to too many disruptions.
|
266
|
+
def storage_server_disruption(host)
|
267
|
+
return if !self.storage_server_online?(host)
|
268
|
+
key = "gh.storage.server.disrupt.#{host}"
|
269
|
+
if counter_suffix = self.client.get(key)
|
270
|
+
count = self.client.incr("#{key}.#{counter_suffix}")
|
271
|
+
if count > DISRUPTION_THRESHOLD
|
272
|
+
if self.set_storage_server_offline(host, 30)
|
273
|
+
self.client.del(key, "#{key}.#{counter_suffix}")
|
274
|
+
-1
|
275
|
+
end
|
276
|
+
else
|
277
|
+
count
|
278
|
+
end
|
279
|
+
else
|
280
|
+
if self.client.set_unless_exists(key, Time.now.to_f * 1000)
|
281
|
+
self.client.expire(key, DISRUPTION_WINDOW)
|
282
|
+
self.storage_server_disruption(host)
|
283
|
+
else
|
284
|
+
# we raced to set first and lost, wrap around and try again
|
285
|
+
self.storage_server_disruption(host)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Lookup a route for the given user.
|
291
|
+
#
|
292
|
+
# user - The String username.
|
293
|
+
#
|
294
|
+
# Returns the hostname of the storage server.
|
295
|
+
def get_user_route(user)
|
296
|
+
try_route(:user, user)
|
297
|
+
end
|
298
|
+
|
299
|
+
# Lookup a route for the given gist.
|
300
|
+
#
|
301
|
+
# gist - The String gist ID.
|
302
|
+
#
|
303
|
+
# Returns the hostname of the storage server.
|
304
|
+
def get_gist_route(gist)
|
305
|
+
try_route(:gist, gist)
|
306
|
+
end
|
307
|
+
|
308
|
+
# Find the least loaded storage server and set a route there for
|
309
|
+
# the given +user+. If the user already exists, do nothing and
|
310
|
+
# simply return the host that user is on.
|
311
|
+
#
|
312
|
+
# user - The String username.
|
313
|
+
#
|
314
|
+
# Returns the chosen hostname.
|
315
|
+
def set_user_route(user)
|
316
|
+
set_route(:user, user)
|
317
|
+
end
|
318
|
+
|
319
|
+
# Explicitly set the user route to the given host.
|
320
|
+
#
|
321
|
+
# user - The String username.
|
322
|
+
# host - The String hostname.
|
323
|
+
#
|
324
|
+
# Returns the new String hostname.
|
325
|
+
# Raises Chimney::NoSuchStorageServer if the storage server is not currently
|
326
|
+
# in the list.
|
327
|
+
def set_user_route!(user, host)
|
328
|
+
unless self.storage_servers.include?(host)
|
329
|
+
raise NoSuchStorageServer.new(host)
|
330
|
+
end
|
331
|
+
set_route(:user, user, host)
|
332
|
+
end
|
333
|
+
|
334
|
+
# Find the least loaded storage server and set a route there for
|
335
|
+
# the given +gist+. If the gist already exists, do nothing and
|
336
|
+
# simply return the host that gist is on.
|
337
|
+
#
|
338
|
+
# gist - The String gist ID.
|
339
|
+
#
|
340
|
+
# Returns the chosen hostname.
|
341
|
+
def set_gist_route(gist)
|
342
|
+
set_route(:gist, gist)
|
343
|
+
end
|
344
|
+
|
345
|
+
# Change the name of the given user without changing the associated host.
|
346
|
+
#
|
347
|
+
# old_user - The old user name.
|
348
|
+
# new_user - The new user name.
|
349
|
+
#
|
350
|
+
# Returns the hostname on success, or nil if the old user was not found
|
351
|
+
# or if the new user already exists.
|
352
|
+
def rename_user_route(old_user, new_user)
|
353
|
+
if (host = get_user_route(old_user)) && !get_user_route(new_user)
|
354
|
+
delete_user_route(old_user)
|
355
|
+
set_route(:user, new_user, host)
|
356
|
+
else
|
357
|
+
nil
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
# Delete the route for the given user.
|
362
|
+
#
|
363
|
+
# user - The String username.
|
364
|
+
#
|
365
|
+
# Returns nothing.
|
366
|
+
def delete_user_route(user)
|
367
|
+
self.client.delete("gh.storage.user.#{user}")
|
368
|
+
end
|
369
|
+
|
370
|
+
# Delete the route for the given gist.
|
371
|
+
#
|
372
|
+
# gist - The String gist ID.
|
373
|
+
#
|
374
|
+
# Returns nothing.
|
375
|
+
def delete_gist_route(gist)
|
376
|
+
self.client.delete("gh.storage.gist.#{gist}")
|
377
|
+
end
|
378
|
+
|
379
|
+
# Set the partition usage for a given host.
|
380
|
+
#
|
381
|
+
# host - The String hostname.
|
382
|
+
# partition - The single lowercase hex digit partition String.
|
383
|
+
# usage - The percent of disk space used as a Float [0.0-100.0].
|
384
|
+
#
|
385
|
+
# Returns nothing.
|
386
|
+
def set_partition_usage(host, partition, usage)
|
387
|
+
self.client.set("gh.storage.server.usage.percent.#{host}.#{partition}", usage.to_s)
|
388
|
+
end
|
389
|
+
|
390
|
+
# The list of partition usage percentages.
|
391
|
+
#
|
392
|
+
# host - The optional String hostname to restrict the response to.
|
393
|
+
#
|
394
|
+
# Returns an Array of [partition:String, percentage:Float].
|
395
|
+
def partition_usage(host = nil)
|
396
|
+
pattern = "gh.storage.server.usage.percent."
|
397
|
+
pattern += host ? "#{host}.*" : "*"
|
398
|
+
self.client.keys(pattern).map do |x|
|
399
|
+
[x, self.client.get(x).to_f]
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
# Calculate the absolute path of the user's storage directory.
|
404
|
+
#
|
405
|
+
# user - The String username.
|
406
|
+
#
|
407
|
+
# Returns the String path:
|
408
|
+
# e.g. '/data/repositories/2/a8/e2/95/mojombo'.
|
409
|
+
def self.shard_user_path(user)
|
410
|
+
hex = Digest::MD5.hexdigest(user)
|
411
|
+
partition = partition_hex(user)
|
412
|
+
shard = File.join(partition, hex[0..1], hex[2..3], hex[4..5])
|
413
|
+
File.join(REPO_DIR, shard, user)
|
414
|
+
end
|
415
|
+
|
416
|
+
def shard_user_path(user)
|
417
|
+
Chimney.shard_user_path(user)
|
418
|
+
end
|
419
|
+
|
420
|
+
# Calculate the absolute path of the repo's storage directory.
|
421
|
+
#
|
422
|
+
# user - The String username.
|
423
|
+
# repo - The String repo name.
|
424
|
+
#
|
425
|
+
# Returns the String path:
|
426
|
+
# e.g. '/data/repositories/2/a8/e2/95/mojombo/god.git'.
|
427
|
+
def self.shard_repo_path(user, repo)
|
428
|
+
hex = Digest::MD5.hexdigest(user)
|
429
|
+
partition = partition_hex(user)
|
430
|
+
shard = File.join(partition, hex[0..1], hex[2..3], hex[4..5])
|
431
|
+
File.join(REPO_DIR, shard, user, "#{repo}.git")
|
432
|
+
end
|
433
|
+
|
434
|
+
def shard_repo_path(user, repo)
|
435
|
+
Chimney.shard_repo_path(user, repo)
|
436
|
+
end
|
437
|
+
|
438
|
+
# Calculate the absolute path of the gist's storage directory.
|
439
|
+
#
|
440
|
+
# gist - The String gist ID.
|
441
|
+
#
|
442
|
+
# Returns String path:
|
443
|
+
# e.g. '/data/repositories/0/81/dc/9b/gist/1234.git'.
|
444
|
+
def self.shard_gist_path(gist)
|
445
|
+
hex = Digest::MD5.hexdigest(gist)
|
446
|
+
partition = partition_hex(gist)
|
447
|
+
shard = File.join(partition, hex[0..1], hex[2..3], hex[4..5])
|
448
|
+
File.join(REPO_DIR, shard, 'gist', "#{gist}.git")
|
449
|
+
end
|
450
|
+
|
451
|
+
def shard_gist_path(gist)
|
452
|
+
Chimney.shard_gist_path(gist)
|
453
|
+
end
|
454
|
+
|
455
|
+
# Calculate the partition hex digit.
|
456
|
+
#
|
457
|
+
# name - The String username or gist.
|
458
|
+
#
|
459
|
+
# Returns a single lowercase hex digit [0-9a-f] as a String.
|
460
|
+
def self.partition_hex(name)
|
461
|
+
Digest::MD5.hexdigest(name)[0].chr
|
462
|
+
end
|
463
|
+
|
464
|
+
def partition_hex(name)
|
465
|
+
Chimney.partition_hex(name)
|
466
|
+
end
|
467
|
+
|
468
|
+
# The unix user account that has access to the repository data.
|
469
|
+
#
|
470
|
+
# Returns the String user e.g. 'root'.
|
471
|
+
def self.unix_user
|
472
|
+
UNIX_USER
|
473
|
+
end
|
474
|
+
|
475
|
+
def unix_user
|
476
|
+
Chimney.unix_user
|
477
|
+
end
|
478
|
+
|
479
|
+
# The short name of the server currently executing this code. If this is a
|
480
|
+
# front end and we're on fe2.rs.github.com, this will return "fe2".
|
481
|
+
#
|
482
|
+
# Returns a String host short name e.g. "fe2".
|
483
|
+
def self.current_server
|
484
|
+
if hostname =~ /github\.com/
|
485
|
+
hostname.split('.').first
|
486
|
+
else
|
487
|
+
"localhost"
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
def current_server
|
492
|
+
Chimney.current_server
|
493
|
+
end
|
494
|
+
|
495
|
+
# The full hostname of the current server.
|
496
|
+
#
|
497
|
+
# Returns a String hostname e.g. "fe2.rs.github.com".
|
498
|
+
def self.hostname
|
499
|
+
`hostname`.chomp
|
500
|
+
end
|
501
|
+
|
502
|
+
private
|
503
|
+
|
504
|
+
# Ensure that a valid connection to the routing server has been made
|
505
|
+
# and that the list of hosts has been fetched.
|
506
|
+
#
|
507
|
+
# Returns nothing.
|
508
|
+
def ensure_client_connection
|
509
|
+
logger.info "Starting Chimney..."
|
510
|
+
self.client = Redis.new(:host => self.host, :port => self.port)
|
511
|
+
if hosts = self.client.get('gh.storage.servers')
|
512
|
+
self.hosts = hosts.split(',')
|
513
|
+
write_hosts_to_file
|
514
|
+
logger.info "Found #{self.hosts.size} hosts from Router."
|
515
|
+
else
|
516
|
+
read_hosts_from_file
|
517
|
+
raise InvalidRoutingServer.new("Hosts could not be loaded.") if self.hosts.empty?
|
518
|
+
logger.warn "Router does not contain hosts list; loaded #{self.hosts.size} hosts from file."
|
519
|
+
end
|
520
|
+
rescue Errno::ECONNREFUSED
|
521
|
+
read_hosts_from_file
|
522
|
+
raise InvalidRoutingServer.new("Hosts could not be loaded.") if self.hosts.empty?
|
523
|
+
logger.warn "Unable to connect to Router; loaded #{self.hosts.size} hosts from file."
|
524
|
+
end
|
525
|
+
|
526
|
+
# Write the hosts list to a file.
|
527
|
+
#
|
528
|
+
# Returns nothing.
|
529
|
+
def write_hosts_to_file
|
530
|
+
File.open(SMOKE_HOSTS_FILE, 'w') do |f|
|
531
|
+
f.write(self.hosts.join(','))
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
# Read the hosts from a file.
|
536
|
+
#
|
537
|
+
# Returns nothing.
|
538
|
+
def read_hosts_from_file
|
539
|
+
if File.exists?(SMOKE_HOSTS_FILE)
|
540
|
+
self.hosts = File.read(SMOKE_HOSTS_FILE).split(',')
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
# Reload the hosts list from the router.
|
545
|
+
#
|
546
|
+
# Returns nothing.
|
547
|
+
def reload_hosts_list
|
548
|
+
self.hosts = self.storage_servers
|
549
|
+
write_hosts_to_file
|
550
|
+
end
|
551
|
+
|
552
|
+
# Find the storage server with the least disk usage for the target partition.
|
553
|
+
#
|
554
|
+
# type - Either :user or :gist.
|
555
|
+
# name - The String username or gist.
|
556
|
+
#
|
557
|
+
# Returns a hostname.
|
558
|
+
def find_least_loaded_host(name)
|
559
|
+
partition = partition_hex(name)
|
560
|
+
self.hosts.select { |h| storage_server_online?(h) }.map do |host|
|
561
|
+
[self.client.get("gh.storage.server.usage.percent.#{host}.#{partition}").to_f, host]
|
562
|
+
end.sort.first.last
|
563
|
+
end
|
564
|
+
|
565
|
+
# Set the route for a given user or gist.
|
566
|
+
#
|
567
|
+
# type - Either :user or :gist.
|
568
|
+
# name - The String username or gist.
|
569
|
+
# host - The String hostname that will be set if it is present (optional).
|
570
|
+
#
|
571
|
+
# Returns the String hostname that was set.
|
572
|
+
def set_route(type, name, host = nil)
|
573
|
+
if !host && existing_host = self.client.get("gh.storage.#{type}.#{name}")
|
574
|
+
return existing_host
|
575
|
+
end
|
576
|
+
|
577
|
+
unless host
|
578
|
+
reload_hosts_list
|
579
|
+
host = find_least_loaded_host(name)
|
580
|
+
end
|
581
|
+
|
582
|
+
self.client.set("gh.storage.#{type}.#{name}", host)
|
583
|
+
host
|
584
|
+
end
|
585
|
+
|
586
|
+
# Try to find a route using a variety of different fallbacks.
|
587
|
+
#
|
588
|
+
# type - Either :user or :gist.
|
589
|
+
# name - The String username or gist.
|
590
|
+
#
|
591
|
+
# Returns the hostname of the storage server.
|
592
|
+
def try_route(type, name)
|
593
|
+
try_route_with_redis(type, name)
|
594
|
+
end
|
595
|
+
|
596
|
+
# Try the lookup from redis. If redis is unavailable, try
|
597
|
+
# to do the lookup from internal cache.
|
598
|
+
#
|
599
|
+
# type - Either :user or :gist.
|
600
|
+
# name - The String username or gist.
|
601
|
+
#
|
602
|
+
# Returns the hostname of the storage server.
|
603
|
+
def try_route_with_redis(type, name)
|
604
|
+
if host = self.client.get("gh.storage.#{type}.#{name}")
|
605
|
+
logger.debug "Found host '#{host}' for #{type} '#{name}' from Router."
|
606
|
+
self.cache[name] = host
|
607
|
+
else
|
608
|
+
self.cache.delete(name)
|
609
|
+
end
|
610
|
+
host
|
611
|
+
rescue Errno::ECONNREFUSED
|
612
|
+
logger.warn "No connection to Router..."
|
613
|
+
try_route_with_internal_cache(type, name)
|
614
|
+
end
|
615
|
+
|
616
|
+
# Try the lookup from the internal route cache. If the key is not
|
617
|
+
# in internal cache, try to reconnect to redis and redo the lookup.
|
618
|
+
#
|
619
|
+
# type - Either :user or :gist.
|
620
|
+
# name - The String username or gist.
|
621
|
+
#
|
622
|
+
# Returns the hostname of the storage server.
|
623
|
+
def try_route_with_internal_cache(type, name)
|
624
|
+
if host = self.cache[name]
|
625
|
+
logger.debug "Found '#{host}' for #{type} '#{name}' from Internal Cache."
|
626
|
+
host
|
627
|
+
else
|
628
|
+
logger.warn "No entry in Internal Cache..."
|
629
|
+
try_route_with_new_redis_connection(type, name)
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
# Try the lookup with a new redis connection. If redis is still
|
634
|
+
# unavailable, try each storage server in turn to look for the user/gist.
|
635
|
+
#
|
636
|
+
# type - Either :user or :gist.
|
637
|
+
# name - The String username or gist.
|
638
|
+
#
|
639
|
+
# Returns the hostname of the storage server.
|
640
|
+
def try_route_with_new_redis_connection(type, name)
|
641
|
+
self.client.connect_to_server
|
642
|
+
host = self.client.get("gh.storage.#{type}.#{name}")
|
643
|
+
logger.debug "Found host '#{host}' for #{type} '#{name}' from Router after reconnect."
|
644
|
+
host
|
645
|
+
rescue Errno::ECONNREFUSED
|
646
|
+
logger.warn "Still no connection to Router..."
|
647
|
+
try_route_with_individual_storage_checks(type, name)
|
648
|
+
end
|
649
|
+
|
650
|
+
# Try the lookup by asking each storage server if the user or gist dir exists.
|
651
|
+
#
|
652
|
+
# type - Either :user or :gist.
|
653
|
+
# name - The String username or gist.
|
654
|
+
#
|
655
|
+
# Returns the hostname of the storage server or nil.
|
656
|
+
def try_route_with_individual_storage_checks(type, name)
|
657
|
+
self.hosts.each do |host|
|
658
|
+
logger.debug "Trying host '#{host}' via Smoke for existence of #{type} '#{name}'..."
|
659
|
+
|
660
|
+
svc = smoke(host)
|
661
|
+
exist =
|
662
|
+
case type
|
663
|
+
when :user: svc.user_dir_exist?(name)
|
664
|
+
when :gist: svc.gist_dir_exist?(name)
|
665
|
+
else false
|
666
|
+
end
|
667
|
+
|
668
|
+
if exist
|
669
|
+
self.cache[name] = host
|
670
|
+
logger.debug "Found host '#{host}' for #{type} '#{name}' from Smoke."
|
671
|
+
return host
|
672
|
+
end
|
673
|
+
end
|
674
|
+
logger.warn "No host found for #{type} '#{name}'."
|
675
|
+
nil
|
676
|
+
rescue Object => e
|
677
|
+
logger.error "No host found for #{type} '#{name}' because of '#{e.message}'."
|
678
|
+
nil
|
679
|
+
end
|
680
|
+
|
681
|
+
def smoke(host)
|
682
|
+
BERTRPC::Service.new(host, 8149, 2).call.store
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
686
|
+
class Math
|
687
|
+
# Duplicate some text an abitrary number of times.
|
688
|
+
#
|
689
|
+
# text - The String to be duplicated.
|
690
|
+
# count - The Integer number of times to duplicate the text.
|
691
|
+
#
|
692
|
+
# Examples
|
693
|
+
# multiplex('Tom', 4)
|
694
|
+
# # => 'TomTomTomTom'
|
695
|
+
#
|
696
|
+
# Returns the duplicated String.
|
697
|
+
def multiplex(text, count)
|
698
|
+
text * count
|
699
|
+
end
|
700
|
+
end
|
701
|
+
end
|
702
|
+
|
703
|
+
module GitHub
|
704
|
+
class Jobs
|
705
|
+
# Performs a job.
|
706
|
+
#
|
707
|
+
# Returns nothing.
|
708
|
+
def perform
|
709
|
+
end
|
710
|
+
end
|
711
|
+
end
|