forked from github/github-mcp-server
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuffer.go
More file actions
133 lines (119 loc) · 3.46 KB
/
buffer.go
File metadata and controls
133 lines (119 loc) · 3.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
package buffer
import (
"bytes"
"fmt"
"io"
"net/http"
"strings"
)
// maxLineSize is the maximum size for a single log line (10MB).
// GitHub Actions logs can contain extremely long lines (base64 content, minified JS, etc.)
const maxLineSize = 10 * 1024 * 1024
// ProcessResponseAsRingBufferToEnd reads the body of an HTTP response line by line,
// storing only the last maxJobLogLines lines using a ring buffer (sliding window).
// This efficiently retains the most recent lines, overwriting older ones as needed.
//
// Parameters:
//
// httpResp: The HTTP response whose body will be read.
// maxJobLogLines: The maximum number of log lines to retain.
//
// Returns:
//
// string: The concatenated log lines (up to maxJobLogLines), separated by newlines.
// int: The total number of lines read from the response.
// *http.Response: The original HTTP response.
// error: Any error encountered during reading.
//
// The function uses a ring buffer to efficiently store only the last maxJobLogLines lines.
// If the response contains more lines than maxJobLogLines, only the most recent lines are kept.
// Lines exceeding maxLineSize are truncated with a marker.
func ProcessResponseAsRingBufferToEnd(httpResp *http.Response, maxJobLogLines int) (string, int, *http.Response, error) {
if maxJobLogLines <= 0 {
maxJobLogLines = 500
}
if maxJobLogLines > 100000 {
maxJobLogLines = 100000
}
lines := make([]string, maxJobLogLines)
validLines := make([]bool, maxJobLogLines)
totalLines := 0
writeIndex := 0
const readBufferSize = 64 * 1024 // 64KB read buffer
const maxDisplayLength = 1000 // Keep first 1000 chars of truncated lines
readBuf := make([]byte, readBufferSize)
var currentLine strings.Builder
lineTruncated := false
// storeLine saves the current line to the ring buffer and resets state
storeLine := func() {
line := currentLine.String()
if lineTruncated && len(line) > maxDisplayLength {
line = line[:maxDisplayLength]
}
if lineTruncated {
line += "... [TRUNCATED]"
}
lines[writeIndex] = line
validLines[writeIndex] = true
totalLines++
writeIndex = (writeIndex + 1) % maxJobLogLines
currentLine.Reset()
lineTruncated = false
}
// accumulate adds bytes to currentLine up to maxLineSize, sets lineTruncated if exceeded
accumulate := func(data []byte) {
if lineTruncated {
return
}
remaining := maxLineSize - currentLine.Len()
if remaining <= 0 {
lineTruncated = true
return
}
if remaining > len(data) {
remaining = len(data)
}
currentLine.Write(data[:remaining])
if currentLine.Len() >= maxLineSize {
lineTruncated = true
}
}
for {
n, err := httpResp.Body.Read(readBuf)
if n > 0 {
chunk := readBuf[:n]
for len(chunk) > 0 {
newlineIdx := bytes.IndexByte(chunk, '\n')
if newlineIdx < 0 {
accumulate(chunk)
break
}
accumulate(chunk[:newlineIdx])
storeLine()
chunk = chunk[newlineIdx+1:]
}
}
if err == io.EOF {
if currentLine.Len() > 0 {
storeLine()
}
break
}
if err != nil {
return "", 0, httpResp, fmt.Errorf("failed to read log content: %w", err)
}
}
var result []string
linesInBuffer := min(totalLines, maxJobLogLines)
startIndex := 0
if totalLines > maxJobLogLines {
startIndex = writeIndex
}
for i := range linesInBuffer {
idx := (startIndex + i) % maxJobLogLines
if validLines[idx] {
result = append(result, lines[idx])
}
}
return strings.Join(result, "\n"), totalLines, httpResp, nil
}