Skip to content

Commit 271633e

Browse files
Add support for bytes limit (#29)
* Add support for byte limits. The "bytes" querystring parameter allows the client to specify the maximum number of bytes the server will send/receive before terminating the connection. * s/byte/bytes/g * Comments * Apply limits over sent bytes Co-Authored-By: Cristina Leon <cristinaleon@google.com>
1 parent 3efa63b commit 271633e

File tree

8 files changed

+125
-17
lines changed

8 files changed

+125
-17
lines changed

cmd/msak-client/client.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ const clientName = "msak-client-go"
1515
var clientVersion = version.Version
1616

1717
var (
18-
flagServer = flag.String("server", "", "Server address")
19-
flagStreams = flag.Int("streams", client.DefaultStreams, "Number of streams")
20-
flagCC = flag.String("cc", "bbr", "Congestion control algorithm to use")
21-
flagDelay = flag.Duration("delay", 0, "Delay between each stream")
22-
flagDuration = flag.Duration("duration", client.DefaultLength, "Length of the last stream")
23-
flagScheme = flag.String("scheme", client.DefaultScheme, "Websocket scheme (wss or ws)")
24-
flagMID = flag.String("mid", uuid.NewString(), "Measurement ID to use")
25-
flagNoVerify = flag.Bool("no-verify", false, "Skip TLS certificate verification")
26-
flagDebug = flag.Bool("debug", false, "Enable debug logging")
18+
flagServer = flag.String("server", "", "Server address")
19+
flagStreams = flag.Int("streams", client.DefaultStreams, "Number of streams")
20+
flagCC = flag.String("cc", "bbr", "Congestion control algorithm to use")
21+
flagDelay = flag.Duration("delay", 0, "Delay between each stream")
22+
flagDuration = flag.Duration("duration", client.DefaultLength, "Length of the last stream")
23+
flagScheme = flag.String("scheme", client.DefaultScheme, "Websocket scheme (wss or ws)")
24+
flagMID = flag.String("mid", uuid.NewString(), "Measurement ID to use")
25+
flagNoVerify = flag.Bool("no-verify", false, "Skip TLS certificate verification")
26+
flagDebug = flag.Bool("debug", false, "Enable debug logging")
27+
flagByteLimit = flag.Int("bytes", 0, "Byte limit to request to the server")
2728
)
2829

2930
func main() {
@@ -46,7 +47,8 @@ func main() {
4647
Emitter: client.HumanReadable{
4748
Debug: *flagDebug,
4849
},
49-
NoVerify: *flagNoVerify,
50+
NoVerify: *flagNoVerify,
51+
ByteLimit: *flagByteLimit,
5052
}
5153

5254
cl := client.New(clientName, clientVersion, config)

internal/handler/handler.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/m-lab/msak/internal/persistence"
1717
"github.com/m-lab/msak/pkg/throughput1"
1818
"github.com/m-lab/msak/pkg/throughput1/model"
19+
"github.com/m-lab/msak/pkg/throughput1/spec"
1920
"github.com/m-lab/msak/pkg/version"
2021
"github.com/prometheus/client_golang/prometheus"
2122
"github.com/prometheus/client_golang/prometheus/promauto"
@@ -130,6 +131,20 @@ func (h *Handler) upgradeAndRunMeasurement(kind model.TestDirection, rw http.Res
130131
model.NameValue{Name: "delay", Value: requestDelay})
131132
}
132133

134+
requestByteLimit := query.Get(spec.ByteLimitParameterName)
135+
var byteLimit int
136+
if requestByteLimit != "" {
137+
if byteLimit, err = strconv.Atoi(requestByteLimit); err != nil {
138+
ClientConnections.WithLabelValues(string(kind), "invalid-byte-limit").Inc()
139+
log.Info("Received request with an invalid byte limit", "source", req.RemoteAddr,
140+
"value", requestByteLimit)
141+
writeBadRequest(rw)
142+
return
143+
}
144+
clientOptions = append(clientOptions,
145+
model.NameValue{Name: spec.ByteLimitParameterName, Value: requestByteLimit})
146+
}
147+
133148
// Read metadata (i.e. everything in the querystring that's not a known
134149
// option).
135150
metadata, err := getRequestMetadata(req)
@@ -198,6 +213,7 @@ func (h *Handler) upgradeAndRunMeasurement(kind model.TestDirection, rw http.Res
198213
defer cancel()
199214

200215
proto := throughput1.New(wsConn)
216+
proto.SetByteLimit(byteLimit)
201217
var senderCh, receiverCh <-chan model.WireMeasurement
202218
var errCh <-chan error
203219
if kind == model.DirectionDownload {

internal/handler/handler_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ func TestHandler_Validation(t *testing.T) {
215215
target: "/?mid=test&streams=2&duration=invalid",
216216
statusCode: http.StatusBadRequest,
217217
},
218+
{
219+
name: "invalid byte limit",
220+
target: "/?mid=test&streams=2&duration=1000&bytes=invalid",
221+
statusCode: http.StatusBadRequest,
222+
},
218223
{
219224
name: "metadata key too long",
220225
target: "/?mid=test&streams=2&" + longKey,

pkg/client/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ func (c *Throughput1Client) connect(ctx context.Context, serviceURL *url.URL) (*
136136
q := serviceURL.Query()
137137
q.Set("streams", fmt.Sprint(c.config.NumStreams))
138138
q.Set("cc", c.config.CongestionControl)
139+
q.Set(spec.ByteLimitParameterName, fmt.Sprint(c.config.ByteLimit))
139140
q.Set("duration", fmt.Sprintf("%d", c.config.Length.Milliseconds()))
140141
q.Set("client_arch", runtime.GOARCH)
141142
q.Set("client_library_name", libraryName)

pkg/client/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,8 @@ type Config struct {
3535

3636
// NoVerify disables the TLS certificate verification.
3737
NoVerify bool
38+
39+
// ByteLimit is the maximum number of bytes to download or upload. If set to 0, the
40+
// limit is disabled.
41+
ByteLimit int
3842
}

pkg/throughput1/protocol.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ type Protocol struct {
4747

4848
applicationBytesReceived atomic.Int64
4949
applicationBytesSent atomic.Int64
50+
51+
byteLimit int
5052
}
5153

5254
// New returns a new Protocol with the specified connection and every other
@@ -61,6 +63,12 @@ func New(conn *websocket.Conn) *Protocol {
6163
}
6264
}
6365

66+
// SetByteLimit sets the number of bytes sent after which a test (either download or upload) will stop.
67+
// Set the value to zero to disable the byte limit.
68+
func (p *Protocol) SetByteLimit(value int) {
69+
p.byteLimit = value
70+
}
71+
6472
// Upgrade takes a HTTP request and upgrades the connection to WebSocket.
6573
// Returns a websocket Conn if the upgrade succeeded, and an error otherwise.
6674
func Upgrade(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
@@ -180,6 +188,7 @@ func (p *Protocol) receiver(ctx context.Context,
180188
func (p *Protocol) sendCounterflow(ctx context.Context,
181189
measurerCh <-chan model.Measurement, results chan<- model.WireMeasurement,
182190
errCh chan<- error) {
191+
byteLimit := int64(p.byteLimit)
183192
for {
184193
select {
185194
case <-ctx.Done():
@@ -218,13 +227,19 @@ func (p *Protocol) sendCounterflow(ctx context.Context,
218227
case results <- wm:
219228
default:
220229
}
230+
231+
// End the test once enough bytes have been received.
232+
if byteLimit > 0 && m.TCPInfo != nil && m.TCPInfo.BytesReceived >= byteLimit {
233+
p.close(ctx)
234+
return
235+
}
221236
}
222237
}
223238
}
224239

225240
func (p *Protocol) sender(ctx context.Context, measurerCh <-chan model.Measurement,
226241
results chan<- model.WireMeasurement, errCh chan<- error) {
227-
size := spec.MinMessageSize
242+
size := p.ScaleMessage(spec.MinMessageSize, 0)
228243
message, err := p.makePreparedMessage(size)
229244
if err != nil {
230245
log.Printf("makePreparedMessage failed (ctx: %p)", ctx)
@@ -283,27 +298,39 @@ func (p *Protocol) sender(ctx context.Context, measurerCh <-chan model.Measureme
283298
}
284299
p.applicationBytesSent.Add(int64(size))
285300

286-
// Determine whether it's time to scale the message size.
287-
if size >= spec.MaxScaledMessageSize {
288-
continue
301+
bytesSent := int(p.applicationBytesSent.Load())
302+
if p.byteLimit > 0 && bytesSent >= p.byteLimit {
303+
p.close(ctx)
304+
return
289305
}
290306

291-
if size > int(p.applicationBytesSent.Load())/spec.ScalingFraction {
307+
// Determine whether it's time to scale the message size.
308+
if size >= spec.MaxScaledMessageSize || size > bytesSent/spec.ScalingFraction {
309+
size = p.ScaleMessage(size, bytesSent)
292310
continue
293311
}
294312

295-
size *= 2
313+
size = p.ScaleMessage(size*2, bytesSent)
296314
message, err = p.makePreparedMessage(size)
297315
if err != nil {
298316
log.Printf("failed to make prepared message (ctx: %p, err: %v)", ctx, err)
299317
errCh <- err
300318
return
301319
}
302-
303320
}
304321
}
305322
}
306323

324+
// ScaleMessage sets the binary message size taking into consideration byte limits.
325+
func (p *Protocol) ScaleMessage(msgSize int, bytesSent int) int {
326+
// Check if the next payload size will push the total number of bytes over the limit.
327+
excess := bytesSent + msgSize - p.byteLimit
328+
if p.byteLimit > 0 && excess > 0 {
329+
msgSize -= excess
330+
}
331+
return msgSize
332+
}
333+
307334
func (p *Protocol) close(ctx context.Context) {
308335
msg := websocket.FormatCloseMessage(
309336
websocket.CloseNormalClosure, "Done sending")

pkg/throughput1/protocol_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,51 @@ func TestProtocol_Download(t *testing.T) {
137137
}
138138
}
139139
}
140+
141+
func TestProtocol_ScaleMessage(t *testing.T) {
142+
tests := []struct {
143+
name string
144+
byteLimit int
145+
msgSize int
146+
bytesSent int
147+
want int
148+
}{
149+
{
150+
name: "no-limit",
151+
byteLimit: 0,
152+
msgSize: 10,
153+
bytesSent: 100,
154+
want: 10,
155+
},
156+
{
157+
name: "under-limit",
158+
byteLimit: 200,
159+
msgSize: 10,
160+
bytesSent: 100,
161+
want: 10,
162+
},
163+
{
164+
name: "at-limit",
165+
byteLimit: 110,
166+
msgSize: 10,
167+
bytesSent: 100,
168+
want: 10,
169+
},
170+
{
171+
name: "over-limit",
172+
byteLimit: 110,
173+
msgSize: 20,
174+
bytesSent: 100,
175+
want: 10,
176+
},
177+
}
178+
for _, tt := range tests {
179+
t.Run(tt.name, func(t *testing.T) {
180+
p := &throughput1.Protocol{}
181+
p.SetByteLimit(tt.byteLimit)
182+
if got := p.ScaleMessage(tt.msgSize, tt.bytesSent); got != tt.want {
183+
t.Errorf("Protocol.ScaleMessage() = %v, want %v", got, tt.want)
184+
}
185+
})
186+
}
187+
}

pkg/throughput1/spec/spec.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ const (
3737

3838
// SecWebSocketProtocol is the value of the Sec-WebSocket-Protocol header.
3939
SecWebSocketProtocol = "net.measurementlab.throughput.v1"
40+
41+
// ByteLimitParameterName is the name of the parameter that clients can use
42+
// to terminate throughput1 download tests once the test has transferred
43+
// the specified number of bytes.
44+
ByteLimitParameterName = "bytes"
4045
)
4146

4247
// SubtestKind indicates the subtest kind

0 commit comments

Comments
 (0)