Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions api/types/load_traffic.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,23 +137,25 @@ type RequestWatchList struct {
}

// RequestPut defines PUT request for target resource type.
//
// NOTE: Today only `configmaps` (core/v1) is supported. The PUT builder
// constructs a ConfigMap body inline (`data.payload = <PayloadSize random
// bytes>`) and overwrites an existing object named `<Name>-<rand[0,
// KeySpaceSize)>`. The target objects must be pre-populated (e.g. via
// `runkperf data cm add`); a request against a non-existent object will
// receive a 404 from the apiserver and be counted as a failure.
type RequestPut struct {
// KubeGroupVersionResource identifies the resource URI.
//
// NOTE: Currently, it should be configmap or secrets because we can
// generate random bytes as blob for it. However, for the pod resource,
// we need to ensure a lot of things are ready, for instance, volumes,
// resource capacity. It's not easy to generate it randomly. Maybe we
// can introduce pod template in the future.
KubeGroupVersionResource `yaml:",inline"`
// Namespace is object's namespace.
Namespace string `json:"namespace" yaml:"namespace"`
// Name is object's prefix name.
Name string `json:"name" yaml:"name"`
// KeySpaceSize is used to generate random number as name's suffix.
KeySpaceSize int `json:"keySpaceSize" yaml:"keySpaceSize"`
// ValueSize is the object's size in bytes.
ValueSize int `json:"valueSize" yaml:"valueSize"`
// PayloadSize is the size in bytes of a random string written into the
// ConfigMap body's `data.payload` field.
PayloadSize int `json:"payloadSize" yaml:"payloadSize"`
}

// RequestPatch defines PATCH request for target resource type.
Expand Down Expand Up @@ -191,6 +193,9 @@ type RequestPostDel struct {
KubeGroupVersionResource `yaml:",inline"`
Namespace string `json:"namespace" yaml:"namespace"`
DeleteRatio float64 `json:"deleteRatio" yaml:"deleteRatio"`
// PayloadSize is the size in bytes of a random padding string injected
// into the created object (e.g. as a Pod env var value). 0 means no padding.
PayloadSize int `json:"payloadSize" yaml:"payloadSize"`
}

// Validate verifies fields of LoadProfile.
Expand Down Expand Up @@ -316,15 +321,20 @@ func (r *RequestPut) Validate() error {
return fmt.Errorf("kube metadata: %v", err)
}

// TODO: check resource type
// Only core/v1 ConfigMap is supported today (see RequestPut doc comment).
if (r.Group != "" && r.Group != "core") || r.Version != "v1" || r.Resource != "configmaps" {
return fmt.Errorf("put currently only supports core/v1 configmaps, got group=%q version=%q resource=%q",
r.Group, r.Version, r.Resource)
}

if r.Name == "" {
return fmt.Errorf("name pattern is required")
}
if r.KeySpaceSize <= 0 {
return fmt.Errorf("keySpaceSize must > 0")
}
if r.ValueSize <= 0 {
return fmt.Errorf("valueSize must > 0")
if r.PayloadSize <= 0 {
return fmt.Errorf("payloadSize must > 0")
}
return nil
}
Expand Down Expand Up @@ -405,5 +415,9 @@ func (r *RequestPostDel) Validate() error {
return fmt.Errorf("delete ratio must be between 0 and 0.5: %v, create proportion should be greater than delete", r.DeleteRatio)
}

if r.PayloadSize < 0 {
return fmt.Errorf("payloadSize must >= 0: %v", r.PayloadSize)
}

return nil
}
4 changes: 2 additions & 2 deletions api/types/load_traffic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ specs:
namespace: kperf
name: kperf-
keySpaceSize: 1000
valueSize: 1024
payloadSize: 1024
shares: 1000
- getPodLog:
namespace: default
Expand Down Expand Up @@ -119,7 +119,7 @@ specs:
assert.Equal(t, "kperf", target.Specs[0].Requests[4].Put.Namespace)
assert.Equal(t, "kperf-", target.Specs[0].Requests[4].Put.Name)
assert.Equal(t, 1000, target.Specs[0].Requests[4].Put.KeySpaceSize)
assert.Equal(t, 1024, target.Specs[0].Requests[4].Put.ValueSize)
assert.Equal(t, 1024, target.Specs[0].Requests[4].Put.PayloadSize)

