sidekiq-status 0.6.0 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yaml +53 -0
  3. data/.gitignore +4 -1
  4. data/.gitlab-ci.yml +17 -0
  5. data/Appraisals +11 -0
  6. data/CHANGELOG.md +36 -18
  7. data/Gemfile +0 -4
  8. data/README.md +129 -50
  9. data/Rakefile +2 -0
  10. data/gemfiles/sidekiq_6.1.gemfile +7 -0
  11. data/gemfiles/sidekiq_6.x.gemfile +7 -0
  12. data/gemfiles/sidekiq_7.x.gemfile +7 -0
  13. data/lib/sidekiq-status/client_middleware.rb +46 -8
  14. data/lib/sidekiq-status/redis_adapter.rb +18 -0
  15. data/lib/sidekiq-status/redis_client_adapter.rb +14 -0
  16. data/lib/sidekiq-status/server_middleware.rb +76 -20
  17. data/lib/sidekiq-status/sidekiq_extensions.rb +7 -0
  18. data/lib/sidekiq-status/storage.rb +19 -9
  19. data/lib/sidekiq-status/testing/inline.rb +10 -0
  20. data/lib/sidekiq-status/version.rb +1 -1
  21. data/lib/sidekiq-status/web.rb +116 -25
  22. data/lib/sidekiq-status/worker.rb +12 -5
  23. data/lib/sidekiq-status.rb +40 -5
  24. data/sidekiq-status.gemspec +7 -4
  25. data/spec/environment.rb +1 -0
  26. data/spec/lib/sidekiq-status/client_middleware_spec.rb +15 -12
  27. data/spec/lib/sidekiq-status/server_middleware_spec.rb +66 -22
  28. data/spec/lib/sidekiq-status/web_spec.rb +62 -15
  29. data/spec/lib/sidekiq-status/worker_spec.rb +19 -1
  30. data/spec/lib/sidekiq-status_spec.rb +94 -21
  31. data/spec/spec_helper.rb +104 -26
  32. data/spec/support/test_jobs.rb +71 -6
  33. data/web/sidekiq-status-single-web.png +0 -0
  34. data/web/views/status.erb +118 -0
  35. data/web/views/status_not_found.erb +6 -0
  36. data/web/views/statuses.erb +108 -22
  37. metadata +74 -13
  38. data/.travis.yml +0 -16
  39. data/gemfiles/Gemfile.sidekiq-2 +0 -5
@@ -1,16 +1,34 @@
1
+ require 'sidekiq-status'
2
+
1
3
  class StubJob
2
4
  include Sidekiq::Worker
3
5
  include Sidekiq::Status::Worker
4
6
 
5
- sidekiq_options 'retry' => 'false'
7
+ sidekiq_options 'retry' => false
8
+
9
+ def perform(*args)
10
+ end
11
+ end
12
+
13
+ class StubNoStatusJob
14
+ include Sidekiq::Worker
15
+
16
+ sidekiq_options 'retry' => false
6
17
 
7
18
  def perform(*args)
8
19
  end
9
20
  end
10
21
 
22
+
23
+ class ExpiryJob < StubJob
24
+ def expiration
25
+ 15
26
+ end
27
+ end
28
+
11
29
  class LongJob < StubJob
12
30
  def perform(*args)
13
- sleep args[0] || 1
31
+ sleep args[0] || 0.25
14
32
  end
15
33
  end
16
34
 
@@ -23,6 +41,13 @@ class DataJob < StubJob
23
41
  end
24
42
  end
25
43
 
44
+ class CustomDataJob < StubJob
45
+ def perform
46
+ store({mister_cat: 'meow'})
47
+ sleep 0.5
48
+ end
49
+ end
50
+
26
51
  class ProgressJob < StubJob
27
52
  def perform
28
53
  total 500
@@ -54,25 +79,65 @@ class FailingJob < StubJob
54
79
  end
55
80
  end
56
81
 
