tomdoc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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