ruby-link-checker 0.3.0 → 0.5.1

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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/coverage.yml +24 -0
  3. data/.github/workflows/danger-comment.yml +17 -0
  4. data/.github/workflows/danger.yml +12 -0
  5. data/.github/workflows/lint.yml +20 -0
  6. data/.github/workflows/release.yml +14 -0
  7. data/.github/workflows/test.yml +25 -0
  8. data/.gitignore +6 -0
  9. data/.rspec +2 -0
  10. data/.rubocop.yml +26 -0
  11. data/.rubocop_todo.yml +332 -0
  12. data/AGENTS.md +23 -0
  13. data/CHANGELOG.md +27 -0
  14. data/CONTRIBUTING.md +4 -0
  15. data/Dangerfile +2 -0
  16. data/Gemfile +5 -1
  17. data/README.md +151 -7
  18. data/RELEASING.md +17 -40
  19. data/Rakefile +8 -0
  20. data/benchmarks/check.rb +27 -20
  21. data/lib/ruby-link-checker/async/http/checker.rb +118 -0
  22. data/lib/ruby-link-checker/async/http/config.rb +39 -0
  23. data/lib/ruby-link-checker/async/http/result.rb +85 -0
  24. data/lib/ruby-link-checker/async/http.rb +3 -0
  25. data/lib/ruby-link-checker/cache.rb +101 -0
  26. data/lib/ruby-link-checker/callbacks.rb +2 -2
  27. data/lib/ruby-link-checker/checker.rb +76 -4
  28. data/lib/ruby-link-checker/config.rb +64 -0
  29. data/lib/ruby-link-checker/errors/connection_error.rb +23 -0
  30. data/lib/ruby-link-checker/errors/redirect_limit_error.rb +15 -0
  31. data/lib/ruby-link-checker/errors.rb +2 -0
  32. data/lib/ruby-link-checker/net/http/checker.rb +4 -1
  33. data/lib/ruby-link-checker/net/http/result.rb +4 -0
  34. data/lib/ruby-link-checker/result.rb +25 -0
  35. data/lib/ruby-link-checker/task.rb +2 -0
  36. data/lib/ruby-link-checker/tasks.rb +43 -7
  37. data/lib/ruby-link-checker/typhoeus/hydra/checker.rb +29 -6
  38. data/lib/ruby-link-checker/typhoeus/hydra/result.rb +4 -0
  39. data/lib/ruby-link-checker/version.rb +1 -1
  40. data/lib/ruby-link-checker.rb +2 -0
  41. data/ruby-link-checker.gemspec +3 -2
  42. data/spec/ruby-link-checker/async/http/checker_spec.rb +184 -0
  43. data/spec/ruby-link-checker/callbacks_spec.rb +33 -0
  44. data/spec/ruby-link-checker/checker_spec.rb +89 -2
  45. data/spec/ruby-link-checker/config_spec.rb +6 -0
  46. data/spec/ruby-link-checker/errors/redirect_loop_error_spec.rb +18 -0
  47. data/spec/ruby-link-checker/net/http/checker_spec.rb +32 -0
  48. data/spec/ruby-link-checker/result_error_spec.rb +34 -0
  49. data/spec/ruby-link-checker/result_spec.rb +41 -0
  50. data/spec/ruby-link-checker/task_spec.rb +20 -0
  51. data/spec/ruby-link-checker/tasks_spec.rb +91 -0
  52. data/spec/ruby-link-checker/typhoeus/hydra/checker_spec.rb +116 -3
  53. data/spec/spec_helper.rb +3 -1
  54. data/spec/support/link_checker.rb +267 -3
  55. metadata +42 -35
  56. data/Gemfile.lock +0 -215
  57. data/coverage/assets/0.13.1/DataTables-1.10.20/images/sort_asc.png +0 -0
  58. data/coverage/assets/0.13.1/DataTables-1.10.20/images/sort_asc_disabled.png +0 -0
  59. data/coverage/assets/0.13.1/DataTables-1.10.20/images/sort_both.png +0 -0
  60. data/coverage/assets/0.13.1/DataTables-1.10.20/images/sort_desc.png +0 -0
  61. data/coverage/assets/0.13.1/DataTables-1.10.20/images/sort_desc_disabled.png +0 -0
  62. data/coverage/assets/0.13.1/application.css +0 -1
  63. data/coverage/assets/0.13.1/application.js +0 -7
  64. data/coverage/assets/0.13.1/colorbox/border.png +0 -0
  65. data/coverage/assets/0.13.1/colorbox/controls.png +0 -0
  66. data/coverage/assets/0.13.1/colorbox/loading.gif +0 -0
  67. data/coverage/assets/0.13.1/colorbox/loading_background.png +0 -0
  68. data/coverage/assets/0.13.1/favicon_green.png +0 -0
  69. data/coverage/assets/0.13.1/favicon_red.png +0 -0
  70. data/coverage/assets/0.13.1/favicon_yellow.png +0 -0
  71. data/coverage/assets/0.13.1/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  72. data/coverage/assets/0.13.1/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  73. data/coverage/assets/0.13.1/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  74. data/coverage/assets/0.13.1/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  75. data/coverage/assets/0.13.1/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  76. data/coverage/assets/0.13.1/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  77. data/coverage/assets/0.13.1/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  78. data/coverage/assets/0.13.1/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  79. data/coverage/assets/0.13.1/images/ui-icons_222222_256x240.png +0 -0
  80. data/coverage/assets/0.13.1/images/ui-icons_2e83ff_256x240.png +0 -0
  81. data/coverage/assets/0.13.1/images/ui-icons_454545_256x240.png +0 -0
  82. data/coverage/assets/0.13.1/images/ui-icons_888888_256x240.png +0 -0
  83. data/coverage/assets/0.13.1/images/ui-icons_cd0a0a_256x240.png +0 -0
  84. data/coverage/assets/0.13.1/loading.gif +0 -0
  85. data/coverage/assets/0.13.1/magnify.png +0 -0
  86. data/coverage/index.html +0 -15014
