tomdoc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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