82
+ class FailingNoStatusJob < StubNoStatusJob
83
+ def perform
84
+ raise StandardError
85
+ end
86
+ end
87
+
88
+ class RetryAndFailJob < StubJob
89
+ sidekiq_options retry: 1
90
+
91
+ def perform
92
+ raise StandardError
93
+ end
94
+ end
95
+
96
+ class FailingHardJob < StubJob
97
+ def perform
98
+ raise Exception
99
+ end
100
+ end
101
+
102
+ class FailingHardNoStatusJob < StubNoStatusJob
103
+ def perform
104
+ raise Exception
105
+ end
106
+ end
107
+
57
108
  class ExitedJob < StubJob
58
109
  def perform
59
110
  raise SystemExit
60
111
  end
61
112
  end
62
113
 
114
+ class ExitedNoStatusJob < StubNoStatusJob
115
+ def perform
116
+ raise SystemExit
117
+ end
118
+ end
119
+
63
120
  class InterruptedJob < StubJob
64
121
  def perform
65
122
  raise Interrupt
66
123
  end
67
124
  end
68
125
 
126
+ class InterruptedNoStatusJob < StubNoStatusJob
127
+ def perform
128
+ raise Interrupt
129
+ end
130
+ end
131
+
69
132
  class RetriedJob < StubJob
70
- sidekiq_options 'retry' => 'true'
71
- def perform()
133
+
134
+ sidekiq_options retry: true
135
+ sidekiq_retry_in do |count| 3 end # 3 second delay > job timeout in test suite
136
+
137
+ def perform
72
138
  Sidekiq.redis do |conn|
73
139
  key = "RetriedJob_#{jid}"
74
- sleep 1
75
- unless conn.exists key
140
+ if [0, false].include? conn.exists(key)
76
141
  conn.set key, 'tried'
77
142
  raise StandardError
78
143
  end