data/README.md CHANGED
@@ -2,9 +2,8 @@ Fast Ruby Link Checker
2
2
  ======================
3
3
 
4
4
  [![Gem Version](http://img.shields.io/gem/v/ruby-link-checker.svg)](http://badge.fury.io/rb/ruby-link-checker)
5
- [![Build Status](https://github.com/dblock/ruby-link-checker/workflows/test/badge.svg?branch=main)](https://github.com/dblock/ruby-link-checker/actions)
6
- [![Code Climate](https://codeclimate.com/github/dblock/ruby-link-checker.svg)](https://codeclimate.com/github/dblock/ruby-link-checker)
7
- [![Test Coverage](https://api.codeclimate.com/v1/badges/164f1e23fc706b6efa63/test_coverage)](https://codeclimate.com/github/dblock/ruby-link-checker/test_coverage)
5
+ [![Build Status](https://github.com/dblock/ruby-link-checker/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/dblock/ruby-link-checker/actions/workflows/test.yml)
6
+ [![codecov](https://codecov.io/gh/dblock/ruby-link-checker/graph/badge.svg)](https://codecov.io/gh/dblock/ruby-link-checker)
8
7
 
9
8
  A fast Ruby link checker with support for multiple HTTP libraries. Does not parse documents, just checks links. Fast. Anecdotal benchmarking on a M1 mac and T1 Internet yields ~50 URLs per second with `LinkChecker::Typhoeus::Hydra`.
10
9
 
@@ -16,16 +15,23 @@ A fast Ruby link checker with support for multiple HTTP libraries. Does not pars
16
15
  - [Passing Options](#passing-options)
17
16
  - [Checkers](#checkers)
18
17
  - [LinkChecker::Typhoeus::Hydra](#linkcheckertyphoeushydra)
18
+ - [LinkChecker::Async::HTTP](#linkcheckerasynchttp)
19
19
  - [LinkChecker::Net::HTTP](#linkcheckernethttp)
20
20
  - [Options](#options)
21
21
  - [Retries](#retries)
22
+ - [Redirects](#redirects)
22
23
  - [Retry After](#retry-after)
24
+ - [Retry Policy](#retry-policy)
23
25
  - [Results](#results)
24
26
  - [Methods](#methods)
27
+ - [Deduplication](#deduplication)
28
+ - [Caching](#caching)
25
29
  - [Logger](#logger)
26
30
  - [User-Agent](#user-agent)
31
+ - [Headers](#headers)
27
32
  - [Global Configuration](#global-configuration)
28
33
  - [Callbacks and Events](#callbacks-and-events)
34
+ - [Benchmarks](#benchmarks)
29
35
  - [Contributing](#contributing)
30
36
  - [Copyright and License](#copyright-and-license)
31
37
 
@@ -127,6 +133,63 @@ LinkChecker::Typhoeus::Hydra.configure do |config|
127
133
  end
128
134
  ```
129
135
 
136
+ #### [LinkChecker::Async::HTTP](lib/ruby-link-checker/async/http/checker.rb)
137
+
138
+ Fast, concurrent link checker that uses [async-http](https://github.com/socketry/async-http).
139
+
140
+ ```ruby
141
+ require 'async'
142
+ require 'async/http'
143
+ require 'ruby-link-checker'
144
+
145
+ # create a new instance of a checker
146
+ checker = LinkChecker::Async::HTTP::Checker.new
147
+
148
+ # log requests and response codes
149
+ checker.logger.level = Logger::INFO
150
+
151
+ links = [...] # array of URLs
152
+ links.each do |url|
153
+ checker.check url
154
+ end
155
+
156
+ # examine failures and errors as they come
157
+ checker.on :error, :failure do |result|
158
+ puts "FAIL: #{result}"
159
+ end
160
+
161
+ # runs all queued checks concurrently, will block until all requests have completed
162
+ checker.run
163
+
164
+ # examine results
165
+ checker.results.each_pair do |bucket, results|
166
+ puts "#{bucket}: #{results.size}"
167
+ end
168
+ ```
169
+
170
+ You can pass `Async::HTTP` timeout options into a new instance of a checker, or configure timeouts globally.
171
+
172
+ ```ruby
173
+ LinkChecker::Async::HTTP.configure do |config|
174
+ config.read_timeout = 5
175
+ config.open_timeout = 10
176
+ end
177
+ ```
178
+
179
+ You can also force a specific HTTP protocol version, `:http1` or `:http2`, per instance or globally. By default, `async-http` negotiates the protocol version with the server.
180
+
181
+ ```ruby
182
+ LinkChecker::Async::HTTP.configure do |config|
183
+ config.protocol = :http2
184
+ end
185
+ ```
186
+
187
+ Async::HTTP reuses connection-pooling clients per scheme/host/port. Its default concurrency is unlimited; set `max_concurrency` to bound active requests without counting delayed retries against that limit.
188
+
189
+ ```ruby
190
+ checker = LinkChecker::Async::HTTP::Checker.new(max_concurrency: 25)
191
+ ```
192
+
130
193
  #### [LinkChecker::Net::HTTP](lib/ruby-link-checker/net/http/checker.rb)
131
194
 
132
195
  Slow, sequential checker.
@@ -165,18 +228,43 @@ end
165
228
 
166
229
  #### Retries
167
230
 
168
- By default link checkers do not retry. You can set a number of times to retry all errors and failures with `retries`.
231
+ By default link checkers do not retry. Set `retries` to enable retries for transient connection errors/timeouts and HTTP statuses 408, 425, 429, 500, 502, 503, and 504. Permanent HTTP failures and invalid URLs are not retried. HEAD-to-GET fallback still applies independently of the retry policy. The retry budget is shared across methods: with `retries: 2`, repeated transient failures produce `HEAD`, `HEAD`, `HEAD`, then `GET`.
232
+
233
+ ```ruby
234
+ checker = LinkChecker::Net::HTTP::Checker.new(retries: 1)
235
+ ```
236
+
237
+ #### Redirects
238
+
239
+ Checkers follow up to 20 redirects per attempt and detect redirect loops. Relative redirects are resolved against the current response URL. Set `max_redirects` to change the limit, or use `0` to disable redirects.
169
240
 
170
241
  ```ruby
171
- checker = LinkChecker::Net::HTTP::Checker.new(retry: 1)
242
+ checker = LinkChecker::Typhoeus::Hydra::Checker.new(max_redirects: 10)
172
243
  ```
173
244
 
174
245
  #### Retry After
175
246
 
176
- If retry is enabled, link checkers can wait for a number of seconds before retrying.
247
+ Set `retry_after` to a minimum delay in seconds (default `0`). Optional `retry_backoff` multiplies that delay on successive retries (default `1`), and `retry_jitter` adds a random fraction between `0` and the configured value (default `0`, maximum `1`). All delays are capped by `max_retry_delay` (default `60` seconds).
177
248
 
178
249
  ```ruby
179
- checker = LinkChecker::Net::HTTP::Checker.new(retry_after: 1) # wait for 1 second before retrying
250
+ checker = LinkChecker::Typhoeus::Hydra::Checker.new(
251
+ retries: 2,
252
+ retry_after: 1,
253
+ retry_backoff: 2,
254
+ retry_jitter: 0.1,
255
+ respect_retry_after: true,
256
+ max_retry_delay: 60
257
+ )
258
+ ```
259
+
260
+ With `respect_retry_after: true` (default `false`), HTTP `Retry-After` delta-seconds and dates provide a minimum delay, subject to the same cap. Invalid or past dates are ignored. Async delays yield without blocking other requests. Hydra queues delayed retries after the active batch drains, so actual delays can exceed the configured minimum without blocking completion callbacks. Delays apply to retries, not ordinary HEAD-to-GET fallback.
261
+
262
+ #### Retry Policy
263
+
264
+ Customize `retry_status_codes` to choose retryable HTTP statuses. Set it to `nil` to restore the previous behavior of retrying every failure and error, including malformed URLs and programming errors.
265
+
266
+ ```ruby
267
+ checker = LinkChecker::Net::HTTP::Checker.new(retries: 1, retry_status_codes: [429, 503])
180
268
  ```
181
269
 
182
270
  #### Results
@@ -211,6 +299,34 @@ The following examples disables `GET` and only makes `HEAD` requests.
211
299
  checker = LinkChecker::Net::HTTP::Checker.new(methods: %w[HEAD])
212
300
  ```
213
301
 
302
+ #### Deduplication
303
+
304
+ Set `deduplicate: true` to coalesce identical in-flight requests and reuse successful results for the lifetime of the checker. Failures and errors are rechecked after completion. Each submission still receives its own result events and metadata, including events emitted before it joined an in-flight check. Deduplication is disabled by default.
305
+
306
+ ```ruby
307
+ checker = LinkChecker::Typhoeus::Hydra::Checker.new(deduplicate: true)
308
+ checker.check('https://www.example.org', location: 'one.html')
309
+ checker.check('https://www.example.org', location: 'two.html')
310
+ checker.run # one check, two sets of result events
311
+ ```
312
+
313
+ Identity includes the complete URL (including query values and fragment), transport, methods, headers, user-agent, and request-related settings. Changing those settings prevents reuse. URLs containing user-info credentials bypass deduplication and caching.
314
+
315
+ #### Caching
316
+
317
+ Persistent success caching is disabled by default. Enable it with an explicit file path and positive TTL in seconds. Recent successful checks can then be reused across checker instances; failures and errors are never stored. Cache writes are atomic and locked, and unreadable or malformed files fall back to normal checking.
318
+
319
+ ```ruby
320
+ checker = LinkChecker::Typhoeus::Hydra::Checker.new(
321
+ deduplicate: true,
322
+ cache: { file: 'tmp/link-checker/cache.json', ttl: 3600 }
323
+ )
324
+ ```
325
+
326
+ Each cache hit emits `:result` and `:success` with the new submission's metadata and `cached? == true`. Persistent hits return `LinkChecker::CachedResult` with the HTTP method, status, original/final URIs, and metadata; native request/response objects and headers are not persisted. Redirect targets containing user-info credentials are not written. Add the cache directory to your ignore rules.
327
+
328
+ Caching alone does not coalesce in-flight checks. With deduplication enabled, successful results remain reusable within that checker even after the persistent TTL expires. Cached successes do not replay historical redirect/retry events.
329
+
214
330
  #### Logger
215
331
 
216
332
  Pass your own logger.
@@ -227,6 +343,20 @@ Pass your own user-agent. Default is `Ruby Link Checker/x.y.z`.
227
343
  checker = LinkChecker::Net::HTTP::Checker.new(user_agent: 'Custom Agent/1.0')
228
344
  ```
229
345
 
346
+ #### Headers
347
+
348
+ Some sites reject or return errors (403, 404) to requests that look like bots. Pass additional headers to mimic a browser.
349
+
350
+ ```ruby
351
+ checker = LinkChecker::Net::HTTP::Checker.new(
352
+ headers: {
353
+ 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
354
+ 'Accept-Language' => 'en-US,en;q=0.5',
355
+ 'DNT' => '1'
356
+ }
357
+ )
358
+ ```
359
+
230
360
  ### Global Configuration
231
361
 
232
362
  All options can also be configured globally.
@@ -234,6 +364,7 @@ All options can also be configured globally.
234
364
  ```ruby
235
365
  LinkChecker.configure do |config|
236
366
  config.user_agent = 'Custom Agent/1.0'
367
+ config.headers = { 'DNT' => '1' }
237
368
  config.methods = ['HEAD', 'GET']
238
369
  config.logger = ::Logger.new(STDOUT)
239
370
  end
@@ -274,9 +405,22 @@ Events are called with results, which contain the following properties.
274
405
  | :request_headers | Request headers. |
275
406
  | :redirect_to | A redirect URL in case of redirects. |
276
407
  | :error | A raised error in case of errors. |
408
+ | :error_type | `:timeout`, `:connection`, or `:other` for errors; otherwise `nil`. |
409
+ | :response_headers | Response headers, accessible using `[]`. |
410
+ | :cached? | Whether this result was reused from a successful check. |
277
411
 
278
412
  See [result.rb](lib/ruby-link-checker/result.rb) for more details.
279
413
 
414
+ ## Benchmarks
415
+
416
+ See [benchmarks/check.rb](benchmarks/check.rb) for a script comparing the performance of the three checkers (`LinkChecker::Net::HTTP`, `LinkChecker::Typhoeus::Hydra`, and `LinkChecker::Async::HTTP`) against a fixed set of URLs.
417
+
418
+ ```
419
+ bundle exec ruby -Ilib benchmarks/check.rb
420
+ ```
421
+
422
+ Anecdotal results show `LinkChecker::Async::HTTP` using significantly less CPU time than `LinkChecker::Typhoeus::Hydra` (both concurrent checkers), while wall-clock time is comparable between the two and depends on timeouts and the responsiveness of the URLs being checked. `LinkChecker::Net::HTTP` checks links sequentially and is significantly slower in wall-clock time for large batches of URLs; it's commented out by default in the script.
423
+
280
424
  ## Contributing
281
425
 
282
426
  You're encouraged to contribute to ruby-link-checker. See [CONTRIBUTING](CONTRIBUTING.md) for details.
data/RELEASING.md CHANGED
@@ -2,59 +2,36 @@
2
2
 
3
3
  There are no hard rules about when to release ruby-link-checker. Release bug fixes frequently, features not so frequently and breaking API changes rarely.
4
4
 
5
- ### Release
5
+ Releases use [ruby-gem-release](https://github.com/dblock/ruby-gem-release/tree/v0.3.1) and the manual [Release workflow](.github/workflows/release.yml). Merges do not publish. Every workflow dispatch performs a real release.
6
6
 
7
- Run tests, check that all tests succeed locally.
7
+ ## One-Time Setup
8
8
 
9
- ```
10
- bundle install
11
- rake
12
- ```
13
-
14
- Check that the last build succeeded.
15
-
16
- Add a date to this release in [CHANGELOG.md](CHANGELOG.md).
17
-
18
- ```
19
- ### 0.2.2 (2015/7/10)
20
- ```
9
+ Create a GitHub environment named `release` in this repository. Restrict deployment to `main` and require approval when available.
21
10
 
22
- Remove the line with "Your contribution here.", since there will be no more contributions to this release.
11
+ On [RubyGems.org](https://rubygems.org/gems/ruby-link-checker/trusted_publishers), configure a trusted publisher with repository owner `dblock`, repository name `ruby-link-checker`, workflow filename `release.yml`, and environment `release`. Set the separate workflow repository owner to `dblock` and name to `ruby-gem-release` because publication uses its reusable workflow. No stored RubyGems API key is needed.
23
12
 
24
- Commit your changes.
13
+ The workflow requests `contents: write` and `id-token: write`. Branch rules must allow the release identity to push fast-forward release and next-version commits to `main` and create tags, without allowing force pushes or deletion. Adding the workflow does not configure these permissions or the trusted publisher.
25
14
 
26
- ```
27
- git add README.md CHANGELOG.md lib/ruby-link-checker/version.rb
28
- git commit -m "Preparing for release, 0.2.2."
29
- ```
15
+ ## Prepare and Publish
30
16
 
31
- Release.
17
+ Prepare the intended version in `lib/ruby-link-checker/version.rb` and its matching `### VERSION (Next)` section in `CHANGELOG.md` through a PR. Keep exactly one `* Your contribution here.` placeholder in that section. The workflow releases the current version, dates the changelog, and removes the placeholder; it does not select a new release version.
32
18
 
33
- ```
34
- $ rake release
19
+ Run tests and RuboCop locally and review the release changes before merging:
35
20
 
36
- ruby-link-checker 0.2.2 built to pkg/ruby-link-checker-0.2.2.gem.
37
- Tagged v0.2.2.
38
- Pushed git commits and tags.
39
- Pushed ruby-link-checker 0.2.2 to rubygems.org.
21
+ ```sh
22
+ bundle exec rake
40
23
  ```
41
24
 
42
- ### Prepare for the Next Version
43
-
44
- Add the next release to [CHANGELOG.md](CHANGELOG.md).
25
+ After merging, explicit release approval, and successful Trusted Publishing setup, dispatch from `main`:
45
26
 
27
+ ```sh
28
+ gh workflow run release.yml --ref main
46
29
  ```
47
- ### 0.2.3 (Next)
48
30
 
49
- * Your contribution here.
50
- ```
31
+ The workflow runs `bundle exec rake`, publishes the verified gem to RubyGems, pushes the release commit and tag atomically, and creates a GitHub Release with the gem attached. It then increments the patch version and pushes a new development commit with a fresh changelog section. Actions-token pushes do not trigger ordinary push workflows, so the release workflow runs tests and RuboCop before publication.
51
32
 
52
- Increment the third version number in [lib/ruby-link-checker/version.rb](lib/ruby-link-checker/version.rb).
33
+ ## Verify and Recover
53
34
 
54
- Commit your changes.
35
+ Verify the published RubyGems version, matching tag and GitHub Release, attached gem, and next development version on `main`.
55
36
 
56
- ```
57
- git add CHANGELOG.md lib/ruby-link-checker/version.rb
58
- git commit -m "Preparing for next development iteration, 0.2.3."
59
- git push origin main
60
- ```
37
+ Never automatically rerun a failed or timed-out publication: RubyGems may have accepted the upload. Check registry and GitHub state first. Do not republish an existing version or move its tag. Available `release.gem`, `release.json`, and `release.bundle` recovery artifacts are retained for 30 days; follow the [upstream recovery instructions](https://github.com/dblock/ruby-gem-release/blob/v0.3.1/RELEASING.md#verify-and-recover) to complete only the missing steps.
data/Rakefile CHANGED
@@ -15,4 +15,12 @@ end
15
15
  require 'rubocop/rake_task'
16
16
  RuboCop::RakeTask.new(:rubocop)
17
17
 
18
+ require 'ruby-gem-release/workflow/ruby_gem'
19
+ require 'ruby-gem-release/changelog'
20
+
21
+ RubyGemRelease::Workflow::RubyGem.new(
22
+ version: RubyGemRelease::Version::RubyConstant.new(path: 'lib/ruby-link-checker/version.rb', constant: 'VERSION'),
23
+ changelog: RubyGemRelease::Changelog::Intridea.new(placeholder: '* Your contribution here.')
24
+ ).install_tasks
25
+
18
26
  task default: %i[rubocop spec]
data/benchmarks/check.rb CHANGED
@@ -1,31 +1,38 @@
1
1
  require 'benchmark'
2
2
  require 'net/http'
3
3
  require 'typhoeus'
4
+ require 'async'
5
+ require 'async/http'
4
6
  require 'ruby-link-checker'
5
7
 
6
8
  input = File.readlines(File.join(__dir__, 'data/opensearch.org/small.txt')).map(&:strip)
7
9
 
8
- Benchmark.bm do |benchmark|
9
- # benchmark.report('LinkChecker::Net::HTTP::Checker') do
10
- # checker = LinkChecker::Net::HTTP::Checker.new
11
- # checker.logger.level = Logger::INFO
12
- # input.each do |url|
13
- # checker.check url
14
- # end
15
- # checker.results.each_pair do |bucket, results|
16
- # puts "#{bucket}: #{results.size}"
17
- # end
18
- # end
10
+ CHECKERS = [
11
+ # LinkChecker::Net::HTTP::Checker,
12
+ LinkChecker::Typhoeus::Hydra::Checker,
13
+ LinkChecker::Async::HTTP::Checker
14
+ ].freeze
19
15
 
20
- benchmark.report('LinkChecker::Typhoeus::Hydra::Checker') do
21
- checker = LinkChecker::Typhoeus::Hydra::Checker.new
22
- checker.logger.level = Logger::INFO
23
- input.each do |url|
24
- checker.check url
25
- end
26
- checker.run
27
- checker.results.each_pair do |bucket, results|
28
- puts "#{bucket}: #{results.size}"
16
+ Benchmark.bm(40) do |benchmark|
17
+ CHECKERS.each do |checker_klass|
18
+ benchmark.report(checker_klass.name) do
19
+ checker = checker_klass.new
20
+ checker.logger.level = Logger::INFO
21
+ case checker
22
+ when LinkChecker::Typhoeus::Hydra::Checker
23
+ checker.timeout = 5 # 1/12th of the default 60s
24
+ checker.connecttimeout = 1 # 1/10th of the default 10s
25
+ else
26
+ checker.read_timeout = 5 # 1/12th of the default 60s
27
+ checker.open_timeout = 5 # 1/12th of the default 60s
28
+ end
29
+ input.each do |url|
30
+ checker.check url
31
+ end
32
+ checker.run if checker.respond_to?(:run)
33
+ checker.results.each_pair do |bucket, results|
34
+ puts "#{bucket}: #{results.size}"
35
+ end
29
36
  end
30
37
  end
31
38
  end
@@ -0,0 +1,118 @@
1
+ module LinkChecker
2
+ module Async
3
+ module HTTP
4
+ class Task < ::LinkChecker::Task
5
+ def run!
6
+ checker._queue(delay: delay) { _run! }
7
+ end
8
+
9
+ private
10
+
11
+ def _run!
12
+ client = checker._client_for(uri)
13
+ headers = ::Protocol::HTTP::Headers[
14
+ [['user-agent', checker.user_agent]] + checker.headers.map { |key, value| [key, value] }
15
+ ]
16
+ request = ::Protocol::HTTP::Request[method, uri.request_uri, headers]
17
+ response = _call(client, request)
18
+ result! Result.new(uri, method, original_uri, request, response, options)
19
+ rescue StandardError => e
20
+ result! ResultError.new(uri, method, original_uri, e, options)
21
+ ensure
22
+ response&.close
23
+ end
24
+
25
+ def _call(client, request)
26
+ if checker.read_timeout
27
+ ::Async::Task.current.with_timeout(checker.read_timeout) { client.call(request) }
28
+ else
29
+ client.call(request)
30
+ end
31
+ end
32
+ end
33
+
34
+ class Checker < LinkChecker::Checker
35
+ extend ::LinkChecker::Async::HTTP::Config
36
+
37
+ attr_accessor(*LinkChecker::Async::HTTP::Config::ATTRIBUTES)
38
+
39
+ def initialize(options = {})
40
+ LinkChecker::Async::HTTP::Config::ATTRIBUTES.each do |key|
41
+ send("#{key}=", options[key] || LinkChecker::Async::HTTP::Config.send(key))
42
+ end
43
+ @queue = []
44
+ @clients = {}
45
+ if max_concurrency && (!max_concurrency.is_a?(Integer) || max_concurrency < 1)
46
+ raise ArgumentError, "Invalid concurrency limit: #{max_concurrency}"
47
+ end
48
+
49
+ super
50
+ end
51
+
52
+ # Runs all queued checks concurrently and blocks until they complete.
53
+ def run
54
+ require 'async/semaphore'
55
+
56
+ Barrier do |barrier|
57
+ @barrier = barrier
58
+ @semaphore = ::Async::Semaphore.new(max_concurrency) if max_concurrency
59
+ _flush!
60
+ end
61
+ ensure
62
+ @barrier = nil
63
+ @semaphore = nil
64
+ _close_clients!
65
+ end
66
+
67
+ def _queue(delay: 0, &block)
68
+ if @barrier
69
+ @barrier.async do
70
+ sleep(delay) if delay.positive?
71
+ @semaphore ? @semaphore.acquire(&block) : block.call
72
+ end
73
+ else
74
+ @queue << [delay, block]
75
+ end
76
+ end
77
+
78
+ # Returns a client for the given URI's host/port/scheme, reusing (and
79
+ # pooling connections on) one client per endpoint across all checks.
80
+ def _client_for(uri)
81
+ key = [uri.scheme, uri.host, uri.port]
82
+ @clients[key] ||= begin
83
+ endpoint = ::Async::HTTP::Endpoint.parse(uri.to_s, timeout: open_timeout, **_endpoint_options)
84
+ ::Async::HTTP::Client.new(endpoint)
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ # Maps the configured protocol (:http1 or :http2) to the
91
+ # corresponding async-http protocol implementation.
92
+ def _endpoint_options
93
+ case protocol
94
+ when :http1
95
+ { protocol: ::Async::HTTP::Protocol::HTTP1 }
96
+ when :http2
97
+ { protocol: ::Async::HTTP::Protocol::HTTP2 }
98
+ when nil
99
+ {}
100
+ else
101
+ raise ArgumentError, "Unsupported protocol: #{protocol.inspect}, expected :http1 or :http2"
102
+ end
103
+ end
104
+
105
+ def _flush!
106
+ queue = @queue
107
+ @queue = []
108
+ queue.each { |delay, block| _queue(delay: delay, &block) }
109
+ end
110
+
111
+ def _close_clients!
112
+ @clients.each_value(&:close)
113
+ @clients = {}
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkChecker
4
+ module Async
5
+ module HTTP
6
+ module Config
7
+ extend self
8
+
9
+ ATTRIBUTES = %i[
10
+ read_timeout
11
+ open_timeout
12
+ protocol
13
+ max_concurrency
14
+ ].freeze
15
+
16
+ attr_accessor(*Config::ATTRIBUTES)
17
+
18
+ def reset
19
+ self.read_timeout = nil
20
+ self.open_timeout = nil
21
+ self.protocol = nil
22
+ self.max_concurrency = nil
23
+ end
24
+ end
25
+
26
+ class << self
27
+ def configure
28
+ block_given? ? yield(Config) : Config
29
+ end
30
+
31
+ def config
32
+ Config
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ LinkChecker::Async::HTTP::Config.reset
@@ -0,0 +1,85 @@
1
+ module LinkChecker
2
+ module Async
3
+ module HTTP
4
+ # A thin, case-insensitive wrapper around Protocol::HTTP::Headers,
5
+ # since Protocol::HTTP::Headers#[] expects a lower-cased key.
6
+ class Headers
7
+ def initialize(headers)
8
+ @headers = headers
9
+ end
10
+
11
+ def [](key)
12
+ value = @headers[key.to_s.downcase]
13
+ value.is_a?(Array) ? value.join(', ') : value
14
+ end
15
+ end
16
+
17
+ # Wraps an Async::HTTP response to expose a `code` and case-insensitive
18
+ # `headers`, similar to the other checkers' response objects.
19
+ class Response
20
+ attr_reader :status, :headers
21
+
22
+ def initialize(response)
23
+ @status = response.status
24
+ @headers = Headers.new(response.headers)
25
+ end
26
+
27
+ def code
28
+ @status.to_s
29
+ end
30
+ end
31
+
32
+ class Result < ::LinkChecker::Result
33
+ attr_accessor :request, :response
34
+
35
+ def initialize(uri, method, original_uri, request, response, options)
36
+ @request = request
37
+ @response = response && Response.new(response)
38
+ super(uri, method, original_uri, options)
39
+ end
40
+
41
+ def error?
42
+ false
43
+ end
44
+
45
+ def failure?
46
+ !success? && !redirect?
47
+ end
48
+
49
+ def code
50
+ @code ||= begin
51
+ response.status
52
+ rescue StandardError
53
+ -1
54
+ end
55
+ end
56
+
57
+ def request_headers
58
+ Headers.new(request.headers)
59
+ end
60
+
61
+ def response_headers
62
+ response&.headers || {}
63
+ end
64
+
65
+ def redirect_to
66
+ return nil unless response
67
+
68
+ response.headers['location']
69
+ end
70
+
71
+ def redirect?
72
+ return false unless response
73
+
74
+ [301, 302, 303, 307, 308].include?(code)
75
+ end
76
+
77
+ def success?
78
+ return false unless response
79
+
80
+ code >= 200 && code <= 299
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'http/config'
2
+ require_relative 'http/result'
3
+ require_relative 'http/checker'