vpsb_client 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +29 -0
  6. data/Rakefile +2 -0
  7. data/bin/close_trial.rb +20 -0
  8. data/bin/create_trial.rb +13 -0
  9. data/bin/get_current_trial.rb +8 -0
  10. data/bin/get_item_ids.rb +9 -0
  11. data/bin/get_trial_last_metric.rb +8 -0
  12. data/bin/signed_in.rb +7 -0
  13. data/bin/sync_metrics.rb +19 -0
  14. data/bin/test_timing_interval.rb +18 -0
  15. data/bin/upload_metric.rb +37 -0
  16. data/doc/design.rb +109 -0
  17. data/lib/vpsb_client/api/close_trial_request.rb +23 -0
  18. data/lib/vpsb_client/api/create_trial_request.rb +38 -0
  19. data/lib/vpsb_client/api/get_current_trial_request.rb +30 -0
  20. data/lib/vpsb_client/api/get_item_id_request.rb +33 -0
  21. data/lib/vpsb_client/api/get_trial_last_metric_request.rb +25 -0
  22. data/lib/vpsb_client/api/post_metric_request.rb +28 -0
  23. data/lib/vpsb_client/api/request.rb +57 -0
  24. data/lib/vpsb_client/api/response.rb +30 -0
  25. data/lib/vpsb_client/builders/system_info_parser.rb +127 -0
  26. data/lib/vpsb_client/builders/trial.rb +54 -0
  27. data/lib/vpsb_client/client/upload_metrics.rb +39 -0
  28. data/lib/vpsb_client/config.rb +18 -0
  29. data/lib/vpsb_client/curl_wrapper.rb +41 -0
  30. data/lib/vpsb_client/datafiles/formatted_sar_log_parser.rb +38 -0
  31. data/lib/vpsb_client/datafiles/logfile_decompressor.rb +48 -0
  32. data/lib/vpsb_client/datafiles/pxx_aggregator.rb +94 -0
  33. data/lib/vpsb_client/datafiles/sar_manager.rb +71 -0
  34. data/lib/vpsb_client/datafiles/timing_log_parser.rb +32 -0
  35. data/lib/vpsb_client/http_client.rb +60 -0
  36. data/lib/vpsb_client/manager.rb +149 -0
  37. data/lib/vpsb_client/metrics/interval_builder.rb +85 -0
  38. data/lib/vpsb_client/metrics/interval_config.rb +45 -0
  39. data/lib/vpsb_client/metrics/manager.rb +34 -0
  40. data/lib/vpsb_client/metrics/uploader.rb +16 -0
  41. data/lib/vpsb_client/version.rb +3 -0
  42. data/lib/vpsb_client.rb +33 -0
  43. data/spec/lib/client/upload_metrics_spec.rb +23 -0
  44. data/spec/lib/config_spec.rb +26 -0
  45. data/spec/lib/logfile_decompressor_spec.rb +74 -0
  46. data/spec/lib/metrics/interval_builder_spec.rb +88 -0
  47. data/spec/lib/metrics/interval_config_spec.rb +99 -0
  48. data/spec/lib/metrics/manager_spec.rb +60 -0
  49. data/spec/lib/metrics/uploader_spec.rb +45 -0
  50. data/spec/lib/pxx_spec.rb +120 -0
  51. data/spec/lib/request_spec.rb +186 -0
  52. data/spec/lib/sar_manager_spec.rb +57 -0
  53. data/spec/lib/system_info_parser_spec.rb +51 -0
  54. data/spec/spec_helper.rb +9 -0
  55. data/spec/support/sarfiles/formatted/formatted_sa20 +147 -0
  56. data/spec/support/sarfiles/formatted/formatted_sa21 +85 -0
  57. data/spec/support/sarfiles/orig/sa18 +0 -0
  58. data/spec/support/sarfiles/orig/sa19 +0 -0
  59. data/spec/support/sarfiles/orig/sa20 +0 -0
  60. data/spec/support/sarfiles/orig/sa21 +0 -0
  61. data/spec/support/timingfiles/other.log.2.gz +0 -0
  62. data/spec/support/timingfiles/timings.log +11 -0
  63. data/spec/support/timingfiles/timings.log.1 +11 -0
  64. data/spec/support/timingfiles/timings.log.2.gz +0 -0
  65. data/spec/support/timingfiles/timings.log.3.gz +0 -0
  66. data/spec/support/vpsb.yml +12 -0
  67. data/vpsb_client.gemspec +29 -0
  68. metadata +241 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 10ca3e79c344f1e1453d72c01354f772f1349a92
