Skip to content

Commit eeb85b8

Browse files
authored
chore(kbagent): keep probe and task events within the Event message limit (#10511)
1 parent dcf5184 commit eeb85b8

5 files changed

Lines changed: 191 additions & 4 deletions

File tree

pkg/kbagent/service/event_util.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
Copyright (C) 2022-2026 ApeCloud Co., Ltd
3+
4+
This file is part of KubeBlocks project
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU Affero General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU Affero General Public License for more details.
15+
16+
You should have received a copy of the GNU Affero General Public License
17+
along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
package service
21+
22+
import (
23+
"encoding/json"
24+
)
25+
26+
// maxEventMessageLength is the Kubernetes limit for the Event message field;
27+
// creating an Event with a longer message is rejected by the apiserver.
28+
const maxEventMessageLength = 1024
29+
30+
// marshalEventWithSizeLimit marshals an event and, when the encoded form
31+
// exceeds the Event message limit, shrinks the event's free-text fields —
32+
// message first (keeping its head, where the root error lives), then output —
33+
// so that the payload stays valid JSON that event consumers can unmarshal.
34+
// The message and output pointers must reference fields of the event being
35+
// marshaled.
36+
func marshalEventWithSizeLimit(event any, message *string, output *[]byte) ([]byte, error) {
37+
const marker = "...(truncated)"
38+
for {
39+
msg, err := json.Marshal(event)
40+
if err != nil {
41+
return nil, err
42+
}
43+
if len(msg) <= maxEventMessageLength {
44+
return msg, nil
45+
}
46+
overflow := len(msg) - maxEventMessageLength
47+
switch {
48+
case len(*message) > len(marker):
49+
cut := min(overflow+len(marker), len(*message))
50+
*message = (*message)[:len(*message)-cut] + marker
51+
case len(*output) > 0:
52+
*output = (*output)[:len(*output)-min(overflow, len(*output))]
53+
default:
54+
// nothing left to shrink; send as-is and let the apiserver decide
55+
return msg, nil
56+
}
57+
}
58+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
Copyright (C) 2022-2026 ApeCloud Co., Ltd
3+
4+
This file is part of KubeBlocks project
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU Affero General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU Affero General Public License for more details.
15+
16+
You should have received a copy of the GNU Affero General Public License
17+
along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
package service
21+
22+
import (
23+
"encoding/json"
24+
"strings"
25+
26+
. "github.com/onsi/ginkgo/v2"
27+
. "github.com/onsi/gomega"
28+
29+
"github.com/apecloud/kubeblocks/pkg/kbagent/proto"
30+
)
31+
32+
var _ = Describe("marshal task event with size limit", func() {
33+
It("keeps small task events unchanged", func() {
34+
event := proto.TaskEvent{
35+
Instance: "comp",
36+
Task: "newReplica",
37+
Replica: "pod-1",
38+
Message: "ok",
39+
}
40+
msg, err := marshalEventWithSizeLimit(&event, &event.Message, &event.Output)
41+
Expect(err).Should(BeNil())
42+
plain, err := json.Marshal(&event)
43+
Expect(err).Should(BeNil())
44+
Expect(msg).Should(Equal(plain))
45+
})
46+
47+
It("truncates a long task failure message but keeps its head and valid JSON", func() {
48+
event := proto.TaskEvent{
49+
Instance: "comp",
50+
Task: "newReplica",
51+
Replica: "pod-1",
52+
Code: -1,
53+
Message: "replication setup failed: connection refused\n" + strings.Repeat("verbose replication log line\n", 200),
54+
}
55+
msg, err := marshalEventWithSizeLimit(&event, &event.Message, &event.Output)
56+
Expect(err).Should(BeNil())
57+
Expect(len(msg)).Should(BeNumerically("<=", maxEventMessageLength))
58+
59+
var decoded proto.TaskEvent
60+
Expect(json.Unmarshal(msg, &decoded)).Should(Succeed())
61+
Expect(decoded.Code).Should(Equal(int32(-1)))
62+
Expect(decoded.Message).Should(HavePrefix("replication setup failed"))
63+
Expect(decoded.Message).Should(HaveSuffix("...(truncated)"))
64+
})
65+
66+
It("shrinks oversized task output while keeping valid JSON", func() {
67+
event := proto.TaskEvent{
68+
Instance: "comp",
69+
Task: "newReplica",
70+
Output: []byte(strings.Repeat("y", 8192)),
71+
}
72+
msg, err := marshalEventWithSizeLimit(&event, &event.Message, &event.Output)
73+
Expect(err).Should(BeNil())
74+
Expect(len(msg)).Should(BeNumerically("<=", maxEventMessageLength))
75+
76+
var decoded proto.TaskEvent
77+
Expect(json.Unmarshal(msg, &decoded)).Should(Succeed())
78+
Expect(decoded.Task).Should(Equal("newReplica"))
79+
})
80+
})

pkg/kbagent/service/probe.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ package service
2121

2222
import (
2323
"context"
24-
"encoding/json"
2524
"fmt"
2625
"net"
2726
"os"
@@ -405,7 +404,7 @@ func (r *probeRunner) launchReportLoop(probe *proto.Probe) {
405404
return true
406405
}
407406

408-
msg, err := json.Marshal(event)
407+
msg, err := marshalEventWithSizeLimit(&event, &event.Message, &event.Output)
409408
if err != nil {
410409
log(err, "failed to marshal the probe event", retry, periodically)
411410
return true

pkg/kbagent/service/probe_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"fmt"
2525
"os"
2626
"path/filepath"
27+
"strings"
2728
"time"
2829

2930
"github.com/fsnotify/fsnotify"
@@ -285,4 +286,54 @@ var _ = Describe("probe", func() {
285286

286287
// TODO: more test cases
287288
})
289+
290+
Context("marshal event with size limit", func() {
291+
It("keeps small events unchanged", func() {
292+
event := proto.ProbeEvent{
293+
Probe: "roleProbe",
294+
Code: 0,
295+
Output: []byte("leader"),
296+
Message: "ok",
297+
}
298+
msg, err := marshalEventWithSizeLimit(&event, &event.Message, &event.Output)
299+
Expect(err).Should(BeNil())
300+
plain, err := json.Marshal(event)
301+
Expect(err).Should(BeNil())
302+
Expect(msg).Should(Equal(plain))
303+
})
304+
305+
It("truncates a long message but keeps its head and valid JSON", func() {
306+
longStderr := "DB manager initialize failed: fatal error config file: open /tools/config/dbctl/components/: no such file or directory\n" +
307+
strings.Repeat("usage: dbctl [command] [flags] help text filler\n", 200)
308+
event := proto.ProbeEvent{
309+
Probe: "roleProbe",
310+
Code: 255,
311+
Message: longStderr,
312+
}
313+
msg, err := marshalEventWithSizeLimit(&event, &event.Message, &event.Output)
314+
Expect(err).Should(BeNil())
315+
Expect(len(msg)).Should(BeNumerically("<=", maxEventMessageLength))
316+
317+
var decoded proto.ProbeEvent
318+
Expect(json.Unmarshal(msg, &decoded)).Should(Succeed())
319+
Expect(decoded.Code).Should(Equal(int32(255)))
320+
Expect(decoded.Message).Should(HavePrefix("DB manager initialize failed"))
321+
Expect(decoded.Message).Should(HaveSuffix("...(truncated)"))
322+
})
323+
324+
It("shrinks the output when the message alone cannot absorb the overflow", func() {
325+
event := proto.ProbeEvent{
326+
Probe: "roleProbe",
327+
Code: 255,
328+
Output: []byte(strings.Repeat("x", 8192)),
329+
}
330+
msg, err := marshalEventWithSizeLimit(&event, &event.Message, &event.Output)
331+
Expect(err).Should(BeNil())
332+
Expect(len(msg)).Should(BeNumerically("<=", maxEventMessageLength))
333+
334+
var decoded proto.ProbeEvent
335+
Expect(json.Unmarshal(msg, &decoded)).Should(Succeed())
336+
Expect(decoded.Probe).Should(Equal("roleProbe"))
337+
})
338+
})
288339
})

pkg/kbagent/service/task.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ package service
2121

2222
import (
2323
"context"
24-
"encoding/json"
2524
"fmt"
2625
"reflect"
2726
"slices"
@@ -154,7 +153,7 @@ func (s *taskService) wait(ch chan error) error {
154153
}
155154

156155
func (s *taskService) notify(task proto.Task, event proto.TaskEvent, sync bool) error {
157-
msg, err := json.Marshal(&event)
156+
msg, err := marshalEventWithSizeLimit(&event, &event.Message, &event.Output)
158157
if err == nil {
159158
return util.SendEventWithMessage(&s.logger, "task", string(msg), sync)
160159
} else {

0 commit comments

Comments
 (0)