Compare commits

..

1 Commits

Author SHA1 Message Date
Hui Wang
b7ef6cc3ed app/vmselect: fail the query request directly when there is not enough disk space
Previously, vmselect may crash if there is not enough free disk space to store tmp files from vmstorage. vmselect stores data blocks received from vmstorage if response size exceeds in-memory limit.
 It reduced vmselect availability, because it makes impossible to serve small queries. And huge query may crash vmselect for all users.

 So this commit adds a error check instead. If there is not enough disk space, vmselect will stop query execution and return error back to user.

fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4688
2026-08-21 17:05:11 +02:00
4 changed files with 106 additions and 92 deletions

View File

@@ -62,6 +62,9 @@ type tmpBlocksFile struct {
r *fs.ReaderAt
offset uint64
// err stores the first error occurred while writing the temporary blocks file.
err error
}
func getTmpBlocksFile() *tmpBlocksFile {
@@ -82,6 +85,7 @@ func putTmpBlocksFile(tbf *tmpBlocksFile) {
tbf.f = nil
tbf.r = nil
tbf.offset = 0
tbf.err = nil
tmpBlocksFilePool.Put(tbf)
}
@@ -109,6 +113,10 @@ var (
// and this must be handled.
func (tbf *tmpBlocksFile) WriteBlockRefData(b []byte) (tmpBlockAddr, error) {
var addr tmpBlockAddr
if tbf.err != nil {
// Do not write anything to the tbf after the first failed write
return addr, tbf.err
}
addr.offset = tbf.offset
addr.size = len(b)
tbf.offset += uint64(addr.size)
@@ -122,7 +130,8 @@ func (tbf *tmpBlocksFile) WriteBlockRefData(b []byte) (tmpBlockAddr, error) {
if tbf.f == nil {
f, err := os.CreateTemp(tmpBlocksDir, "")
if err != nil {
return addr, err
tbf.err = fmt.Errorf("cannot create temporary blocks file at %q: %w", tmpBlocksDir, err)
return addr, tbf.err
}
tbf.f = f
tmpBlocksFilesCreated.Inc()
@@ -130,7 +139,9 @@ func (tbf *tmpBlocksFile) WriteBlockRefData(b []byte) (tmpBlockAddr, error) {
_, err := tbf.f.Write(tbf.buf)
tbf.buf = append(tbf.buf[:0], b...)
if err != nil {
return addr, fmt.Errorf("cannot write block to %q: %w", tbf.f.Name(), err)
// The blocks buffered at tbf.buf could be partially lost, mark the tbf as unusable.
tbf.err = fmt.Errorf("cannot write block to %q: %w", tbf.f.Name(), err)
return addr, tbf.err
}
return addr, nil
}
@@ -141,12 +152,16 @@ func (tbf *tmpBlocksFile) Len() uint64 {
}
func (tbf *tmpBlocksFile) Finalize() error {
if tbf.err != nil {
return tbf.err
}
if tbf.f == nil {
return nil
}
fname := tbf.f.Name()
if _, err := tbf.f.Write(tbf.buf); err != nil {
return fmt.Errorf("cannot write the remaining %d bytes to %q: %w", len(tbf.buf), fname, err)
tbf.err = fmt.Errorf("cannot write the remaining %d bytes to %q: %w", len(tbf.buf), fname, err)
return tbf.err
}
tbf.buf = tbf.buf[:0]
r := fs.NewReaderAt(tbf.f)
@@ -166,6 +181,10 @@ func (tbf *tmpBlocksFile) Finalize() error {
}
func (tbf *tmpBlocksFile) MustReadBlockRefAt(partRef storage.PartRef, addr tmpBlockAddr) storage.BlockRef {
if tbf.err != nil {
// This should never happen, since Finalize() already returns the error for such a tbf.
logger.Panicf("BUG: cannot read block at %s from the temporary blocks file with the failed write: %s", addr, tbf.err)
}
var buf []byte
if tbf.r == nil {
buf = tbf.buf[addr.offset : addr.offset+uint64(addr.size)]

View File

@@ -29,11 +29,11 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/), [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/), `vmstorage` and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): expose the `vm_app_prev_shutdown_unclean` gauge. It is set to `1` when the previous process run didn't shut down cleanly. Added the `UncleanShutdown` [alerting rule](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/rules/alerts-health.yml), which fires for 10 minutes after an unclean shutdown is detected. See [#8443](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8443).
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): show the selected time zone UTC offset next to the date/time controls and allow opening time zone settings from it. See [#11332](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11332).
* FEATURE: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/), [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/), [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/), and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): show how the default value is calculated for command-line flags which derive it from the number of available CPU cores. For example, `-maxConcurrentInserts` now prints `(default 16 = 2*cgroup.AvailableCPUs())` in `-help` output instead of `(default 16)`. Updated flags: `-search.maxConcurrentRequests`, `-search.maxWorkersPerQuery`, `-fs.maxConcurrency`, `-remoteWrite.concurrency`, `-remoteWrite.queues`. See [#9680](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9680). Thanks to @Vandit1604 for contribution.
* FEATURE: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): accept Unix timestamp values with negative scientific notation exponents in [Prometheus querying API](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#prometheus-querying-api-usage) time parameters such as `start` and `end` in `/api/v1/query_range`. Previously, values such as `1000e-1` were rejected. See [#11427](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11427).
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) and `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): fix infinite loop in the OpenTelemetry Firehose ingestion endpoint (`/opentelemetry/api/v1/push`) when receiving a malformed record with an incomplete varint in the `data` field. Previously this caused the goroutine to spin forever, permanently consuming CPU until the process was restarted.
* BUGFIX: [vmalert-tool](https://docs.victoriametrics.com/victoriametrics/vmalert-tool/): reuse connections to `-remoteWrite.url` when writing the results of recording rules and alerts. Previously every series was sent over a new connection, which left a lot of sockets in `TIME_WAIT` state and could exhaust the ephemeral port range. The number of idle connections can be tuned via the new `-remoteWrite.maxIdleConnections` command-line flag. Thanks @evkuzin for contribution.
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): prevent process crash in `sort_by_label_numeric()` and `sort_by_label_numeric_desc()` when a label value contains a number with 309 or more digits. See [#11423](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11423).
* BUGFIX: `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): fail the query request directly when there is not enough disk space to store temporary search results. Previously, such queries could lead to vmselect crash. See [#4688](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4688).
## [v1.150.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.150.0)

View File

@@ -79,7 +79,7 @@ func ParseTimeAt(s string, currentTimestamp int64) (int64, error) {
// Parse YYYY
return parseTimeAt("2006", s, tzOffset, sOrig)
}
if !strings.Contains(sOrig, "-") || getExpIndex(sOrig) >= 0 {
if !strings.Contains(sOrig, "-") {
nsec, ok := TryParseUnixTimestamp(sOrig)
if !ok {
return 0, fmt.Errorf("cannot parse numeric timestamp %q", sOrig)
@@ -186,84 +186,80 @@ func getExpIndex(s string) int {
}
func tryParseScientificUnixTimestamp(s string, decimalExp int64) (int64, bool) {
intStr, fracStr, ok := expandScientificUnixTimestamp(s, decimalExp)
if !ok {
if decimalExp < 0 {
// Negative exponents on a fractional mantissa are intentionally not
// supported. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11268
return 0, false
}
if fracStr == "" {
n, ok := tryParseInt64(intStr)
dotIdx := strings.IndexByte(s, '.')
if dotIdx < 0 {
n, ok := tryParseInt64(s)
if !ok {
return 0, false
}
n, ok = multiplyByDecimalExp(n, decimalExp)
if !ok {
return 0, false
}
return getUnixTimestampNanoseconds(n), true
}
intStr := s[:dotIdx]
fracStr := s[dotIdx+1:]
if decimalExp >= int64(len(fracStr)) {
// The exponent shifts the decimal point past every fractional digit.
n, ok := tryParseDecimalMantissaAsInt(intStr, fracStr)
if !ok {
return 0, false
}
decimalExp -= int64(len(fracStr))
n, ok = multiplyByDecimalExp(n, decimalExp)
if !ok {
return 0, false
}
return getUnixTimestampNanoseconds(n), true
}
// The exponent leaves fractional digits, e.g. 1.784144612388E9 == 1784144612.388
if decimalExp >= int64(len(decimalMultipliers)) {
return 0, false
}
decimalExpInt := int(decimalExp)
intStr = s[:dotIdx] + fracStr[:decimalExpInt]
fracStr = fracStr[decimalExpInt:]
return tryParseFractionalUnixTimestamp(intStr, fracStr)
}
func expandScientificUnixTimestamp(s string, decimalExp int64) (string, string, bool) {
dotIdx := strings.IndexByte(s, '.')
intStr := s
fracStr := ""
if dotIdx >= 0 {
intStr = s[:dotIdx]
fracStr = s[dotIdx+1:]
}
if _, ok := tryParseInt64(intStr); !ok {
return "", "", false
}
if !isDecimalString(fracStr) {
return "", "", false
func tryParseDecimalMantissaAsInt(intStr, fracStr string) (int64, bool) {
n, ok := tryParseInt64(intStr)
if !ok {
return 0, false
}
isNegativeExp := decimalExp < 0
if isNegativeExp {
if decimalExp <= -int64(len(decimalMultipliers)) {
return "", "", false
}
decimalExp = -decimalExp
}
if decimalExp > int64(math.MaxInt) || decimalExp < int64(math.MinInt) {
return "", "", false
decimalExp := int64(len(fracStr))
num, ok := multiplyByDecimalExp(n, decimalExp)
if !ok {
return 0, false
}
intStr = strings.TrimPrefix(intStr, "+")
isNegative := strings.HasPrefix(intStr, "-")
if isNegative {
intStr = intStr[1:]
frac, ok := tryParseInt64(fracStr)
if !ok {
return 0, false
}
var shiftedIntStr, shiftedFracStr string
if isNegativeExp {
// e.g.
// 1. the integer and fractional part of 1.23e-5 should be 0 and 0000123 respectively.
// 2. the integer and fractional part of 123.4e-1 should be 12 and 34 respectively.
if decimalExp >= int64(len(intStr)) {
zerosToAdd := decimalExp - int64(len(intStr))
shiftedIntStr = "0"
shiftedFracStr = strings.Repeat("0", int(zerosToAdd)) + intStr + fracStr
} else {
decimalExpInt := int(decimalExp)
shiftedIntStr = intStr[:len(intStr)-decimalExpInt]
shiftedFracStr = intStr[len(intStr)-decimalExpInt:] + fracStr
if num >= 0 {
if num > math.MaxInt64-frac {
return 0, false
}
} else if decimalExp >= int64(len(fracStr)) {
zerosToAdd := decimalExp - int64(len(fracStr))
if zerosToAdd >= int64(len(decimalMultipliers)) {
return "", "", false
}
// e.g. the integer part and fractional part of 1.23e5 should be 123000 and 0 respectively.
shiftedIntStr = intStr + fracStr + strings.Repeat("0", int(zerosToAdd))
shiftedFracStr = ""
num += frac
} else {
decimalExpInt := int(decimalExp)
shiftedIntStr = intStr + fracStr[:decimalExpInt]
shiftedFracStr = fracStr[decimalExpInt:]
if num < math.MinInt64+frac {
return 0, false
}
num -= frac
}
if isNegative {
shiftedIntStr = "-" + shiftedIntStr
}
return shiftedIntStr, shiftedFracStr, true
return num, true
}
func tryParseFractionalUnixTimestamp(intStr, fracStr string) (int64, bool) {
@@ -274,11 +270,15 @@ func tryParseFractionalUnixTimestamp(intStr, fracStr string) (int64, bool) {
isNegative := n < 0 || n == 0 && strings.HasPrefix(intStr, "-")
multiplier, maxFracDigits := getUnixTimestampMultiplier(n)
if !isDecimalString(fracStr) {
return 0, false
}
// Truncate the fractional digits to valid length according to the unit precision.
if len(fracStr) > maxFracDigits {
// 1.123456789XXX is invalid.
tail := fracStr[maxFracDigits:]
for i := 0; i < len(tail); i++ {
if tail[i] < '0' || tail[i] > '9' {
return 0, false
}
}
fracStr = fracStr[:maxFracDigits]
}
if len(fracStr) == 0 {
@@ -309,6 +309,26 @@ func tryParseFractionalUnixTimestamp(intStr, fracStr string) (int64, bool) {
return n + frac, true
}
func multiplyByDecimalExp(n int64, decimalExp int64) (int64, bool) {
if decimalExp < 0 {
return 0, false
}
if decimalExp >= int64(len(decimalMultipliers)) {
return 0, false
}
if decimalExp == 0 {
return n, true
}
m := decimalMultipliers[decimalExp]
if n >= 0 && n > math.MaxInt64/m || n < 0 && n < math.MinInt64/m {
return 0, false
}
return n * m, true
}
var decimalMultipliers = [...]int64{0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18}
const (
@@ -349,12 +369,3 @@ func tryParseInt64(s string) (int64, bool) {
}
return n, true
}
func isDecimalString(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return false
}
}
return true
}

View File

@@ -76,7 +76,6 @@ func TestTryParseUnixTimestamp_Success(t *testing.T) {
f("-1.23456789e9", -1234567890_000_000_000)
f("1.234567890123456789e18", 1234567890_123_456_789)
f("-1.234567890123456789e18", -1234567890_123_456_789)
f("0.0000000001e19", 1000000000_000_000_000)
f("0.23456789e9", 234567890_000_000_000)
f("123.456789123e9", 123456789_123_000_000)
f("-1234.5678912e9", -1234567891_200_000_000)
@@ -100,19 +99,7 @@ func TestTryParseUnixTimestamp_Success(t *testing.T) {
f("1.23456789e9", 1234567890_000_000_000) // exponent consumes all frac digits (integer result)
f("1.23e1", 12_300_000_000) // == 12.3
f("1.234e0", 1_234_000_000) // == 1.234
f("-0.1e1", -1_000_000_000) // == -1
f("1234567890123456789.0e0", 1234567890_123_456_789)
f("1000e-1", 100_000_000_000)
f("1000E-1", 100_000_000_000)
f("-1000e-1", -100_000_000_000)
f("1e-1", 100_000_000)
f("-1e-1", -100_000_000)
f("1000.01e-1", 100_001_000_000)
f("-1000.01e-1", -100_001_000_000)
f("1.2E-1", 120_000_000)
f("1.2345678901e-1", 123_456_789)
f("0.12345678901", 123_456_789)
f("1.2345678901234567890123e-1", 123_456_789)
}
@@ -151,8 +138,7 @@ func TestTryParseUnixTimestamp_Failure(t *testing.T) {
f("1.3e123456789090123")
// negative decimal exponent
f("1e-9223372036854775808")
f("1e-123456789090123")
f("1E-1")
f("1.3e-123456789090123")
}
@@ -178,8 +164,6 @@ func TestParseTimeAtSuccess(t *testing.T) {
f("1562529662.678", now, 1562529662_678_000_000)
f("1562529662.678123", now, 1562529662_678_123_000)
f("1562529662.678123456", now, 1562529662_678_123_456)
f("1000e-1", now, 100_000_000_000)
f("1000.01e-1", now, 100_001_000_000)
// unix timestamp in milliseconds
f("1562529662678", now, 1562529662_678_000_000)