assert.Equal(t, 10, target.Specs[0].Requests[5].Shares)
assert.NotNil(t, target.Specs[0].Requests[5].GetPodLog)
Expand Down
4 changes: 4 additions & 0 deletions contrib/internal/manifests/workload/pods/templates/pod.tpl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{{- $name:= .Values.namePattern }}
{{- $namespace:= .Values.namespace }}
{{- $payload:= .Values.payload }}
apiVersion: v1
kind: Pod
metadata:
Expand All @@ -11,3 +12,6 @@ spec:
containers:
- name: fake-container
image: fake-image
env:
- name: PAYLOAD
value: {{ $payload | quote }}
106 changes: 105 additions & 1 deletion request/random.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package request
import (
"context"
"crypto/rand"
"encoding/json"
"fmt"
"math/big"
"sync"
Expand Down Expand Up @@ -64,8 +65,10 @@ func NewWeightedRandomRequests(spec *types.LoadProfileSpec) (*WeightedRandomRequ
builder = newRequestPatchBuilder(r.Patch, "", spec.MaxRetries)
case r.PostDel != nil:
builder = newRequestPostDelBuilder(r.PostDel, "", spec.MaxRetries)
case r.Put != nil:
builder = newRequestPutBuilder(r.Put, spec.MaxRetries)
default:
return nil, fmt.Errorf("not implement for PUT yet")
return nil, fmt.Errorf("unknown request type: %+v", r)
}
reqBuilders = append(reqBuilders, builder)
}
Expand Down Expand Up @@ -429,6 +432,7 @@ type requestPostDelBuilder struct {
resourceVersion string
namespace string
deleteRatio float64
payloadSize int
maxRetries int

// Per-builder cache for created resources
Expand All @@ -445,6 +449,7 @@ func newRequestPostDelBuilder(src *types.RequestPostDel, resourceVersion string,
resourceVersion: resourceVersion,
namespace: src.Namespace,
deleteRatio: src.DeleteRatio,
payloadSize: src.PayloadSize,
maxRetries: maxRetries,
cache: InitCache(), // Initialize the cache
}
Expand Down Expand Up @@ -498,6 +503,7 @@ func (b *requestPostDelBuilder) Build(cli rest.Interface) Requester {
body, _ := utils.RenderTemplate(b.resource, map[string]interface{}{
"namePattern": name,
"namespace": b.namespace,
"payload": randomPayload(b.payloadSize),
})

return &PostDelDiscardRequester{
Expand Down Expand Up @@ -545,3 +551,101 @@ func (reqr *PostDelDiscardRequester) Do(ctx context.Context) (bytes int64, err e
func toPtr[T any](v T) *T {
return &v
}

// requestPutBuilder builds PUT requests that overwrite an existing ConfigMap
// named `<name>-<rand[0, keySpaceSize)>` with a body whose `data.payload` field
// carries `payloadSize` random bytes. The target ConfigMap must already exist
// (e.g. created via `runkperf data cm add`); otherwise the apiserver returns
// 404 and the request is counted as a failure. Only `configmaps` is supported
// today — see RequestPut.Validate.
type requestPutBuilder struct {
version schema.GroupVersion
resource string
namespace string
name string
keySpaceSize int
payloadSize int
maxRetries int
}

func newRequestPutBuilder(src *types.RequestPut, maxRetries int) *requestPutBuilder {
return &requestPutBuilder{
version: schema.GroupVersion{
Group: src.Group,
Version: src.Version,
},
resource: src.Resource,
namespace: src.Namespace,
name: src.Name,
keySpaceSize: src.KeySpaceSize,
payloadSize: src.PayloadSize,
maxRetries: maxRetries,
}
}

// Build implements RESTRequestBuilder.Build.
func (b *requestPutBuilder) Build(cli rest.Interface) Requester {
comps := []string{"api", b.version.Version, "namespaces", b.namespace, b.resource}

randomInt, _ := rand.Int(rand.Reader, big.NewInt(int64(b.keySpaceSize)))
finalName := fmt.Sprintf("%s-%d", b.name, randomInt.Int64())
comps = append(comps, finalName)

cm := &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "ConfigMap"},
ObjectMeta: metav1.ObjectMeta{
Name: finalName,
Namespace: b.namespace,
},
Data: map[string]string{"payload": randomPayload(b.payloadSize)},
}
body, _ := json.Marshal(cm)

return &DiscardRequester{
BaseRequester: BaseRequester{
method: "PUT",
req: cli.Put().AbsPath(comps...).Body(body).MaxRetries(b.maxRetries),
},
}
}

// randomPayload returns a string of exactly n bytes, uniformly sampled from
// [a-zA-Z0-9] via crypto/rand with rejection sampling to avoid modulo bias.
// Returns "" when n <= 0.
func randomPayload(n int) string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about this?

func randomPayload(n int) string {
        if n <= 0 {
                return ""
        }

        const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

        // Only use byte values in [0, max). Since len(alphabet)=62 and
        // 248 is the largest multiple of 62 below 256, mapping b%62 is uniform
        // for accepted bytes. Bytes 248..255 are skipped to avoid modulo bias.
        const max = byte(256 - 256%len(alphabet))

        out := make([]byte, n)

        bufSize := n
        if bufSize > 4096 {
                bufSize = 4096
        }
        buf := make([]byte, bufSize)

        for i := 0; i < n; {
                if _, err := rand.Read(buf); err != nil {
                        panic(fmt.Errorf("failed to read random bytes: %w", err))
                }

                for _, b := range buf {
                        if b >= max {
                                continue
                        }
                        out[i] = alphabet[int(b)%len(alphabet)]
                        i++
                        if i == n {
                                return string(out)
                        }
                }
        }

        return string(out)
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! Updated in the latest commit, PTAL.

if n <= 0 {
return ""
}
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// Only use byte values in [0, threshold). Since len(alphabet)=62 and
// 248 is the largest multiple of 62 below 256, mapping b%62 is uniform
// for accepted bytes. Bytes 248..255 are skipped to avoid modulo bias.
const threshold = byte(256 - 256%len(alphabet))

out := make([]byte, n)

bufSize := n
if bufSize > 4096 {
bufSize = 4096
}
buf := make([]byte, bufSize)

for i := 0; i < n; {
if _, err := rand.Read(buf); err != nil {
panic(fmt.Errorf("failed to read random bytes: %w", err))
}

for _, b := range buf {
if b >= threshold {
continue
}
out[i] = alphabet[int(b)%len(alphabet)]
i++
if i == n {
return string(out)
}
}
}

return string(out)
}
Loading