Compare commits

...

4 Commits

Author SHA1 Message Date
“Jayice”
d3f1c38b87 address review comments
Signed-off-by: “Jayice” <jzhou@victoriametrics.com>
2026-07-31 17:54:11 +08:00
“Jayice”
127eaa6f87 address review comments
Signed-off-by: “Jayice” <jzhou@victoriametrics.com>
2026-07-31 16:59:29 +08:00
“Jayice”
f25ee774cd polish CHANGELOG.md
Signed-off-by: “Jayice” <jzhou@victoriametrics.com>
2026-07-31 16:41:57 +08:00
“Jayice”
d89d5734cc properly parse small fractional Unix timestamps
Signed-off-by: “Jayice” <jzhou@victoriametrics.com>
2026-07-31 16:29:18 +08:00
3 changed files with 157 additions and 99 deletions

View File

@@ -48,6 +48,7 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
* BUGFIX: [vmbackupmanager](https://docs.victoriametrics.com/victoriametrics/vmbackupmanager/): previously, `vmbackupmanager` was crashing on startup when it failed to restore backup state from remote storage, causing a crash loop. Now it logs the error and continues running, retrying the state restore before each scheduled backup. Added `vm_backup_errors_total{type="restoreState"}` metric to track backup state restore failures. See [#11217](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11217).
* BUGFIX: [stream aggregation](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/): fix incorrect [sum_samples_total](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#sum_samples_total) results when `enable_windows: true` is set. See [#11261](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11261). Thanks to @beyond-infra for contribution.
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/), `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) and [vmctl](https://docs.victoriametrics.com/victoriametrics/vmctl/): accept scientific notation with sub-second precision (e.g. `1.784144612388E9`) for timestamp args such as `start` and `end` in `/api/v1/query_range` and `--vm-native-filter-time-start` and `--vm-native-filter-time-end` in `vmctl`. Previously, values with this pattern were rejected, which is incompatible with Prometheus. See [#11268](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11268). Thanks to @STiFLeR7 for contribution.
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/), `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) and [vmctl](https://docs.victoriametrics.com/victoriametrics/vmctl/): properly parse small fractional Unix timestamps in timestamp args such as `start` and `end` in `/api/v1/query_range` and `--vm-native-filter-time-start` and `--vm-native-filter-time-end` in `vmctl`. Previously, fractional Unix timestamps with the integer part below `9223372` will be interpreted with the wrong unit, for example `12.0` previously will be parsed as `12000` seconds instead of `12` seconds. See [#11324](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11324).
## [v1.148.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.148.0)

View File

@@ -148,11 +148,11 @@ func TryParseUnixTimestamp(s string) (int64, bool) {
if !ok {
return 0, false
}
n, ok := tryParseScientificNumberForUnixTimestamp(s[:expIdx], decimalExp)
n, ok := tryParseScientificUnixTimestamp(s[:expIdx], decimalExp)
if !ok {
return 0, false
}
return getUnixTimestampNanoseconds(n), true
return n, true
}
dotIdx := strings.IndexByte(s, '.')
@@ -168,22 +168,11 @@ func TryParseUnixTimestamp(s string) (int64, bool) {
// The timestamp is fractional.
intStr := s[:dotIdx]
fracStr := s[dotIdx+1:]
n, ok := tryParseFractionalNumberForUnixTimestamp(intStr, fracStr)
n, ok := tryParseFractionalUnixTimestamp(intStr, fracStr)
if !ok {
return 0, false
}
// Adjust the n to multiples of thousands, since this is expected by getUnixTimestampNanoseconds.
decimalExp := len(fracStr)
for decimalExp%3 != 0 {
if n >= 0 && n > math.MaxInt64/10 || n < 0 && n < math.MinInt64/10 {
return 0, false
}
n *= 10
decimalExp++
}
return getUnixTimestampNanoseconds(n), true
return n, true
}
func getExpIndex(s string) int {
@@ -196,14 +185,18 @@ func getExpIndex(s string) int {
return -1
}
func tryParseScientificNumberForUnixTimestamp(s string, decimalExp int64) (int64, bool) {
func tryParseScientificUnixTimestamp(s string, decimalExp int64) (int64, bool) {
dotIdx := strings.IndexByte(s, '.')
if dotIdx < 0 {
n, ok := tryParseInt64(s)
if !ok {
return 0, false
}
return multiplyByDecimalExp(n, decimalExp)
n, ok = multiplyByDecimalExp(n, decimalExp)
if !ok {
return 0, false
}
return getUnixTimestampNanoseconds(n), true
}
if decimalExp < 0 {
@@ -214,31 +207,32 @@ func tryParseScientificNumberForUnixTimestamp(s string, decimalExp int64) (int64
intStr := s[:dotIdx]
fracStr := s[dotIdx+1:]
n, ok := tryParseFractionalNumberForUnixTimestamp(intStr, fracStr)
if !ok {
return 0, false
}
if decimalExp >= int64(len(fracStr)) {
// The exponent shifts the decimal point past every fractional digit,
// so the value is an integer number of seconds (or coarser).
n, ok := tryParseDecimalMantissaAsInt(intStr, fracStr)
if !ok {
return 0, false
}
decimalExp -= int64(len(fracStr))
return multiplyByDecimalExp(n, decimalExp)
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
// Pad n as plain fractional timestamps do.
fracDigits := int64(len(fracStr)) - decimalExp
for fracDigits%3 != 0 {
if n >= 0 && n > math.MaxInt64/10 || n < 0 && n < math.MinInt64/10 {
return 0, false
}
n *= 10
fracDigits++
if decimalExp >= int64(len(decimalMultipliers)) {
return 0, false
}
return n, true
decimalExpInt := int(decimalExp)
intStr = s[:dotIdx] + fracStr[:decimalExpInt]
fracStr = fracStr[decimalExpInt:]
return tryParseFractionalUnixTimestamp(intStr, fracStr)
}
func tryParseFractionalNumberForUnixTimestamp(intStr, fracStr string) (int64, bool) {
func tryParseDecimalMantissaAsInt(intStr, fracStr string) (int64, bool) {
n, ok := tryParseInt64(intStr)
if !ok {
return 0, false
@@ -270,6 +264,49 @@ func tryParseFractionalNumberForUnixTimestamp(intStr, fracStr string) (int64, bo
return num, true
}
func tryParseFractionalUnixTimestamp(intStr, fracStr string) (int64, bool) {
n, ok := tryParseInt64(intStr)
if !ok {
return 0, false
}
frac, ok := tryParseInt64(fracStr)
if !ok {
return 0, false
}
decimalExp := len(fracStr)
if decimalExp == 0 || decimalExp >= len(decimalMultipliers) {
return 0, false
}
multiplier := getUnixTimestampNanosecondsMultiplier(n)
if multiplier == 1 && frac != 0 {
return 0, false
}
n *= multiplier
scale := decimalMultipliers[decimalExp]
var fracNsec int64
if scale >= multiplier {
divisor := scale / multiplier
// Reject fractional values that cannot be represented as a whole number of nanoseconds.
// e.g. 1700000000000000000.1 ns
if frac%divisor != 0 {
return 0, false
}
fracNsec = frac / divisor
} else {
fracNsec = frac * (multiplier / scale)
}
if strings.HasPrefix(intStr, "-") {
if n < math.MinInt64+fracNsec {
return 0, false
}
return n - fracNsec, true
}
if n > math.MaxInt64-fracNsec {
return 0, false
}
return n + fracNsec, true
}
func multiplyByDecimalExp(n int64, decimalExp int64) (int64, bool) {
if decimalExp < 0 {
return 0, false
@@ -302,20 +339,24 @@ const (
)
func getUnixTimestampNanoseconds(n int64) int64 {
return n * getUnixTimestampNanosecondsMultiplier(n)
}
func getUnixTimestampNanosecondsMultiplier(n int64) int64 {
if n <= maxValidSecond && n >= minValidSecond {
// The timestamp is in seconds.
return n * 1e9
return 1e9
}
if n <= maxValidMilli && n >= minValidMilli {
// The timestamp is in milliseconds.
return n * 1e6
return 1e6
}
if n <= maxValidMicro && n >= minValidMicro {
// The timestamp is in microseconds.
return n * 1e3
return 1e3
}
// The timestamp is in nanoseconds
return n
return 1
}
func tryParseInt64(s string) (int64, bool) {

View File

@@ -24,62 +24,74 @@ func TestTryParseUnixTimestamp_Success(t *testing.T) {
f("0", 0)
// nanoseconds
f("-1234567890123456789", -1234567890123456789)
f("1234567890123456789", 1234567890123456789)
f("1234567890123456.789", 1234567890123456789)
f("-1234567890123456789", -1234567890_123_456_789)
f("1234567890123456789", 1234567890_123_456_789)
f("1234567890123456.789", 1234567890_123_456_789)
// microseconds
f("-1234567890123456", -1234567890123456000)
f("1234567890123456", 1234567890123456000)
f("1234567890123456.789", 1234567890123456789)
f("-1234567890123456", -1234567890_123_456_000)
f("1234567890123456", 1234567890_123_456_000)
f("1234567890123456.789", 1234567890_123_456_789)
f("12345678901234.5000", 12345678_901_234_500)
// milliseconds
f("-1234567890123", -1234567890123000000)
f("1234567890123", 1234567890123000000)
f("1234567890123.456", 1234567890123456000)
f("-1234567890123", -1234567890_123_000_000)
f("1234567890123", 1234567890_123_000_000)
f("1234567890123.456", 1234567890_123_456_000)
// seconds
f("-1234567890", -1234567890000000000)
f("1234567890", 1234567890000000000)
f("1234567890.123456789", 1234567890123456789)
f("1234567890.12345678", 1234567890123456780)
f("1234567890.1234567", 1234567890123456700)
f("-1234567890.123456", -1234567890123456000)
f("-1234567890.12345", -1234567890123450000)
f("-1234567890.1234", -1234567890123400000)
f("-1234567890.123", -1234567890123000000)
f("-1234567890.12", -1234567890120000000)
f("-1234567890.1", -1234567890100000000)
f("-1234567890", -1234567890_000_000_000)
f("1234567890", 1234567890_000_000_000)
f("1234567890.123456789", 1234567890_123_456_789)
f("1234567890.12345678", 1234567890_123_456_780)
f("1234567890.1234567", 1234567890_123_456_700)
f("-1234567890.123456", -1234567890_123_456_000)
f("-1234567890.12345", -1234567890_123_450_000)
f("-1234567890.1234", -1234567890_123_400_000)
f("-1234567890.123", -1234567890_123_000_000)
f("-1234567890.12", -1234567890_120_000_000)
f("-1234567890.1", -1234567890_100_000_000)
f("12", 12_000_000_000)
f("12.0", 12_000_000_000)
f("12.34", 12_340_000_000)
f("12.999999999000000000", 12_999_999_999)
f("-12", -12_000_000_000)
f("-12.34", -12_340_000_000)
f("8223372", 8223372_000_000_000)
f("8223372.0", 8223372_000_000_000)
f("1700000000", 1700000000_000_000_000)
f("1700000000.0", 1700000000_000_000_000)
// scientific notation
f("1e9", 1000000000000000000)
f("1.234e9", 1234000000000000000)
f("-1.23456789e9", -1234567890000000000)
f("1.234567890123456789e18", 1234567890123456789)
f("-1.234567890123456789e18", -1234567890123456789)
f("0.23456789e9", 234567890000000000)
f("123.456789123e9", 123456789123000000)
f("-1234.5678912e9", -1234567891200000000)
f("123.678912e7", 1236789120000000000)
f("1.23e7", 12300000000000000)
f("1.23e6", 1230000000000000)
f("1.23e5", 123000000000000)
f("1.23e4", 12300000000000)
f("1.23e3", 1230000000000)
f("1.23e2", 123000000000)
f("1.2e1", 12000000000)
f("1123.456789123456789E15", 1123456789123456789)
f("1e9", 1000000000_000_000_000)
f("1.234e9", 1234000000_000_000_000)
f("-1.23456789e9", -1234567890_000_000_000)
f("1.234567890123456789e18", 1234567890_123_456_789)
f("-1.234567890123456789e18", -1234567890_123_456_789)
f("0.23456789e9", 234567890_000_000_000)
f("123.456789123e9", 123456789_123_000_000)
f("-1234.5678912e9", -1234567891_200_000_000)
f("123.678912e7", 1236789120_000_000_000)
f("1.23e7", 12300000_000_000_000)
f("1.23e6", 1230000_000_000_000)
f("1.23e5", 123000_000_000_000)
f("1.23e4", 12300_000_000_000)
f("1.23e3", 1230_000_000_000)
f("1.23e2", 123_000_000_000)
f("1.2e1", 12_000_000_000)
f("1123.456789123456789E15", 1123456789_123_456_789)
// scientific notation with sub-second precision, i.e. more fractional digits
// than the exponent shifts (https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11268).
// These must match the equivalent plain fractional form.
f("1.784144612388E9", 1784144612388000000) // == 1784144612.388
f("1.784144612388e9", 1784144612388000000)
f("-1.784144612388e9", -1784144612388000000)
f("1.5000000005e9", 1500000000500000000) // == 1500000000.5
f("1.23456789e9", 1234567890000000000) // exponent consumes all frac digits (integer result)
f("1.23e1", 12300000000000) // == 12.3
f("1.234e0", 1234000000000) // == 1.234
f("1.784144612388E9", 1784144612_388_000_000) // == 1784144612.388
f("1.784144612388e9", 1784144612_388_000_000)
f("-1.784144612388e9", -1784144612_388_000_000)
f("1.5000000005e9", 1500000000_500_000_000) // == 1500000000.5
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("1234567890123456789.0e0", 1234567890_123_456_789)
}
func TestTryParseUnixTimestamp_Failure(t *testing.T) {
@@ -129,6 +141,10 @@ func TestTryParseUnixTimestamp_Failure(t *testing.T) {
// negative decimal exponent
f("1E-1")
f("1.3e-123456789090123")
// fractional part for nanoseconds
f("1234567890123456789.1")
f("1234567890123456789.1e0")
}
func TestParseTimeAtSuccess(t *testing.T) {
@@ -146,27 +162,27 @@ func TestParseTimeAtSuccess(t *testing.T) {
now := time.Now().UnixNano()
// unix timestamp in seconds
f("1562529662", now, 1562529662*1e9)
f("1562529662.6", now, 1562529662600*1e6)
f("1562529662.67", now, 1562529662670*1e6)
f("1562529662.678", now, 1562529662678*1e6)
f("1562529662.678123", now, 1562529662678123*1e3)
f("1562529662.678123456", now, 1562529662678123456)
f("1562529662", now, 1562529662_000_000_000)
f("1562529662.6", now, 1562529662_600_000_000)
f("1562529662.67", now, 1562529662_670_000_000)
f("1562529662.678", now, 1562529662_678_000_000)
f("1562529662.678123", now, 1562529662_678_123_000)
f("1562529662.678123456", now, 1562529662_678_123_456)
// unix timestamp in milliseconds
f("1562529662678", now, 1562529662678*1e6)
f("1562529662678.9", now, 1562529662678900*1e3)
f("1562529662678.901", now, 1562529662678901*1e3)
f("1562529662678.901324", now, 1562529662678901324)
f("1562529662678", now, 1562529662_678_000_000)
f("1562529662678.9", now, 1562529662_678_900_000)
f("1562529662678.901", now, 1562529662_678_901_000)
f("1562529662678.901324", now, 1562529662_678_901_324)
// unix timestamp in microseconds
f("1562529662678901", now, 1562529662678901*1e3)
f("1562529662678901.3", now, 1562529662678901300)
f("1562529662678901.32", now, 1562529662678901320)
f("1562529662678901.321", now, 1562529662678901321)
f("1562529662678901", now, 1562529662_678_901_000)
f("1562529662678901.3", now, 1562529662_678_901_300)
f("1562529662678901.32", now, 1562529662_678_901_320)
f("1562529662678901.321", now, 1562529662_678_901_321)
// unix timestamp in nanoseconds
f("1562529662678901234", now, 1562529662678901234)
f("1562529662678901234", now, 1562529662_678_901_234)
// duration relative to the current time
f("now", now, now)
@@ -175,7 +191,7 @@ func TestParseTimeAtSuccess(t *testing.T) {
// negative duration relative to the current time
f("-5m", now, now-5*60*1e9)
f("-123", now, now-123*1e9)
f("-123.456", now, now-123456*1e6)
f("-123.456", now, now-123_456*1e6)
f("now-1h5m", now, now-(3600+5*60)*1e9)
// Year
@@ -209,9 +225,9 @@ func TestParseTimeAtSuccess(t *testing.T) {
f("2023-05-20T04:57:43-02:30", now, 1.684567663e+09*1e9)
// milliseconds
f("2023-05-20T04:57:43.123Z", now, 1684558663123000000)
f("2023-05-20T04:57:43.123456789+02:30", now, 1684549663123456789)
f("2023-05-20T04:57:43.123456789-02:30", now, 1684567663123456789)
f("2023-05-20T04:57:43.123Z", now, 1684558663_123_000_000)
f("2023-05-20T04:57:43.123456789+02:30", now, 1684549663_123_456_789)
f("2023-05-20T04:57:43.123456789-02:30", now, 1684567663_123_456_789)
}
func TestParseTimeAtLimits(t *testing.T) {