Binary file
@@ -0,0 +1,118 @@
1
+ <% require 'cgi'; def h(v); CGI.escape_html(v.to_s); end %>
2
+ <style>
3
+ .progress {
4
+ background-color: #C8E1ED;
5
+ }
6
+ .progress-percentage {
7
+ margin: 5px;
8
+ padding-left: 4px;
9
+ color: #333;
10
+ text-align: left;
11
+ text-shadow: 0 0 5px white;
12
+ font-weight: bold;
13
+ font-size: 14px;
14
+ }
15
+ </style>
16
+
17
+ <h3>
18
+ Job Status: <%= h @status["jid"] %>
19
+ <span class='label label-<%= h @status["label"] %>'>
20
+ <%= h @status["status"] %>
21
+ </span>
22
+ </h3>
23
+
24
+ <div class="progress" style="height: 30px;">
25
+ <div class="progress-bar" role="progressbar" aria-valuenow="<%= @status["pct_complete"].to_i %>" aria-valuemin="0" aria-valuemax="100" style="width: <%= @status["pct_complete"].to_i %>%">
26
+ <div class="progress-percentage">
27
+ <%= @status["pct_complete"].to_i %>%
28
+ </div>
29
+ </div>
30
+ </div>
31
+
32
+ <div class="panel panel-default" style="background-color: inherit">
33
+ <div class="panel-body">
34
+ <h4><%= h @status["worker"] %></h4>
35
+
36
+ <div class="row">
37
+ <div class="col-sm-2">
38
+ <strong>Arguments</strong>
39
+ </div>
40
+ <div class="col-sm-10">
41
+ <p><%= @status["args"].empty? ? "<i>none</i>" : h(@status["args"]) %></p>
42
+ </div>
43
+ </div>
44
+
45
+ <div class="row">
46
+ <div class="col-sm-2">
47
+ <strong>Message</strong>
48
+ </div>
49
+ <div class="col-sm-10">
50
+ <p><%= h(@status["message"]) || "<i>none</i>" %></p>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="row">
55
+ <div class="col-sm-2">
56
+ <strong>Last Updated</strong>
57
+ </div>
58
+ <div class="col-sm-10">
59
+ <p>
60
+ <% secs = Time.now.to_i - @status["update_time"].to_i %>
61
+ <% if secs > 0 %>
62
+ <%= ChronicDuration.output(secs, :weeks => true, :units => 2) %> ago
63
+ <% else %>
64
+ Now
65
+ <% end %>
66
+ </p>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="row">
71
+ <div class="col-sm-2">
72
+ <strong>Elapsed Time</strong>
73
+ </div>
74
+ <div class="col-sm-10">
75
+ <p>
76
+ <% if @status["elapsed"] %>
77
+ <%= ChronicDuration.output(@status["elapsed"].to_i, :weeks => true, :units => 2) %>
78
+ <% end %>
79
+ </p>
80
+ </div>
81
+ </div>
82
+
83
+ <div class="row">
84
+ <div class="col-sm-2">
85
+ <strong>ETA</strong>
86
+ </div>
87
+ <div class="col-sm-10">
88
+ <p>
89
+ <% if @status["eta"] %>
90
+ <%= ChronicDuration.output(@status["eta"].to_i, :weeks => true, :units => 2) %>
91
+ <% end %>
92
+ </p>
93
+ </div>
94
+ </div>
95
+
96
+ <% if @status["custom"].any? %>
97
+ <hr>
98
+ <% @status["custom"].each do |key, val| %>
99
+ <div class="row">
100
+ <div class="col-sm-2">
101
+ <strong><%= key %></strong>
102
+ </div>
103
+ <div class="col-sm-10">
104
+ <% if val && val.include?("\n") %>
105
+ <pre><%= h val %></pre>
106
+ <% else %>
107
+ <p><%= h(val) || "<i>none</i>" %></p>
108
+ <% end %>
109
+ </div>
110
+ </div>
111
+ <% end %>
112
+ <% end %>
113
+ </div>
114
+ </div>
115
+
116
+ <a href="<%= root_path %>statuses">
117
+ <small>&larr; back to all statuses</small>
118
+ </a>
@@ -0,0 +1,6 @@
1
+ <% require 'cgi'; def h(v); CGI.escape_html(v.to_s); end %>
2
+ <h3>Job Status: <%= h params[:jid] %></h3>
3
+
4
+ <div role="alert">
5
+ <strong>Uh oh!</strong> That job can't be found. It may have expired already.
6
+ </div>
@@ -1,11 +1,11 @@
1
-
1
+ <% require 'cgi'; def h(v); CGI.escape_html(v.to_s); end %>
2
2
  <style>
3
3
  .progress {
4
4
  background-color: #C8E1ED;
5
5
  }
6
6
  .bar {
7
- background-color: #2897cb;
8
- color: white;
7
+ background-color: #2897cb;
8
+ color: white;
9
9
  text-shadow: 0 0 0;
10
10
  }
11
11
  .message {
@@ -13,35 +13,96 @@
13
13
  font-weight: bold; padding-left: 4px;
14
14
  color: #333;
15
15
  }
