kontena-websocket-client 0.1.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9cfc6e97aaa820488a6553d596598c422d977cb6
4
- data.tar.gz: ab284b440b2ff882dd2c10b39c911f09af2c1c87
3
+ metadata.gz: bfd6cb9f7d3d9f6215854d22474b19f7ed67bb51
4
+ data.tar.gz: 2c3f654081f2e69fb8c783916411fda8a99dbf00
5
5
  SHA512:
6
- metadata.gz: f06b75cf90c0602bf0529f81ad3dcd1bb3fecf81e0d23f5618545591217dc478f843930aba2ea066ae362950bda61ad62c6181310edc9de531c6bfa2b2f57b70
7
- data.tar.gz: eee54689d732efa7dae2d6217a878bb01c13dfc7dabe056fa1917fb36993bcb56881225ba572cdb8a44db6ae907affc3e79f73a6c50257eb596845a0a4ca9cf5
6
+ metadata.gz: 6df6327057cb56e983332987602d3ac8784850e0e8b179a4de2ee08c9cf453a34e53714bd11c6d1876769087ef7a68470bde845e6526587a20560c09991f938e
7
+ data.tar.gz: 0a12250d84f4d9cd0993257e01b8fea0decd70c020a4b9dfaf32ef5b9a58f0d35320b94d66d2975a5d13bfab28e89b2754b0ef398af75a86e2e3c3856d29d76b
data/README.md CHANGED
@@ -1,4 +1,6 @@
1
1
  [![Build Status](https://travis-ci.org/kontena/kontena-websocket-client.svg?branch=master)](https://travis-ci.org/kontena/kontena-websocket-client)
2
+ [![Gem Version](https://badge.fury.io/rb/kontena-websocket-client.svg)](https://badge.fury.io/rb/kontena-websocket-client)
3
+ [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/kontena/kontena-websocket-client/master)
2
4
 
3
5
  # Kontena::Websocket::Client
4
6
 
@@ -0,0 +1 @@
1
+ websocket-echo-server
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ Thread.abort_on_exception = true
4
+
5
+ require 'kontena-websocket-client'
6
+ require_relative './benchmark'
7
+
8
+ WEBSOCKET_OPTIONS = {
9
+ connect_timeout: 1.0,
10
+ open_timeout: 1.0,
11
+ ping_timeout: 1.0,
12
+ ping_interval: nil,
13
+ write_timeout: 5.0,
14
+ }
15
+
16
+ Kontena::Websocket::Logging.initialize_logger(STDERR, LOG_LEVEL)
17
+
18
+ run_benchmark do |url, **options|
19
+ send_thread = nil
20
+ reader = BenchmarkReader.new
21
+
22
+ Kontena::Websocket::Client.connect(url, **WEBSOCKET_OPTIONS) do |client|
23
+ $logger.info "connect: #{client}"
24
+
25
+ send_thread = Thread.new {
26
+ send_stats = benchmark_sender(**options) do |msg, seq|
27
+ client.send(msg)
28
+ end
29
+
30
+ client.close()
31
+
32
+ send_stats
33
+ }
34
+
35
+ reader.start()
36
+ client.read do |message|
37
+ reader.on_message(Time.now, message)
38
+ end
39
+ end
40
+
41
+ read_stats = reader.stop()
42
+ send_stats = send_thread.value
43
+
44
+ next send_stats, read_stats
45
+ end
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ Thread.abort_on_exception = true
4
+
5
+ require 'faye/websocket'
6
+ require 'eventmachine'
7
+ require_relative './benchmark'
8
+
9
+ run_benchmark do |url, **options|
10
+ send_thread = nil
11
+ reader = BenchmarkReader.new
12
+
13
+ EM.run {
14
+ ws = Faye::WebSocket::Client.new(url)
15
+
16
+ ws.on :open do |event|
17
+ $logger.info "open"
18
+
19
+ send_thread = Thread.new {
20
+ send_stats = benchmark_sender(**options) do |msg, seq|
21
+ EM.next_tick {
22
+ $logger.debug "send seq=%d" % [seq]
23
+ ws.send(msg)
24
+ }
25
+ end
26
+
27
+ EM.next_tick {
28
+ $logger.info "close..."
29
+ ws.close()
30
+ }
31
+
32
+ send_stats
33
+ }
34
+
35
+ reader.start()
36
+ end
37
+
38
+ read = 0
39
+
40
+ ws.on :message do |event|
41
+ seq, rtt = reader.on_message(Time.now, event.data)
42
+
43
+ $logger.debug "read %d: seq=%d rtt=%.2f" % [read, seq, rtt]
44
+
45
+ read += 1
46
+ end
47
+
48
+ ws.on :close do |event|
49
+ $logger.info "closed"
50
+
51
+ reader.stop
52
+ EM.stop
53
+ end
54
+
55
+ ws.on :error do |event|
56
+ $logger.warn "error: #{event}"
57
+
58
+ exit 1
59
+ end
60
+ }
61
+
62
+ read_stats = reader.stop()
63
+ send_stats = send_thread.value
64
+
65
+ next send_stats, read_stats
66
+ end
@@ -0,0 +1,161 @@
1
+ require 'logger'
2
+
3
+ LOG_LEVEL = ENV['LOG_LEVEL'] || Logger::WARN
4
+
5
+ $logger = Logger.new(STDERR)
6
+ $logger.level = LOG_LEVEL
7
+ $logger.progname = 'websocket-benchmark'
8
+
9
+ # @yield at given target interval
10
+ # @return after duration
11
+ def with_rate(rate, duration, &block)
12
+ t0 = Time.now
13
+ interval = 1.0 / rate
14
+ count = 0
15
+ count_miss = 0
16
+ total_yield = 0.0
17
+
18
+ while (t = Time.now) < t0 + duration
19
+ yield t
20
+
21
+ t_yield = Time.now
22
+
23
+ count += 1
24
+ total_yield += (t_yield - t)
25
+
26
+ t_next = t0 + count * interval
27
+
28
+ if t_next > t_yield
29
+ sleep t_next - t_yield
30
+ else
31
+ count_miss += 1
32
+ end
33
+ end
34
+
35
+ t_total = t - t0
36
+
37
+ return {
38
+ time: t_total,
39
+ count: count,
40
+ rate: count / t_total,
41
+ util: total_yield / t_total,
42
+ miss: (count_miss / count),
43
+ }
44
+ end
45
+
46
+ # @yield [seq, message]
47
+ # @return [Hash] after duration
48
+ def benchmark_sender(rate: 1000, duration: 5.0, message_size: 1000)
49
+ total_size = 0
50
+ seq = 0
51
+
52
+ padding = 'X'*(message_size - 16 - 16)
53
+
54
+ stats = with_rate(rate, duration) do |t|
55
+ message = '%15.6f %15d %s' % [t.to_f, seq, padding]
56
+
57
+ yield message, seq
58
+
59
+ total_size += message.length
60
+ seq += 1
61
+ end
62
+
63
+ return stats.merge(
64
+ bytes: total_size,
65
+ )
66
+ end
67
+
68
+ class BenchmarkReader
69
+ def initialize
70
+ @count = 0
71
+ @bytes = 0
72
+ @latency_total = 0.0
73
+ end
74
+
75
+ def start
76
+ @t_start = Time.now
77
+ end
78
+
79
+ # @param time [Time] Time.now
80
+ # @param message [String]
81
+ # @return [Integer, Float] seq, rtt
82
+ def on_message(time, message)
83
+ msg_time_s, msg_seq_s, padding = message.split(' ', 3)
84
+ msg_t = msg_time_s.to_f
85
+ msg_seq = msg_seq_s.to_i
86
+ t = time.to_f
87
+
88
+ @count += 1
89
+ @bytes += message.length
90
+ @latency_total += (t - msg_t)
91
+
92
+ return msg_seq, t - msg_t
93
+ end
94
+
95
+ # @return [Hash]
96
+ def stop
97
+ @t_stop = Time.now
98
+ seconds = @t_stop - @t_start
99
+
100
+ return {
101
+ time: seconds,
102
+ count: @count,
103
+ rate: @count / seconds,
104
+ bytes: @bytes,
105
+ bytes_rate: @bytes / seconds,
106
+ latency_avg: @latency_total / @count,
107
+ }
108
+ end
109
+ end
110
+
111
+ URL = 'ws://localhost:8080/echo'
112
+ RATES = [1, 10, 100, 1000, 3000, 5000, 10000]
113
+ DURATION = 5.0
114
+ MESSAGE_SIZE = 1000
115
+
116
+ HEADER = '%5s %6s/s %9s: send @ %9s/s (%6s%% %6s%%) read @ %9s/s (%6s%%) = %12s/s ~%9s'
117
+ FORMAT = '%5.2fs %6d/s %9d: send @ %9.2f/s (%6.2f%% %6.2f%%) read @ %9.2f/s (%6.2f%%) = %12s/s ~%9.6fs'
118
+
119
+ def to_si(val)
120
+ if val > 10**9
121
+ '%.3fG' % [val / 10**9]
122
+ elsif val > 10**6
123
+ '%.3fM' % [val / 10**6]
124
+ elsif val > 10**3
125
+ '%.3fK' % [val / 10**3]
126
+ else
127
+ '%.3f ' % [val]
128
+ end
129
+ end
130
+
131
+ # @yield [url, **options]
132
+ # @yieldreturn [send_stats, read_stats]
133
+ def run_benchmark()
134
+ url = ENV['URL'] || URL
135
+
136
+ rates = (ENV['RATES']&.split&.map{|r| Integer(r)} || RATES)
137
+ duration = (ENV['DURATION'] || DURATION).to_f
138
+ message_size = (ENV['MESSAGE_SIZE'] || MESSAGE_SIZE).to_i
139
+
140
+ puts HEADER % ['TIME ', 'RATE', 'COUNT', 'MESSAGES', 'UTIL', 'MISS', 'MESSAGES', 'DROP', 'BYTES', 'LATENCY']
141
+
142
+ for rate in rates
143
+ options = {
144
+ rate: rate,
145
+ duration: duration,
146
+ message_size: message_size,
147
+ }
148
+
149
+ $logger.info "benchmark: #{url} #{options}"
150
+
151
+ send_stats, read_stats = yield(url, **options)
152
+
153
+ drop_ratio = 1.0 - read_stats[:count].to_f / send_stats[:count].to_f
154
+
155
+ puts FORMAT % [
156
+ duration, rate, send_stats[:count],
157
+ send_stats[:rate], send_stats[:util] * 100.0, send_stats[:miss] * 100.0,
158
+ read_stats[:rate], drop_ratio * 100.0, to_si(read_stats[:bytes_rate]), read_stats[:latency_avg],
159
+ ]
160
+ end
161
+ end
@@ -0,0 +1,17 @@
1
+ #!/bin/sh
2
+
3
+ set -uex
4
+
5
+ DIR=$(dirname $0)
6
+ SERVER_ARGS=${SERVER_ARGS:- -drop}
7
+ BENCHMARK=${BENCHMARK:-benchmark-client.rb}
8
+
9
+ go get -d github.com/gorilla/websocket
10
+ go build -o $DIR/websocket-echo-server $DIR/websocket-echo-server.go
11
+
12
+ killall websocket-echo-server || true
13
+ $DIR/websocket-echo-server -quiet $SERVER_ARGS &
14
+
15
+ $DIR/${BENCHMARK}
16
+
17
+ trap "kill 0" INT EXIT
@@ -0,0 +1,207 @@
1
+ package main
2
+
3
+ import (
4
+ "flag"
5
+ "fmt"
6
+ "github.com/gorilla/websocket"
7
+ "log"
8
+ "net/http"
9
+ "time"
10
+ )
11
+
12
+ var websocketUpgrader = websocket.Upgrader{}
13
+
14
+ // single-goroutine read+write loop
15
+ // blocks reads if writes block
16
+ func websocketEchoSync(conn *websocket.Conn) error {
17
+ for {
18
+ if messageType, data, err := conn.ReadMessage(); err != nil {
19
+ if websocket.IsCloseError(err, 1000) {
20
+ break
21
+ } else {
22
+ return fmt.Errorf("websocket read: %v", err)
23
+ }
24
+ } else if err := conn.WriteMessage(messageType, data); err != nil {
25
+ return fmt.Errorf("websocket write: %v", err)
26
+ } else {
27
+ if options.Verbose {
28
+ log.Printf("websocket echo: %v", data)
29
+ }
30
+ }
31
+ }
32
+
33
+ return nil
34
+ }
35
+
36
+ type websocketMessage struct {
37
+ Type int
38
+ Data []byte
39
+ }
40
+
41
+ func websocketAsyncReader(conn *websocket.Conn, c chan websocketMessage) error {
42
+ defer close(c)
43
+
44
+ var messages, dropped int
45
+ var start = time.Now()
46
+
47
+ for {
48
+ if messageType, data, err := conn.ReadMessage(); err != nil {
49
+ if websocket.IsCloseError(err, 1000) {
50
+ if options.Verbose {
51
+ log.Printf("websocket read close: %v", err)
52
+ }
53
+
54
+ break
55
+ } else {
56
+ return fmt.Errorf("websocket read: %v", err)
57
+ }
58
+ } else {
59
+ var m = websocketMessage{messageType, data}
60
+ messages += 1
61
+
62
+ select {
63
+ case c <- m:
64
+ if options.Verbose {
65
+ log.Printf("websocket read: %v", m.Data)
66
+ }
67
+
68
+ default:
69
+ dropped += 1
70
+
71
+ if options.Verbose {
72
+ log.Printf("websocket drop: %v", m.Data)
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ var end = time.Now()
79
+
80
+ if !options.Quiet {
81
+ var seconds = end.Sub(start).Seconds()
82
+
83
+ log.Printf("websocket read: %d messages in %.1fs (%.2f/s, dropped %.2f%%)",
84
+ messages, seconds,
85
+ float64(messages)/seconds,
86
+ float64(dropped)/float64(messages)*100.0,
87
+ )
88
+ }
89
+
90
+ return nil
91
+ }
92
+
93
+ func websocketAsyncWriter(conn *websocket.Conn, c <-chan websocketMessage) error {
94
+ for m := range c {
95
+ if err := conn.WriteMessage(m.Type, m.Data); err != nil {
96
+ return fmt.Errorf("websocket write: %v", err)
97
+ } else {
98
+ if options.Verbose {
99
+ log.Printf("websocket write: %v", m.Data)
100
+ }
101
+ }
102
+ }
103
+
104
+ return nil
105
+ }
106
+
107
+ func websocketEchoAsync(conn *websocket.Conn) error {
108
+ var messageChan = make(chan websocketMessage, options.DropBuffer)
109
+ var readClose struct {
110
+ code int
111
+ text string
112
+ }
113
+ var readError error
114
+
115
+ // custom close handler to send close frame after reader drains the message queue
116
+ conn.SetCloseHandler(func(code int, text string) error {
117
+ readClose.code = code
118
+ readClose.text = text
119
+
120
+ return nil
121
+ })
122
+
123
+ go func() {
124
+ if err := websocketAsyncReader(conn, messageChan); err != nil {
125
+ readError = err
126
+ log.Printf("websocket read error: %v", err)
127
+ }
128
+ }()
129
+
130
+ if err := websocketAsyncWriter(conn, messageChan); err != nil {
131
+ return err
132
+ } else if readError != nil {
133
+ return readError
134
+ } else {
135
+ var closeMessage = websocket.FormatCloseMessage(readClose.code, readClose.text)
136
+
137
+ if err := conn.WriteControl(websocket.CloseMessage, closeMessage, time.Time{}); err != nil {
138
+ log.Printf("websocket write close: %v", err)
139
+ } else {
140
+ if options.Verbose {
141
+ log.Printf("websocket write close")
142
+ }
143
+ }
144
+
145
+ return nil
146
+ }
147
+ }
148
+
149
+ func EchoHandler(w http.ResponseWriter, r *http.Request) {
150
+ if websocketConn, err := websocketUpgrader.Upgrade(w, r, nil); err != nil {
151
+ log.Printf("Websocket Upgrade error: %v", err)
152
+ w.WriteHeader(500)
153
+ fmt.Fprintf(w, "%v", err)
154
+ } else {
155
+ if !options.Quiet {
156
+ log.Printf("Websocket echo connect: %v", r.RemoteAddr)
157
+ }
158
+
159
+ defer websocketConn.Close()
160
+
161
+ if options.Drop {
162
+ if err := websocketEchoAsync(websocketConn); err != nil {
163
+ log.Printf("Websocket echo error: %v", err)
164
+ return
165
+ }
166
+ } else {
167
+ if err := websocketEchoSync(websocketConn); err != nil {
168
+ log.Printf("Websocket echo error: %v", err)
169
+ return
170
+ }
171
+ }
172
+
173
+ if !options.Quiet {
174
+ log.Printf("Websocket echo close: %v", r.RemoteAddr)
175
+ }
176
+ }
177
+ }
178
+
179
+ var options struct {
180
+ Listen string
181
+ Verbose bool
182
+ Quiet bool
183
+ Drop bool
184
+ DropBuffer uint
185
+ }
186
+
187
+ func init() {
188
+ flag.StringVar(&options.Listen, "listen", "localhost:8080", "HOST:PORT")
189
+ flag.BoolVar(&options.Verbose, "verbose", false, "log echo messages")
190
+ flag.BoolVar(&options.Quiet, "quiet", false, "do not log connects")
191
+ flag.BoolVar(&options.Drop, "drop", false, "drop messages if client is sending faster than reading")
192
+ flag.UintVar(&options.DropBuffer, "drop-buffer", 1000, "message buffer length")
193
+ }
194
+
195
+ func main() {
196
+ flag.Parse()
197
+
198
+ if !options.Quiet {
199
+ log.Printf("Websocket listen: %v", options.Listen)
200
+ }
201
+
202
+ http.HandleFunc("/echo", EchoHandler)
203
+
204
+ if err := http.ListenAndServe(options.Listen, nil); err != nil {
205
+ log.Fatalf("http listen: %v", err)
206
+ }
207
+ }
@@ -1,7 +1,7 @@
1
1
  # coding: utf-8
2
2
  Gem::Specification.new do |spec|
3
3
  spec.name = "kontena-websocket-client"
4
- spec.version = '0.1.0'
4
+ spec.version = '0.1.1'
5
5
  spec.authors = ["Kontena, Inc"]
6
6
  spec.email = ["info@kontena.io"]
7
7
 
@@ -348,7 +348,7 @@ class Kontena::Websocket::Client
348
348
  debug "ping-pong at #{ping_at} in #{ping_delay}s"
349
349
 
350
350
  # XXX: defer call without mutex?
351
- @on_pong.call(ping_delay) # TODO: also pass ping_at
351
+ @on_pong.call(ping_delay) if @on_pong # TODO: also pass ping_at
352
352
  end
353
353
  end
354
354
  end
@@ -1,5 +1,5 @@
1
1
  class Kontena::Websocket::Client
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
 
4
4
  # Running ruby >= version?
5
5
  # @param gte_version [String]
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kontena-websocket-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kontena, Inc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-07-21 00:00:00.000000000 Z
11
+ date: 2017-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: websocket-driver
@@ -80,6 +80,12 @@ files:
80
80
  - LICENSE
81
81
  - README.md
82
82
  - Rakefile
83
+ - benchmark/.gitignore
84
+ - benchmark/benchmark-client.rb
85
+ - benchmark/benchmark-em.rb
86
+ - benchmark/benchmark.rb
87
+ - benchmark/benchmark.sh
88
+ - benchmark/websocket-echo-server.go
83
89
  - examples/websocket-echo-client.rb
84
90
  - kontena-websocket-client.gemspec
85
91
  - lib/kontena-websocket-client.rb