4
+ data.tar.gz: 5bb7605cad616ec8eb65445f6c497aea5f83ae1b
5
+ SHA512:
6
+ metadata.gz: ee479c738e4708b7515c9b5a34b94dbc4ee96d99e26bf6ca7bfea8d2466eb455d9f63457b0b6d1cf55a39dc0ff4787c7685e0a47e1523418c2e66ca02cc33990
7
+ data.tar.gz: 813ee17457596d106558a87aa8e32ead6b0b85f31bed244dc2401a4dc1530825501f5ff05af2b3c802adb0a3a81c8ad0c53a9e6fe94c6fac7dff33271f8aa219
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ config/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in vpsb_client.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Philippe Le Rohellec
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # VpsbClient
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'vpsb_client'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install vpsb_client
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it ( https://github.com/[my-github-username]/vpsb_client/fork )
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'awesome_print'
4
+ require File.expand_path('../../lib/vpsb_client', __FILE__)
5
+
6
+ manager = VpsbClient::Manager.new(File.expand_path('../../config/vpsb.yml', __FILE__))
7
+ manager.setup
8
+
9
+ trial = manager.current_trial
10
+ unless trial
11
+ puts "Current trial not found"
12
+ exit 1
13
+ end
14
+
15
+ trial_id = trial['id']
16
+ puts "trial_id=#{trial_id}"
17
+
18
+ trial = manager.current_trial
19
+ metric_ids = manager.close_trial(trial)
20
+
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../lib/vpsb_client', __FILE__)
4
+
5
+ manager = VpsbClient::Manager.new(File.expand_path('../../config/vpsb.yml', __FILE__))
6
+ manager.setup
7
+
8
+ t = manager.create_trial
9
+ if t
10
+ puts "new trial = #{t.inspect}"
11
+ else
12
+ puts "no trial was created"
13
+ end
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../lib/vpsb_client', __FILE__)
4
+
5
+ manager = VpsbClient::Manager.new(File.expand_path('../../config/vpsb.yml', __FILE__))
6
+ manager.setup
7
+
8
+ puts manager.current_trial
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require '/home/philippe/src/plerohellec/vpsb_client/lib/vpsb_client'
4
+
5
+ m = VpsbClient::Manager.new '/home/philippe/src/plerohellec/vpsb_client/config/vpsb.yml'
6
+ m.setup
7
+ puts m.hoster_id
8
+ puts m.application_id
9
+ puts m.plan_id
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../lib/vpsb_client', __FILE__)
4
+
5
+ manager = VpsbClient::Manager.new(File.expand_path('../../config/vpsb.yml', __FILE__))
6
+ manager.setup
7
+
8
+ puts manager.trial_last_metric_started_at(22, 3600)
data/bin/signed_in.rb ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../lib/vpsb_client', __FILE__)
4
+
5
+ manager = VpsbClient::Manager.new(File.expand_path('../../config/vpsb.yml', __FILE__))
6
+ manager.setup
7
+ puts "signed in? #{manager.signed_in?}"
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'awesome_print'
4
+ require File.expand_path('../../lib/vpsb_client', __FILE__)
5
+
6
+ manager = VpsbClient::Manager.new(File.expand_path('../../config/vpsb.yml', __FILE__))
7
+ manager.setup
8
+
9
+ trial_id = manager.current_trial['id']
10
+ unless trial_id
11
+ puts "Cannot find current trial"
12
+ exit 1
13
+ end
14
+ puts "trial_id=#{trial_id}"
15
+
16
+ trial = manager.current_trial
17
+ metric_ids = manager.upload_metrics(trial)
18
+ puts "metric_ids=#{metric_ids.inspect}"
19
+
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/vpsb_client'
4
+
5
+ logfile = LogfileInterval::Logfile.new('timings.log.2', VpsbClient::Datafiles::TimingLogParser)
6
+
7
+ builder = LogfileInterval::IntervalBuilder.new(logfile, VpsbClient::Datafiles::TimingLogParser, 600)
8
+ builder.each_interval do |interval|
9
+ next unless interval.size > 0
10
+
11
+ if interval[:resptime_total_ms] > interval[:pxx_total_ms][95]
12
+ puts
13
+ puts "start time of interval: #{interval.start_time}"
14
+ puts "number of requests found in interval: #{interval.size}"
15
+ puts "average total ms #{interval[:resptime_total_ms]}"
16
+ puts "p95 total ms #{interval[:pxx_total_ms][95]}"
17
+ end
18
+ end
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../lib/vpsb_client', __FILE__)
4
+
5
+ manager = VpsbClient::Manager.new(File.expand_path('../../config/vpsb.yml', __FILE__))
6
+ manager.setup
7
+
8
+ trial_id = manager.current_trial['id']
9
+ unless trial_id
10
+ puts "Cannot find current trial"
11
+ exit 1
12
+ end
13
+
14
+ interval = {}
15
+ interval[:duration_seconds] = 3600
16
+ interval[:started_at] = Time.now - 3600
17
+ interval[:cpu_idle] = 100
18
+ interval[:cpu_steal] = 0
19
+ interval[:iowait] = 0
20
+ interval[:p50_total_ms] = 1
21
+ interval[:p75_total_ms] = 1
22
+ interval[:p95_total_ms] = 1
23
+ interval[:p99_total_ms] = 1
24
+ interval[:p75_iowait_pct] = 1
25
+ interval[:p95_iowait_pct] = 1
26
+ interval[:p99_iowait_pct] = 1
27
+ interval[:p75_cpusteal_pct] = 1
28
+ interval[:p95_cpusteal_pct] = 1
29
+ interval[:p99_cpusteal_pct] = 1
30
+ interval[:p75_cpuidle_pct] = 1
31
+ interval[:p95_cpuidle_pct] = 1
32
+ interval[:p99_cpuidle_pct] = 1
33
+
34
+ upload_request = VpsbClient::Api::PostMetricRequest.new(manager.http_client, trial_id, interval)
35
+ http_response = VpsbClient::Api::Response.new(upload_request.run)
36
+ metric_id = VpsbClient::Api::PostMetricRequest.metric_id(http_response)
37
+ puts metric_id
data/doc/design.rb ADDED
@@ -0,0 +1,109 @@
1
+ class Manager
2
+ attr_reader :client
3
+
4
+ def initialize(client)
5
+ end
6
+
7
+ def get_current_trial
8
+ end
9
+
10
+ def create_trial
11
+ end
12
+
13
+ def last_uploaded_metric_end_date
14
+ end
15
+
16
+ def run
17
+
18
+ end
19
+ end
20
+
21
+ class Config
22
+ attr_reader :application_name, :hoster_name, :plan_name
23
+
24
+ def initialize(path)
25
+ raise ArgumentError, "Can't find #{path}" unless File.exist?(path)
26
+ @yml = YAML.load_file(path)
27
+ end
28
+
29
+ def value(name)
30
+ @yam.fetch(name)
31
+ end
32
+ end
33
+
34
+ class AuthenticatedClient
35
+ attr_reader :host
36
+
37
+ def inialize(config_file_path)
38
+ end
39
+
40
+ def get_current_trial(date, app_name, hoster_name, plan_name)
41
+ end
42
+
43
+ def create_trial(start_date, app_name, hoster_name, plan_name)
44
+ end
45
+
46
+ def get_last_upload_date(trial_id)
47
+ end
48
+
49
+ def post_metric_interval(start_date, length, post_data)
50
+ end
51
+ end
52
+
53
+ module Request
54
+
55
+ class Request
56
+ def initialize(config, http_client)
57
+ end
58
+
59
+ def run
60
+ case method
61
+ when :get
62
+ then http_client.get(url, query_params)
63
+ when :post
64
+ then http_client.post(url, query_params, post_params)
65
+ end
66
+ end
67
+ end
68
+
69
+ class GetRequest < Request
70
+ def method
71
+ :get
72
+ end
73
+ end
74
+
75
+ class PostRequest
76
+ def method
77
+ :post
78
+ end
79
+ end
80
+
81
+ class GetCurrentTrial < GetRequest
82
+ def initialize(config, http_client, application_id, plan_id, hoster_id)
83
+ end
84
+
85
+ def url
86
+ "https://#{@host}/api/trials/current"
87
+ end
88
+
89
+ def query_params
90
+ { hoster_id: @hoster_id,
91
+ application_id: @application_id,
92
+ plan_id: @plan_id
93
+ }
94
+ end
95
+ end
96
+ end
97
+
98
+
99
+ class DataCollector
100
+ def cpuinfo
101
+ end
102
+
103
+ def freemem
104
+ end
105
+
106
+
107
+ end
108
+
109
+
@@ -0,0 +1,23 @@
1
+ module VpsbClient
2
+ module Api
3
+ class CloseTrialRequest < PutRequest
4
+
5
+ def initialize(http_client, trial_id)
6
+ super(http_client)
7
+ @trial_id = trial_id
8
+ end
9
+
10
+ def url_path
11
+ "/api/trials/#{@trial_id}/close"
12
+ end
13
+
14
+ def put_params
15
+ @put_params = { }
16
+ end
17
+
18
+ def content_type
19
+ 'application/json'
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ module VpsbClient
2
+ module Api
3
+ class CreateTrialRequest < PostRequest
4
+ MANDATORY_PARAM_NAMES = [ :started_at, :hoster_id, :application_id, :plan_id, :comment, :os, :free_memory_mb, :cpu_type, :num_cores, :kernel, :client_hostname, :ruby_version, :rails_version, :datacenter, :cpu_mhz]
5
+
6
+ def initialize(http_client, trial)
7
+ super(http_client)
8
+ @trial = trial
9
+ MANDATORY_PARAM_NAMES.each do |name|
10
+ raise ArgumentError, "param #{name} is mandatory" unless @trial.keys.include?(name)
11
+ end
12
+ @trial.keys.each do |name|
13
+ raise ArgumentError, "param #{name} is not allowed" unless MANDATORY_PARAM_NAMES.include?(name)
14
+ end
15
+ end
16
+
17
+ def url_path
18
+ "/api/trials"
19
+ end
20
+
21
+ def post_params
22
+ @post_params = { trial: @trial }
23
+ end
24
+
25
+ def content_type
26
+ 'application/json'
27
+ end
28
+
29
+ def self.trial(http_response)
30
+ http_response.parsed_response
31
+ end
32
+
33
+ def self.trial_id(http_response)
34
+ trial(http_response)['id']
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,30 @@
1
+ module VpsbClient
2
+ module Api
3
+ class GetCurrentTrialRequest < GetRequest
4
+ def initialize(http_client, params)
5
+ super(http_client)
6
+ @client_hostname = params.fetch(:client_hostname)
7
+ end
8
+
9
+ def url_path
10
+ "/api/trials/current"
11
+ end
12
+
13
+ def query_params
14
+ {
15
+ client_hostname: @client_hostname
16
+ }
17
+ end
18
+
19
+ def self.trial(http_response)
20
+ return nil if http_response.parsed_response.empty?
21
+ http_response.parsed_response.first
22
+ end
23
+
24
+ def self.trial_id(http_response)
25
+ t = trial(http_response)
26
+ t ? t['id'] : nil
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ require 'erb'
2
+
3
+ module VpsbClient
4
+ module Api
5
+ class GetItemIdRequest < GetRequest
6
+ def initialize(http_client, item_type, name)
7
+ super(http_client)
8
+ @item_type = item_type
9
+ @name = name
10
+ end
11
+
12
+ def url_path
13
+ "/api/#{@item_type}/by_name/#{ERB::Util.url_encode(@name)}"
14
+ end
15
+
16
+ def self.item_id(http_response)
17
+ return nil unless http_response.parsed_response.any?
18
+ http_response.parsed_response.first['id']
19
+ end
20
+ end
21
+
22
+ class GetPlanIdRequest < GetItemIdRequest
23
+ def initialize(http_client, hoster_id, plan_name)
24
+ super(http_client, 'plans', plan_name)
25
+ @hoster_id = hoster_id
26
+ end
27
+
28
+ def query_params
29
+ { hoster_id: @hoster_id }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ module VpsbClient
2
+ module Api
3
+ class GetTrialLastMetricRequest < GetRequest
4
+ def initialize(http_client, params)
5
+ super(http_client)
6
+ @trial_id = params[:trial_id]
7
+ @length = params[:length]
8
+ end
9
+
10
+ def url_path
11
+ "/api/trials/#{@trial_id}/last_metric"
12
+ end
13
+
14
+ def query_params
15
+ { length: @length }
16
+ end
17
+
18
+ def self.started_at(http_response)
19
+ return nil if http_response.parsed_response.empty?
20
+ metric = http_response.parsed_response.first
21
+ DateTime.parse(metric['started_at']).to_time
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ module VpsbClient
2
+ module Api
3
+ class PostMetricRequest < PostRequest
4
+
5
+ def initialize(http_client, trial_id, metric)
6
+ super(http_client)
7
+ @trial_id = trial_id
8
+ @metric = metric
9
+ end
10
+
11
+ def url_path
12
+ "/api/metrics"
13
+ end
14
+
15
+ def post_params
16
+ @post_params = { metric: @metric.merge(trial_id: @trial_id) }
17
+ end
18
+
19
+ def content_type
20
+ 'application/json'
21
+ end
22
+
23
+ def self.metric_id(http_response)
24
+ http_response.parsed_response['id']
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ module VpsbClient
2
+ module Api
3
+ class Request
4
+ def initialize(http_client)
5
+ @http_client = http_client
6
+ end
7
+
8
+ def query_params
9
+ {}
10
+ end
11
+
12
+ def post_params
13
+ {}
14
+ end
15
+
16
+ def put_params
17
+ {}
18
+ end
19
+
20
+ def accept
21
+ 'application/json'
22
+ end
23
+ end
24
+
25
+ class GetRequest < Request
26
+ def run
27
+ VpsbClient.logger.debug "class=#{self.class} url_path=#{url_path}"
28
+ @http_client.get(self)
29
+ end
30
+ end
31
+
32
+ class PostRequest < Request
33
+ def initialize(http_client)
34
+ super(http_client)
35
+ end
36
+
37
+ def run
38
+ @http_client.post(self)
39
+ end
40
+
41
+ def content_type
42
+ 'application/x-www-form-urlencoded'
43
+ end
44
+ end
45
+
46
+
47
+ class PutRequest < Request
48
+ def initialize(http_client)
49
+ super(http_client)
50
+ end
51
+
52
+ def run
53
+ @http_client.put(self)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,30 @@
1
+ require 'json'
2
+
3
+ module VpsbClient
4
+ module Api
5
+ class Response
6
+ attr_reader :code, :body_str, :content_type
7
+
8
+ class NotAuthenticated < StandardError; end
9
+ class HttpError < StandardError; end
10
+
11
+ def initialize(curl_response)
12
+ @code = curl_response.response_code
13
+ raise NotAuthenticated, "code=#{@code}" if @code == 401
14
+ raise HttpError, "code=#{@code}" unless success?
15
+
16
+ @body_str = curl_response.body_str
17
+ @content_type = curl_response.content_type
18
+ end
19
+
20
+ def parsed_response
21
+ @parsed_response ||= JSON.parse(@body_str)
22
+ end
23
+
24
+ private
25
+ def success?
26
+ [200, 201, 204, 302].include?(@code)
27
+ end
28
+ end
29
+ end
30
+ end