16
- .header{
16
+ .actions {
17
+ text-align: center;
18
+ }
19
+ .header {
17
20
  text-align: center;
18
21
  }
22
+ .header_update_time {
23
+ width: 10%;
24
+ }
25
+ .header_pct_complete {
26
+ width: 45%;
27
+ }
28
+ .btn-warning {
29
+ background-image: linear-gradient(#f0ad4e, #eea236)
30
+ }
31
+ .nav-container {
32
+ display: flex;
33
+ line-height: 45px;
34
+ }
35
+ .nav-container .pull-right {
36
+ float: none !important;
37
+ }
38
+ .nav-container .pagination {
39
+ display: flex;
40
+ align-items: center;
41
+ }
42
+ .nav-container .per-page, .filter-status {
43
+ display: flex;
44
+ align-items: center;
45
+ margin: 20px 0 20px 10px;
46
+ white-space: nowrap;
47
+ }
48
+ .nav-container .per-page SELECT {
49
+ margin: 0 0 0 5px;
50
+ }
19
51
  </style>
52
+ <script>
53
+ function setPerPage(select){
54
+ window.location = select.options[select.selectedIndex].getAttribute('data-url')
55
+ }
56
+ </script>
57
+ <div style="display: flex; justify-content: space-between;">
58
+ <h3 class="wi">Recent job statuses</h3>
59
+ <div class="nav-container">
60
+ <%= erb :_paging, locals: { url: "#{root_path}statuses" } %>
61
+ <div class="filter-status">
62
+ Filter Status:
63
+ <select class="form-control" onchange="setPerPage(this)">
64
+ <% (['all', 'complete', 'failed', 'interrupted', 'queued', 'retrying', 'stopped', 'working']).each do |status| %>
65
+ <option data-url="?<%= qparams(status: status)%>" value="<%= status %>" <%= 'selected="selected"' if status == (params[:status]) %>><%= status %></option>
66
+ <% end %>
67
+ </select>
68
+ </div>
20
69
 
21
- <h3 class="wi">Recent job statuses</h3>
22
- <table class="table table-hover table-bordered table-striped table-white">
70
+ <div class="per-page">
71
+ Per page:
72
+ <select class="form-control" onchange="setPerPage(this)">
73
+ <% (Sidekiq::Status::Web.per_page_opts + ['all']).each do |num| %>
74
+ <option data-url="?<%= qparams(page: 1, per_page: num)%>" value="<%= num %>" <%= 'selected="selected"' if num.to_s == (params[:per_page] || @count) %>><%= num %></option>
75
+ <% end %>
76
+ </select>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ <table class="table table-hover table-bordered table-striped">
23
81
  <tr>
24
- <th class="header">Worker/jid</th>
25
- <th class="header">Arguments</th>
26
- <th class="header">Status</th>
27
- <th class="header" style="width: 10%;">Last Updated</th>
28
- <th class="header" style="width: 45%;">Progress</th>
82
+ <% @headers.each do |hdr| %>
83
+ <th class="header <%= h hdr[:class] %> header_<%= h hdr[:id] %>">
84
+ <a href="<%= h hdr[:url] %>"><%= h hdr[:name] %></a>
85
+ </th>
86
+ <% end %>
87
+ <th class="header">
88
+ Actions
89
+ </th>
29
90
  </tr>
30
91
  <% @statuses.each do |container| %>
31
92
  <tr>
32
93
  <td>
33
- <div title='<%= container.jid %>'><%= container.worker %></div>
94
+ <div title='<%= h container["jid"] %>'><a href="<%= root_path %>statuses/<%= h container["jid"] %>"><%= h container["worker"] %></a></div>
34
95
  </td>
35
96
  <td>
36
- <div class='args' title='<%= container.jid %>'><%= container.args %></div>
97
+ <div class='args' title='<%= h container["jid"] %>'><%= h container["args"] %></div>
37
98
  </td>
38
99
  <td style='text-align: center;'>
39
- <div class='label label-<%= container.label %>'><%= container.status %></div>
100
+ <div class='label label-<%= h container["label"] %>'><%= h container["status"] %></div>
40
101
  </td>
41
- <% secs = Time.now.to_i - container.update_time.to_i %>
42
- <td style='text-align: center;'>
102
+ <% secs = Time.now.to_i - container["update_time"].to_i %>
103
+ <td style='text-align: center; white-space: nowrap;' title="<%= Time.at(container["update_time"].to_i) %>">
43
104
  <% if secs > 0 %>
44
- <%= secs %> sec<%= secs == 1 ? '' : 's' %> ago
105
+ <%= ChronicDuration.output(secs, :weeks => true, :units => 2) %> ago
45
106
  <% else %>
46
107
  Now
47
108
  <% end %>
@@ -49,15 +110,40 @@
49
110
  <td>
50
111
  <div class="progress progress-striped" style="margin-bottom: 0">
51
112
  <div class='message' style='text-align:right; padding-right:0.5em; background-color: transparent; float:right;'>
52
- <%= container.message %>
113
+ <%= h container["message"] %>
53
114
  </div>
54
- <% if container.pct_complete.to_i > 0 %>
55
- <div class="bar message" style="width: <%= container.pct_complete %>%;">
56
- <%= container.pct_complete %>%
115
+ <% if container["pct_complete"].to_i > 0 %>
116
+ <div class="bar message" style="width: <%= h container["pct_complete"] %>%;">
117
+ <%= h container["pct_complete"] %>%
57
118
  </div>
58
119
  <% end %>
59
120
  </div>
60
121
  </td>
122
+ <td style='text-align: center; white-space: nowrap;'>
123
+ <% if container["elapsed"] %>
124
+ <%= ChronicDuration.output(container["elapsed"].to_i, :weeks => true, :units => 2) %>
125
+ <% end %>
126
+ </td>
127
+ <td style='text-align: center; white-space: nowrap;'>
128
+ <% if container["eta"] %>
129
+ <%= ChronicDuration.output(container["eta"].to_i, :weeks => true, :units => 2) %>
130
+ <% end %>
131
+ </td>
132
+ <td>
133
+ <div class="actions">
134
+ <form action="statuses" method="post">
135
+ <input type="hidden" name="jid" value="<%= h container["jid"] %>" />
136
+ <%= csrf_tag %>
137
+ <% if container["status"] == "complete" %>
138
+ <input type="hidden" name="_method" value="delete" />
139
+ <input type="submit" class="btn btn-danger btn-xs" value="Remove" />
140
+ <% elsif container["status"] == "failed" %>
141
+ <input type="hidden" name="_method" value="put" />
142
+ <input type="submit" class="btn btn-warning btn-xs" value="Retry Now" />
143
+ <% end %>
144
+ </form>
145
+ </div>
146
+ </td>
61
147
  </tr>
62
148
  <% end %>
63
149
  <% if @statuses.empty? %>
@@ -65,4 +151,4 @@
65
151
  <td colspan="6"></td>
66
152
  </tr>
67
153
  <% end %>
68
- </table>
154
+ </table>
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-status
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 3.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgeniy Tsvigun
8
- autorequire:
8
+ - Kenaniah Cerny
9
+ autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2015-12-27 00:00:00.000000000 Z
12
+ date: 2023-05-04 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: sidekiq
@@ -16,14 +17,62 @@ dependencies:
16
17
  requirements:
17
18
  - - ">="
18
19
  - !ruby/object:Gem::Version
19
- version: '2.7'
20
+ version: '6.0'
21
+ - - "<"
22
+ - !ruby/object:Gem::Version
23
+ version: '8'
20
24
  type: :runtime
21
25
  prerelease: false
22
26
  version_requirements: !ruby/object:Gem::Requirement
23
27
  requirements:
24
28
  - - ">="
25
29
  - !ruby/object:Gem::Version
26
- version: '2.7'
30
+ version: '6.0'
31
+ - - "<"
32
+ - !ruby/object:Gem::Version
33
+ version: '8'
34
+ - !ruby/object:Gem::Dependency
35
+ name: chronic_duration
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ type: :runtime
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ - !ruby/object:Gem::Dependency
49
+ name: appraisal
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: colorize
64
+ requirement: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ type: :development
70
+ prerelease: false
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
27
76
  - !ruby/object:Gem::Dependency
28
77
  name: rack-test
29
78
  requirement: !ruby/object:Gem::Requirement
@@ -80,31 +129,40 @@ dependencies:
80
129
  - - ">="
81
130
  - !ruby/object:Gem::Version
82
131
  version: '0'
83
- description:
132
+ description:
84
133
  email:
85
134
  - utgarda@gmail.com
135
+ - kenaniah@gmail.com
86
136
  executables: []
87
137
  extensions: []
88
138
  extra_rdoc_files: []
89
139
  files:
140
+ - ".github/workflows/ci.yaml"
90
141
  - ".gitignore"
142
+ - ".gitlab-ci.yml"
91
143
  - ".rspec"
92
- - ".travis.yml"
144
+ - Appraisals
93
145
  - CHANGELOG.md
94
146
  - Gemfile
95
147
  - LICENSE
96
148
  - README.md
97
149
  - Rakefile
98
- - gemfiles/Gemfile.sidekiq-2
150
+ - gemfiles/sidekiq_6.1.gemfile
151
+ - gemfiles/sidekiq_6.x.gemfile
152
+ - gemfiles/sidekiq_7.x.gemfile
99
153
  - lib/sidekiq-status.rb
100
154
  - lib/sidekiq-status/client_middleware.rb
155
+ - lib/sidekiq-status/redis_adapter.rb
156
+ - lib/sidekiq-status/redis_client_adapter.rb
101
157
  - lib/sidekiq-status/server_middleware.rb
158
+ - lib/sidekiq-status/sidekiq_extensions.rb
102
159
  - lib/sidekiq-status/storage.rb
103
160
  - lib/sidekiq-status/testing/inline.rb
104
161
  - lib/sidekiq-status/version.rb
105
162
  - lib/sidekiq-status/web.rb
106
163
  - lib/sidekiq-status/worker.rb
107
164
  - sidekiq-status.gemspec
165
+ - spec/environment.rb
108
166
  - spec/lib/sidekiq-status/client_middleware_spec.rb
109
167
  - spec/lib/sidekiq-status/server_middleware_spec.rb
110
168
  - spec/lib/sidekiq-status/testing_spec.rb
@@ -113,13 +171,16 @@ files:
113
171
  - spec/lib/sidekiq-status_spec.rb
114
172
  - spec/spec_helper.rb
115
173
  - spec/support/test_jobs.rb
174
+ - web/sidekiq-status-single-web.png
116
175
  - web/sidekiq-status-web.png
176
+ - web/views/status.erb
177
+ - web/views/status_not_found.erb
117
178
  - web/views/statuses.erb
118
- homepage: http://github.com/utgarda/sidekiq-status
179
+ homepage: https://github.com/kenaniah/sidekiq-status
119
180
  licenses:
120
181
  - MIT
121
182
  metadata: {}
122
- post_install_message:
183
+ post_install_message:
123
184
  rdoc_options: []
124
185
  require_paths:
125
186
  - lib
@@ -134,12 +195,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
134
195
  - !ruby/object:Gem::Version
135
196
  version: '0'
136
197
  requirements: []
137
- rubyforge_project:
138
- rubygems_version: 2.4.5.1
139
- signing_key:
198
+ rubygems_version: 3.4.1
199
+ signing_key:
140
200
  specification_version: 4
141
201
  summary: An extension to the sidekiq message processing to track your jobs
142
202
  test_files:
203
+ - spec/environment.rb
143
204
  - spec/lib/sidekiq-status/client_middleware_spec.rb
144
205
  - spec/lib/sidekiq-status/server_middleware_spec.rb
145
206
  - spec/lib/sidekiq-status/testing_spec.rb
data/.travis.yml DELETED
@@ -1,16 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.0
4
- - 2.1
5
- - 2.2
6
- - rbx-2
7
- gemfile:
8
- - gemfiles/Gemfile.sidekiq-2
9
- - Gemfile
10
- before_install:
11
- - gem update --system
12
- - gem update bundler
13
- services: redis
14
- matrix:
15
- allow_failures:
16
- - rvm: rbx-2
@@ -1,5 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- gemspec path: '..'
4
-
5
- gem 'sidekiq', '~> 2.17'