Repository: VictoriaMetrics/VictoriaMetrics
Pull request #5812 (2024-02-15 10:12:51)

[lib/httputils] fixed error message for getting zero duration (#5795)
issue #5795 


Patch:
diff --git a/app/vmctl/flags.go b/app/vmctl/flags.go
index 01d0a7dbb..7aa227cd7 100644
--- a/app/vmctl/flags.go
+++ b/app/vmctl/flags.go
@@ -208,6 +208,11 @@ const (
 	influxMeasurementFieldSeparator = "influx-measurement-field-separator"
 	influxSkipDatabaseLabel         = "influx-skip-database-label"
 	influxPrometheusMode            = "influx-prometheus-mode"
+	influxCertFile                  = "influx-cert-file"
+	influxKeyFile                   = "influx-key-file"
+	influxCAFile                    = "influx-CA-file"
+	influxServerName                = "influx-server-name"
+	influxInsecureSkipVerify        = "influx-insecure-skip-verify"
 )
 
 var (
@@ -272,7 +277,28 @@ var (
 		},
 		&cli.BoolFlag{
 			Name:  influxPrometheusMode,
-			Usage: "Wether to restore the original timeseries name previously written from Prometheus to InfluxDB v1 via remote_write.",
+			Usage: "Whether to restore the original timeseries name previously written from Prometheus to InfluxDB v1 via remote_write.",
+			Value: false,
+		},
+		&cli.StringFlag{
+			Name:  influxCertFile,
+			Usage: "Optional path to client-side TLS certificate file to use when connecting to -influx-addr",
+		},
+		&cli.StringFlag{
+			Name:  influxKeyFile,
+			Usage: "Optional path to client-side TLS key to use when connecting to -influx-addr",
+		},
+		&cli.StringFlag{
+			Name:  influxCAFile,
+			Usage: "Optional path to TLS CA file to use for verifying connections to -influx-addr. By default, system CA is used",
+		},
+		&cli.StringFlag{
+			Name:  influxServerName,
+			Usage: "Optional TLS server name to use for connections to -influx-addr. By default, the server name from -influx-addr is used",
+		},
+		&cli.BoolFlag{
+			Name:  influxInsecureSkipVerify,
+			Usage: "Whether to skip tls verification when connecting to -influx-addr",
 			Value: false,
 		},
 	}
@@ -496,6 +522,10 @@ const (
 	remoteReadPassword           = "remote-read-password"
 	remoteReadHTTPTimeout        = "remote-read-http-timeout"
 	remoteReadHeaders            = "remote-read-headers"
+	remoteReadCertFile           = "remote-read-cert-file"
+	remoteReadKeyFile            = "remote-read-key-file"
+	remoteReadCAFile             = "remote-read-CA-file"
+	remoteReadServerName         = "remote-read-server-name"
 	remoteReadInsecureSkipVerify = "remote-read-insecure-skip-verify"
 	remoteReadDisablePathAppend  = "remote-read-disable-path-append"
 )
@@ -574,6 +604,22 @@ var (
 				"For example, --remote-read-headers='My-Auth:foobar' would send 'My-Auth: foobar' HTTP header with every request to the corresponding remote source storage. \n" +
 				"Multiple headers must be delimited by '^^': --remote-read-headers='header1:value1^^header2:value2'",
 		},
+		&cli.StringFlag{
+			Name:  remoteReadCertFile,
+			Usage: "Optional path to client-side TLS certificate file to use when connecting to -remote-read-src-addr",
+		},
+		&cli.StringFlag{
+			Name:  remoteReadKeyFile,
+			Usage: "Optional path to client-side TLS key to use when connecting to -remote-read-src-addr",
+		},
+		&cli.StringFlag{
+			Name:  remoteReadCAFile,
+			Usage: "Optional path to TLS CA file to use for verifying connections to -remote-read-src-addr. By default, system CA is used",
+		},
+		&cli.StringFlag{
+			Name:  remoteReadServerName,
+			Usage: "Optional TLS server name to use for connections to remoteReadSrcAddr. By default, the server name from -remote-read-src-addr is used",
+		},
 		&cli.BoolFlag{
 			Name:  remoteReadInsecureSkipVerify,
 			Usage: "Whether to skip TLS certificate verification when connecting to the remote read address",
diff --git a/app/vmctl/influx/influx.go b/app/vmctl/influx/influx.go
index 13b374a63..3b1c4c482 100644
--- a/app/vmctl/influx/influx.go
+++ b/app/vmctl/influx/influx.go
@@ -1,6 +1,7 @@
 package influx
 
 import (
+	"crypto/tls"
 	"fmt"
 	"io"
 	"log"
@@ -33,7 +34,8 @@ type Config struct {
 	Retention string
 	ChunkSize int
 
-	Filter Filter
+	Filter    Filter
+	TLSConfig *tls.Config
 }
 
 // Filter contains configuration for filtering
@@ -86,10 +88,10 @@ type LabelPair struct {
 // configured with passed Config
 func NewClient(cfg Config) (*Client, error) {
 	c := influx.HTTPConfig{
-		Addr:               cfg.Addr,
-		Username:           cfg.Username,
-		Password:           cfg.Password,
-		InsecureSkipVerify: true,
+		Addr:      cfg.Addr,
+		Username:  cfg.Username,
+		Password:  cfg.Password,
+		TLSConfig: cfg.TLSConfig,
 	}
 	hc, err := influx.NewHTTPClient(c)
 	if err != nil {
diff --git a/app/vmctl/main.go b/app/vmctl/main.go
index 96cd936f2..78913790b 100644
--- a/app/vmctl/main.go
+++ b/app/vmctl/main.go
@@ -25,6 +25,7 @@ import (
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/prometheus"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/common"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/native/stream"
 )
@@ -84,6 +85,18 @@ func main() {
 				Action: func(c *cli.Context) error {
 					fmt.Println("InfluxDB import mode")
 
+					// create TLS config
+					certFile := c.String(influxCertFile)
+					keyFile := c.String(influxKeyFile)
+					caFile := c.String(influxCAFile)
+					serverName := c.String(influxServerName)
+					insecureSkipVerify := c.Bool(influxInsecureSkipVerify)
+
+					tc, err := httputils.TLSConfig(certFile, caFile, keyFile, serverName, insecureSkipVerify)
+					if err != nil {
+						return fmt.Errorf("failed to create TLS Config: %s", err)
+					}
+
 					iCfg := influx.Config{
 						Addr:      c.String(influxAddr),
 						Username:  c.String(influxUser),
@@ -96,7 +109,9 @@ func main() {
 							TimeEnd:   c.String(influxFilterTimeEnd),
 						},
 						ChunkSize: c.Int(influxChunkSize),
+						TLSConfig: tc,
 					}
+
 					influxClient, err := influx.NewClient(iCfg)
 					if err != nil {
 						return fmt.Errorf("failed to create influx client: %s", err)
@@ -134,6 +149,10 @@ func main() {
 						Headers:            c.String(remoteReadHeaders),
 						LabelName:          c.String(remoteReadFilterLabel),
 						LabelValue:         c.String(remoteReadFilterLabelValue),
+						CertFile:           c.String(remoteReadCertFile),
+						KeyFile:            c.String(remoteReadKeyFile),
+						CAFile:             c.String(remoteReadCAFile),
+						ServerName:         c.String(remoteReadServerName),
 						InsecureSkipVerify: c.Bool(remoteReadInsecureSkipVerify),
 						DisablePathAppend:  c.Bool(remoteReadDisablePathAppend),
 					})
diff --git a/app/vmctl/remoteread/remoteread.go b/app/vmctl/remoteread/remoteread.go
index 5752ac08a..349fbb165 100644
--- a/app/vmctl/remoteread/remoteread.go
+++ b/app/vmctl/remoteread/remoteread.go
@@ -11,9 +11,9 @@ import (
 	"strings"
 	"time"
 
-	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/utils"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
 	"github.com/gogo/protobuf/proto"
 	"github.com/golang/snappy"
 	"github.com/prometheus/prometheus/prompb"
@@ -64,6 +64,13 @@ type Config struct {
 	// LabelName, LabelValue stands for label=~value pair used for read requests.
 	// Is optional.
 	LabelName, LabelValue string
+
+	// Optional cert file, key file, CA file and server name for client side TLS configuration
+	CertFile   string
+	KeyFile    string
+	CAFile     string
+	ServerName string
+
 	// TLSSkipVerify defines whether to skip TLS certificate verification when connecting to the remote read address.
 	InsecureSkipVerify bool
 }
@@ -103,10 +110,15 @@ func NewClient(cfg Config) (*Client, error) {
 		}
 	}
 
+	tr, err := httputils.Transport(cfg.Addr, cfg.CertFile, cfg.KeyFile, cfg.CAFile, cfg.ServerName, cfg.InsecureSkipVerify)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create transport: %s", err)
+	}
+
 	c := &Client{
 		c: &http.Client{
 			Timeout:   cfg.Timeout,
-			Transport: utils.Transport(cfg.Addr, cfg.InsecureSkipVerify),
+			Transport: tr,
 		},
 		addr:              strings.TrimSuffix(cfg.Addr, "/"),
 		disablePathAppend: cfg.DisablePathAppend,
diff --git a/app/vmctl/utils/time.go b/app/vmctl/time.go
similarity index 86%
rename from app/vmctl/utils/time.go
rename to app/vmctl/time.go
index 9b3b496b4..43dd7588d 100644
--- a/app/vmctl/utils/time.go
+++ b/app/vmctl/time.go
@@ -1,4 +1,4 @@
-package utils
+package main
 
 import (
 	"fmt"
@@ -13,8 +13,7 @@ const (
 	maxTimeMsecs = int64(1<<63-1) / 1e6
 )
 
-// GetTime  returns time from the given string.
-func GetTime(s string) (time.Time, error) {
+func parseTime(s string) (time.Time, error) {
 	secs, err := promutils.ParseTime(s)
 	if err != nil {
 		return time.Time{}, fmt.Errorf("cannot parse %s: %w", s, err)
diff --git a/app/vmctl/utils/tls.go b/app/vmctl/utils/tls.go
deleted file mode 100644
index fc0eb3d00..000000000
--- a/app/vmctl/utils/tls.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package utils
-
-import (
-	"crypto/tls"
-	"net/http"
-	"strings"
-)
-
-// Transport creates http.Transport object based on provided URL.
-// Returns Transport with TLS configuration if URL contains `https` prefix
-func Transport(URL string, insecureSkipVerify bool) *http.Transport {
-	t := http.DefaultTransport.(*http.Transport).Clone()
-	if !strings.HasPrefix(URL, "https") {
-		return t
-	}
-	t.TLSClientConfig = TLSConfig(insecureSkipVerify)
-	return t
-}
-
-// TLSConfig creates tls.Config object from provided arguments
-func TLSConfig(insecureSkipVerify bool) *tls.Config {
-	return &tls.Config{
-		InsecureSkipVerify: insecureSkipVerify,
-	}
-}
diff --git a/app/vmctl/vm_native.go b/app/vmctl/vm_native.go
index 3cbea821d..31375ae3d 100644
--- a/app/vmctl/vm_native.go
+++ b/app/vmctl/vm_native.go
@@ -16,7 +16,6 @@ import (
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/limiter"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/native"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/stepper"
-	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/utils"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutils"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
@@ -54,14 +53,14 @@ func (p *vmNativeProcessor) run(ctx context.Context) error {
 		startTime: time.Now(),
 	}
 
-	start, err := utils.GetTime(p.filter.TimeStart)
+	start, err := parseTime(p.filter.TimeStart)
 	if err != nil {
 		return fmt.Errorf("failed to parse %s, provided: %s, error: %w", vmNativeFilterTimeStart, p.filter.TimeStart, err)
 	}
 
 	end := time.Now().In(start.Location())
 	if p.filter.TimeEnd != "" {
-		end, err = utils.GetTime(p.filter.TimeEnd)
+		end, err = parseTime(p.filter.TimeEnd)
 		if err != nil {
 			return fmt.Errorf("failed to parse %s, provided: %s, error: %w", vmNativeFilterTimeEnd, p.filter.TimeEnd, err)
 		}
diff --git a/app/vmstorage/main.go b/app/vmstorage/main.go
index fbd4ffe23..2cd7d371f 100644
--- a/app/vmstorage/main.go
+++ b/app/vmstorage/main.go
@@ -506,6 +506,10 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
 	metrics.WriteGaugeUint64(w, `vm_parts{type="indexdb/inmemory"}`, idbm.InmemoryPartsCount)
 	metrics.WriteGaugeUint64(w, `vm_parts{type="indexdb/file"}`, idbm.FilePartsCount)
 
+	metrics.WriteGaugeUint64(w, `vm_last_partition_parts{type="storage/inmemory"}`, tm.LastPartition.InmemoryPartsCount)
+	metrics.WriteGaugeUint64(w, `vm_last_partition_parts{type="storage/small"}`, tm.LastPartition.SmallPartsCount)
+	metrics.WriteGaugeUint64(w, `vm_last_partition_parts{type="storage/big"}`, tm.LastPartition.BigPartsCount)
+
 	metrics.WriteGaugeUint64(w, `vm_blocks{type="storage/inmemory"}`, tm.InmemoryBlocksCount)
 	metrics.WriteGaugeUint64(w, `vm_blocks{type="storage/small"}`, tm.SmallBlocksCount)
 	metrics.WriteGaugeUint64(w, `vm_blocks{type="storage/big"}`, tm.BigBlocksCount)
diff --git a/dashboards/victoriametrics-cluster.json b/dashboards/victoriametrics-cluster.json
index 48251c870..2ef8cf720 100644
--- a/dashboards/victoriametrics-cluster.json
+++ b/dashboards/victoriametrics-cluster.json
@@ -6210,7 +6210,7 @@
             "type": "prometheus",
             "uid": "$ds"
           },
-          "description": "Shows network usage by vmstorage services.\n* Writes show traffic sent to vmselects.\n* Reads show traffic received from vminserts.",
+          "description": "Shows network usage by vmstorage services.\n* Writes show traffic sent to clients.\n* Reads show traffic received from clients.",
           "fieldConfig": {
             "defaults": {
               "color": {
@@ -6310,10 +6310,10 @@
                 "uid": "$ds"
               },
               "editorMode": "code",
-              "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
+              "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) by(name) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from {{name}}",
               "range": true,
               "refId": "A"
             },
@@ -6323,11 +6323,11 @@
                 "uid": "$ds"
               },
               "editorMode": "code",
-              "expr": "sum(rate(vm_tcplistener_written_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
+              "expr": "sum(rate(vm_tcplistener_written_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) by(name) * 8 > 0",
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to {{name}}",
               "range": true,
               "refId": "B"
             }
@@ -7420,7 +7420,7 @@
               "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_select\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from client",
               "range": true,
               "refId": "A"
             },
@@ -7434,7 +7434,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to client",
               "range": true,
               "refId": "B"
             }
@@ -7550,7 +7550,7 @@
               "expr": "sum(rate(vm_tcpdialer_read_bytes_total{job=~\"$job_select\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from vmstorage",
               "range": true,
               "refId": "A"
             },
@@ -7564,7 +7564,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to vmstorage",
               "range": true,
               "refId": "B"
             }
@@ -8442,7 +8442,7 @@
               "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_insert\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from client",
               "range": true,
               "refId": "A"
             },
@@ -8456,7 +8456,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to client",
               "range": true,
               "refId": "B"
             }
@@ -8572,7 +8572,7 @@
               "expr": "sum(rate(vm_tcpdialer_read_bytes_total{job=~\"$job_insert\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from vmstorage",
               "range": true,
               "refId": "A"
             },
@@ -8586,7 +8586,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to vmstorage",
               "range": true,
               "refId": "B"
             }
diff --git a/dashboards/vm/victoriametrics-cluster.json b/dashboards/vm/victoriametrics-cluster.json
index 30f60d526..eac1a5451 100644
--- a/dashboards/vm/victoriametrics-cluster.json
+++ b/dashboards/vm/victoriametrics-cluster.json
@@ -6211,7 +6211,7 @@
             "type": "victoriametrics-datasource",
             "uid": "$ds"
           },
-          "description": "Shows network usage by vmstorage services.\n* Writes show traffic sent to vmselects.\n* Reads show traffic received from vminserts.",
+          "description": "Shows network usage by vmstorage services.\n* Writes show traffic sent to clients.\n* Reads show traffic received from clients.",
           "fieldConfig": {
             "defaults": {
               "color": {
@@ -6311,10 +6311,10 @@
                 "uid": "$ds"
               },
               "editorMode": "code",
-              "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
+              "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) by(name) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from {{name}}",
               "range": true,
               "refId": "A"
             },
@@ -6324,11 +6324,11 @@
                 "uid": "$ds"
               },
               "editorMode": "code",
-              "expr": "sum(rate(vm_tcplistener_written_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
+              "expr": "sum(rate(vm_tcplistener_written_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) by(name) * 8 > 0",
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to {{name}}",
               "range": true,
               "refId": "B"
             }
@@ -7421,7 +7421,7 @@
               "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_select\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from client",
               "range": true,
               "refId": "A"
             },
@@ -7435,7 +7435,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to client",
               "range": true,
               "refId": "B"
             }
@@ -7551,7 +7551,7 @@
               "expr": "sum(rate(vm_tcpdialer_read_bytes_total{job=~\"$job_select\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from vmstorage",
               "range": true,
               "refId": "A"
             },
@@ -7565,7 +7565,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to vmstorage",
               "range": true,
               "refId": "B"
             }
@@ -8443,7 +8443,7 @@
               "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_insert\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from client",
               "range": true,
               "refId": "A"
             },
@@ -8457,7 +8457,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to client",
               "range": true,
               "refId": "B"
             }
@@ -8573,7 +8573,7 @@
               "expr": "sum(rate(vm_tcpdialer_read_bytes_total{job=~\"$job_insert\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from vmstorage",
               "range": true,
               "refId": "A"
             },
@@ -8587,7 +8587,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to vmstorage",
               "range": true,
               "refId": "B"
             }
diff --git a/deployment/docker/vmanomaly/vmanomaly-integration/docker-compose.yml b/deployment/docker/vmanomaly/vmanomaly-integration/docker-compose.yml
index 3b4af7ce7..5474947b9 100644
--- a/deployment/docker/vmanomaly/vmanomaly-integration/docker-compose.yml
+++ b/deployment/docker/vmanomaly/vmanomaly-integration/docker-compose.yml
@@ -73,7 +73,7 @@ services:
     restart: always
   vmanomaly:
     container_name: vmanomaly
-    image: victoriametrics/vmanomaly:v1.9.2
+    image: victoriametrics/vmanomaly:v1.10.0
     depends_on:
       - "victoriametrics"
     ports:
diff --git a/deployment/docker/vmanomaly/vmanomaly-integration/vmanomaly_config.yml b/deployment/docker/vmanomaly/vmanomaly-integration/vmanomaly_config.yml
index 9990779b1..dd3c20ccf 100644
--- a/deployment/docker/vmanomaly/vmanomaly-integration/vmanomaly_config.yml
+++ b/deployment/docker/vmanomaly/vmanomaly-integration/vmanomaly_config.yml
@@ -3,10 +3,11 @@ scheduler:
   fit_every: "2m"
   fit_window: "3h"
 
-model:
-  class: "model.prophet.ProphetModel"
-  args:
-    interval_width: 0.98
+models:
+  prophet:
+    class: "model.prophet.ProphetModel"
+    args:
+      interval_width: 0.98
 
 reader:
   datasource_url: "http://victoriametrics:8428/"
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 7b8c8c0c7..5c4266b7d 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -28,6 +28,10 @@ The sandbox cluster installation is running under the constant load generated by
 
 ## tip
 
+* FEATURE: [Single-node VictoriaMetrics](https://docs.victoriametrics.com/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/): expose `vm_last_partition_parts` [metrics](https://docs.victoriametrics.com/#monitoring), which show the number of [parts in the latest partition](https://docs.victoriametrics.com/#storage). These metrics may help debugging query performance slowdown related to the increased number of parts in the last partition, since usually all the ingested data is written to the last partition and all the queries are performed over the recently ingested data, e.g. the last partition.
+* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): support client-side TLS configuration for [InfluxDB](https://docs.victoriametrics.com/vmctl/#migrating-data-from-influxdb-1x). See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5748). Thanks to @khushijain21 [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5783).
+* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): support client-side TLS configuration for [Remote Read protocol](https://docs.victoriametrics.com/vmctl/#migrating-data-by-remote-read-protocol). See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5748). Thanks to @khushijain21 [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5798).
+
 ## [v1.98.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.98.0)
 
 Released at 2024-02-14
diff --git a/docs/anomaly-detection/CHANGELOG.md b/docs/anomaly-detection/CHANGELOG.md
index d70469500..b41574c5f 100644
--- a/docs/anomaly-detection/CHANGELOG.md
+++ b/docs/anomaly-detection/CHANGELOG.md
@@ -17,6 +17,23 @@ Please find the changelog for VictoriaMetrics Anomaly Detection below.
 
 > **Important note: Users are strongly encouraged to upgrade to `vmanomaly` [v1.9.2](https://hub.docker.com/repository/docker/victoriametrics/vmanomaly/tags?page=1&ordering=name) or later versions for optimal performance and accuracy. <br><br> This recommendation is crucial for configurations with a low `infer_every` parameter [in your scheduler](https://docs.victoriametrics.com/anomaly-detection/components/scheduler/#parameters-1), and in scenarios where data exhibits significant high-order seasonality patterns (such as hourly or daily cycles). Previous versions from v1.5.1 to v1.8.0 were identified to contain a critical issue impacting model training, where models were inadvertently trained on limited data subsets, leading to suboptimal fits, affecting the accuracy of anomaly detection. <br><br> Upgrading to v1.9.2 addresses this issue, ensuring proper model training and enhanced reliability. For users utilizing Helm charts, it is recommended to upgrade to version [1.0.0](https://github.com/VictoriaMetrics/helm-charts/blob/master/charts/victoria-metrics-anomaly/CHANGELOG.md#100).**
 
+## v1.10.0
+Released: 2024-02-15
+- FEATURE: Multi-model support. Now users can specify multiple [model specs](https://docs.victoriametrics.com/anomaly-detection/components/models/) in a single config (via aliasing), as well as to reference what [queries from VmReader](https://docs.victoriametrics.com/anomaly-detection/components/reader/?highlight=queries#config-parameters) it should be run on.
+  - Introduction of `queries` arg in model spec:
+    - It allows the model to be executed only on a particular query subset from `reader` section. 
+    - Passing an empty list or not specifying this param implies that each model is run on results from **all** queries, which is a backward-compatible behavior.
+  - Please find more details in docs on [Model section](https://docs.victoriametrics.com/anomaly-detection/components/models/#queries)
+
+- DEPRECATION: slight refactor of a model config section 
+  - Now models are passed as a mapping of `model_alias: model_spec` under [model](https://docs.victoriametrics.com/anomaly-detection/components/models/) sections. Using old format (<= [1.9.2](https://docs.victoriametrics.com/anomaly-detection/changelog/#v192)) will produce warnings for now and will be removed in future versions.
+  - Please find more details in docs on [Model section](https://docs.victoriametrics.com/anomaly-detection/components/models/)
+- IMPROVEMENT: now logs from [`monitoring.pull`](https://docs.victoriametrics.com/anomaly-detection/components/monitoring/#monitoring-section-config-example) GET requests to `/metrics` endpoint are shown only in DEBUG mode
+- IMPROVEMENT: labelset for multivariate models is deduplicated and cleaned, resulting in better UX
+
+> **Note**: These updates support more flexible setup and effective resource management in service, as now it's not longer needed to spawn several instances of `vmanomaly` to split queries/models context across.
+
+
 ## v1.9.2
 Released: 2024-01-29
 - BUGFIX: now multivariate models (like [`IsolationForestMultivariateModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#isolation-foresthttpsenwikipediaorgwikiisolation_forest-multivariate)) are properly handled throughout fit/infer phases.
diff --git a/docs/anomaly-detection/Overview.md b/docs/anomaly-detection/Overview.md
index 11498959a..71e8f0bcd 100644
--- a/docs/anomaly-detection/Overview.md
+++ b/docs/anomaly-detection/Overview.md
@@ -162,10 +162,11 @@ scheduler:
   fit_every: "2h"
   fit_window: "14d"
 
-model:
-  class: "model.prophet.ProphetModel"
-  args:
-    interval_width: 0.98
+models:
+  prophet:  # or use a model alias of your choice here
+    class: "model.prophet.ProphetModel"
+    args:
+      interval_width: 0.98
 
 reader:
   datasource_url: [YOUR_DATASOURCE_URL] #Example: "http://victoriametrics:8428/"
@@ -217,7 +218,7 @@ This will expose metrics at `http://0.0.0.0:8080/metrics` page.
 To use *vmanomaly* you need to pull docker image:
 
 ```sh
-docker pull victoriametrics/vmanomaly:v1.9.2
+docker pull victoriametrics/vmanomaly:v1.10.0
 ```
 
 > Note: please check what is latest release in [CHANGELOG](/anomaly-detection/CHANGELOG.html)
@@ -227,7 +228,7 @@ docker pull victoriametrics/vmanomaly:v1.9.2
 You can put a tag on it for your convinience:
 
 ```sh
-docker image tag victoriametrics/vmanomaly:v1.9.2 vmanomaly
+docker image tag victoriametrics/vmanomaly:v1.10.0 vmanomaly
 ```
 Here is an example of how to run *vmanomaly* docker container with [license file](#licensing):
 
diff --git a/docs/anomaly-detection/components/models.md b/docs/anomaly-detection/components/models.md
index c2dffda18..a190f55b8 100644
--- a/docs/anomaly-detection/components/models.md
+++ b/docs/anomaly-detection/components/models.md
@@ -16,10 +16,149 @@ aliases:
 
 # Models
 
-This section describes `Model` component of VictoriaMetrics Anomaly Detection (or simply [`vmanomaly`](/anomaly-detection/overview.html)) and the guide of how to define a respective section of a config to launch the service.
+This section describes `Models` component of VictoriaMetrics Anomaly Detection (or simply [`vmanomaly`](/anomaly-detection/overview.html)) and the guide of how to define a respective section of a config to launch the service.
 vmanomaly includes various [built-in models](#built-in-models) and you can integrate your custom model with vmanomaly see [custom model](#custom-model-guide) 
 
 
+> **Note: Starting from [v1.10.0](/anomaly-detection/changelog#v1100) model section in config supports multiple models via aliasing. <br>Also, `vmanomaly` expects model section to be named `models`. Using old (flat) format with `model` key is deprecated and will be removed in future versions. Having `model` and `models` sections simultaneously in a config will result in only `models` being used:**
+
+```yaml
+models:
+  model_univariate_1:
+    class: "model.zscore.ZscoreModel"
+    z_threshold: 2.5
+    queries: ["query_alias2"]  # referencing queries defined in `reader` section
+  model_multivariate_1:
+    class: "model.isolation_forest.IsolationForestMultivariateModel"
+    contamination: "auto"
+    args:
+      n_estimators: 100
+      # i.e. to assure reproducibility of produced results each time model is fit on the same input
+      random_state: 42
+    # if there is no explicit `queries` arg, then the model will be run on ALL queries found in reader section
+```  
+
+Old-style configs (< [1.10.0](/anomaly-detection/changelog#v1100) )
+
+```yaml
+model:
+    class: "model.zscore.ZscoreModel"
+    z_threshold: 2.5
+    # no explicit `queries` arg is provided
+```
+
+will be **implicitly** converted to
+
+```yaml
+models:
+  default_model:  # default model alias, backward compatibility
+    class: "model.zscore.ZscoreModel"
+    z_threshold: 2.5
+    # queries arg is created and propagated with all query aliases found in `queries` arg of `reader` section
+    queries: ["q1", "q2", "q3"]  # i.e., if your `queries` in `reader` section has exactly q1, q2, q3 aliases
+```
+
+
+## Common args
+
+From [1.10.0](/anomaly-detection/changelog#1100), **common args**, supported by *every model (and model type)* were introduced.
+
+### Queries
+
+Introduced in [1.10.0](/anomaly-detection/changelog#1100), as a part to support multi-model configs, `queries` arg is meant to define [queries from VmReader](https://docs.victoriametrics.com/anomaly-detection/components/reader/?highlight=queries#config-parameters) particular model should be run on (meaning, all the series returned by each of these queries will be used in such model for fitting and inferencing).
+
+`queries` arg is supported for all [the built-in](/anomaly-detection/components/models/#built-in-models) (as well as for [custom](/anomaly-detection/components/models/#custom-model-guide)) models.
+
+This arg is **backward compatible** - if there is no explicit `queries` arg, then the model, defined in a config, will be run on ALL queries found in reader section:
+
+```yaml
+models:
+  model_alias_1:
+    ...
+    # no explicit `queries` arg is provided
+```
+
+will be implicitly converted to
+
+```yaml
+models:
+  model_alias_1:
+    ...
+    # queries arg is created and propagated with all query aliases found in `queries` arg of `reader` section
+    queries: ["q1", "q2", "q3"]  # i.e., if your `queries` in `reader` section has exactly q1, q2, q3 aliases
+```
+
+## Model types
+
+There are **2 model types**, supported in `vmanomaly`, resulting in **4 possible combinations**:
+
+- [Univariate models](#univariate-models)
+- [Multivariate models](#multivariate-models)
+
+Each of these models can be
+- [rolling](#rolling-models)
+- [non-rolling](#non-rolling-models)
+
+### Univariate Models
+
+For a univariate type, **one separate model** is fit/used for inference per **each time series**, defined in its [queries](#queries) arg.
+
+For example, if you have some **univariate** model, defined to use 3 [MetricQL queries](https://docs.victoriametrics.com/metricsql/), each returning 5 time series, there will be 3*5=15 models created in total. Each such model produce **individual [output](#vmanomaly-output)** for each of time series. 
+
+If during an inference, you got a series having **new labelset** (not present in any of fitted models), the inference will be skipped until you get a model, trained particularly for such labelset during forthcoming re-fit step.
+
+**Implications:** Univariate models are a go-to default, when your queries returns **changing** amount of **individual** time series of **different** magnitude, [trend](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#trend) or [seasonality](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#seasonality), so you won't be mixing incompatible data with different behavior within a single fit model (context isolation).
+
+**Examples:** [Prophet](#prophet), [Holt-Winters](#holt-winters)
+
+<!-- TODO: add schema -->
+
+### Multivariate Models
+
+For a multivariate type, **one shared model** is fit/used for inference on **all time series** simultaneously, defined in its [queries](#queries) arg. 
+
+For example, if you have some **multivariate** model to use 3 [MetricQL queries](https://docs.victoriametrics.com/metricsql/), each returning 5 time series, there will be one shared model created in total. Once fit, this model will expect **exactly 15 time series with exact same labelsets as an input**. This model will produce **one shared [output](#vmanomaly-output)**.
+
+If during an inference, you got a **different amount of series** or some series having a **new labelset** (not present in any of fitted models), the inference will be skipped until you get a model, trained particularly for such labelset during forthcoming re-fit step. 
+
+**Implications:** Multivariate models are a go-to default, when your queries returns **fixed** amount of **individual** time series (say, some aggregations), to be used for adding cross-series (and cross-query) context, useful for catching [collective anomalies](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-2/index.html#collective-anomalies) or [novelties](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-2/index.html#novelties) (expanded to multi-input scenario). For example, you may set it up for anomaly detection of CPU usage in different modes (`idle`, `user`, `system`, etc.) and use its cross-dependencies to detect **unseen (in fit data)** behavior.
+
+**Examples:** [IsolationForest](#isolation-forest-multivariate)
+
+<!-- TODO: add schema -->
+
+### Rolling Models
+
+A rolling model is a model that, once trained, **cannot be (naturally) used to make inference on data, not seen during its fit phase**.
+
+An instance of rolling model is **simultaneously fit and used for inference** during its `infer` method call.
+
+As a result, such model instances are **not stored** between consecutive re-fit calls (defined by `fit_every` [arg](/anomaly-detection/components/scheduler/?highlight=fit_every#periodic-scheduler) in `PeriodicScheduler`), leading to **lower RAM** consumption.
+
+Such models put **more pressure** on your reader's source, i.e. if your model should be fit on large amount of data (say, 14 days with 1-minute resolution) and at the same time you have **frequent inference** (say, once per minute) on new chunks of data - that's because such models require (fit + infer) window of data to be fit first to be used later in each inference call.
+
+> **Note**: Rolling models require `fit_every` to be set equal to `infer_every` in your [PeriodicScheduler](/anomaly-detection/components/scheduler/?highlight=fit_every#periodic-scheduler).
+
+**Examples:** [RollingQuantile](#rolling-quantile)
+
+<!-- TODO: add schema -->
+
+### Non-Rolling Models
+
+Everything that is not classified as [rolling](#rolling-models). 
+
+Produced models can be explicitly used to **infer on data, not seen during its fit phase**, thus, it **doesn't require re-fit procedure**.
+
+Such models put **less pressure** on your reader's source, i.e. if you fit on large amount of data (say, 14 days with 1-minute resolution) but do it occasionally (say, once per day), at the same time you have **frequent inference**(say, once per minute) on new chunks of data
+
+> **Note**: However, it's still highly recommended, to keep your model up-to-date with tendencies found in your data as it evolves in time.
+
+Produced model instances are **stored in-memory** between consecutive re-fit calls (defined by `fit_every` [arg](/anomaly-detection/components/scheduler/?highlight=fit_every#periodic-scheduler) in `PeriodicScheduler`), leading to **higher RAM** consumption.
+
+**Examples:** [Prophet](#prophet)
+
+<!-- TODO: add schema -->
+
 ## Built-in Models 
 
 ### Overview
@@ -68,18 +207,19 @@ Depending on chosen `seasonality` parameter FB Prophet can return additional met
 *Config Example*
 
 ```yaml
-model:
-  class: "model.prophet.ProphetModel"
-  seasonalities:
-    - name: 'hourly'
-      period: 0.04166666666
-      fourier_order: 30
-  # Inner model args (key-value pairs) accepted by
-  # https://facebook.github.io/prophet/docs/quick_start.html#python-api
-  args:
-    # See https://facebook.github.io/prophet/docs/uncertainty_intervals.html
-    interval_width: 0.98
-    country_holidays: 'US'
+models:
+  your_desired_alias_for_a_model:
+    class: "model.prophet.ProphetModel"
+    seasonalities:
+      - name: 'hourly'
+        period: 0.04166666666
+        fourier_order: 30
+    # Inner model args (key-value pairs) accepted by
+    # https://facebook.github.io/prophet/docs/quick_start.html#python-api
+    args:
+      # See https://facebook.github.io/prophet/docs/uncertainty_intervals.html
+      interval_width: 0.98
+      country_holidays: 'US'
 ```
 
 
@@ -95,9 +235,10 @@ Resulting metrics of the model are described [here](#vmanomaly-output)
 
 
 ```yaml
-model:
-  class: "model.zscore.ZscoreModel"
-  z_threshold: 2.5
+models:
+  your_desired_alias_for_a_model:
+    class: "model.zscore.ZscoreModel"
+    z_threshold: 2.5
 ```
 
 
@@ -131,14 +272,15 @@ Used to compute "seasonal_periods" param for the model (e.g. '1D' or '1W').
 *Config Example*
 
 ```yaml
-model:
-  class: "model.holtwinters.HoltWinters"
-  seasonality: '1d'
-  frequency: '1h'
-  # Inner model args (key-value pairs) accepted by statsmodels.tsa.holtwinters.ExponentialSmoothing
-  args:
-    seasonal: 'add'
-    initialization_method: 'estimated'
+models:
+  your_desired_alias_for_a_model:
+    class: "model.holtwinters.HoltWinters"
+    seasonality: '1d'
+    frequency: '1h'
+    # Inner model args (key-value pairs) accepted by statsmodels.tsa.holtwinters.ExponentialSmoothing
+    args:
+      seasonal: 'add'
+      initialization_method: 'estimated'
 ```
 
 
@@ -156,9 +298,10 @@ The MAD model is a robust method for anomaly detection that is *less sensitive*
 
 
 ```yaml
-model:
-  class: "model.mad.MADModel"
-  threshold: 2.5
+models:
+  your_desired_alias_for_a_model:
+    class: "model.mad.MADModel"
+    threshold: 2.5
 ```
 
 
@@ -175,13 +318,13 @@ Resulting metrics of the model are described [here](#vmanomaly-output).
 *Config Example*
 
 ```yaml
-model:
-  class: "model.rolling_quantile.RollingQuantileModel"
-  quantile: 0.9
-  window_steps: 96
+models:
+  your_desired_alias_for_a_model:
+    class: "model.rolling_quantile.RollingQuantileModel"
+    quantile: 0.9
+    window_steps: 96
 ```
 
-
 Resulting metrics of the model are described [here](#vmanomaly-output).
 
 ### [Seasonal Trend Decomposition](https://en.wikipedia.org/wiki/Seasonal_adjustment)
@@ -198,9 +341,10 @@ Here we use Seasonal Decompose implementation from `statsmodels` [library](https
 
 
 ```yaml
-model:
-  class: "model.std.StdModel"
-  period: 2
+models:
+  your_desired_alias_for_a_model:
+    class: "model.std.StdModel"
+    period: 2
 ```
 
 
@@ -233,15 +377,16 @@ Here we use ARIMA implementation from `statsmodels` [library](https://www.statsm
 *Config Example*
 
 ```yaml
-model:
-  class: "model.arima.ArimaModel"
-  # ARIMA's (p,d,q) order
-  order: [1, 1, 0] 
-  z_threshold: 2.7
-  resample_freq: '1m'
-  # Inner model args (key-value pairs) accepted by statsmodels.tsa.arima.model.ARIMA
-  args:
-    trend: 'c'
+models:
+  your_desired_alias_for_a_model:
+    class: "model.arima.ArimaModel"
+    # ARIMA's (p,d,q) order
+    order: [1, 1, 0] 
+    z_threshold: 2.7
+    resample_freq: '1m'
+    # Inner model args (key-value pairs) accepted by statsmodels.tsa.arima.model.ARIMA
+    args:
+      trend: 'c'
 ```
 
 
@@ -264,14 +409,15 @@ Here we use Isolation Forest implementation from `scikit-learn` [library](https:
 
 
 ```yaml
-model:
-  # To use univariate model, substitute class argument with "model.isolation_forest.IsolationForestModel".
-  class: "model.isolation_forest.IsolationForestMultivariateModel"
-  contamination: "auto"
-  args:
-    n_estimators: 100
-    # i.e. to assure reproducibility of produced results each time model is fit on the same input
-    random_state: 42
+models:
+  your_desired_alias_for_a_model:
+    # To use univariate model, substitute class argument with "model.isolation_forest.IsolationForestModel".
+    class: "model.isolation_forest.IsolationForestMultivariateModel"
+    contamination: "auto"
+    args:
+      n_estimators: 100
+      # i.e. to assure reproducibility of produced results each time model is fit on the same input
+      random_state: 42
 ```
 
 
@@ -318,6 +464,8 @@ Here in this guide, we will
 
 ### 1. Custom model
 
+> **Note**: By default, each custom model is created as [**univariate**](#univariate-models) / [**non-rolling**](#non-rolling-models) model. If you want to override this behavior, define models inherited from `RollingModel` (to get a rolling model), or having `is_multivariate` class arg set to `True` (please refer to the code example below).
+
 We'll create `custom_model.py` file with `CustomModel` class that will inherit from vmanomaly `Model` base class.
 In the `CustomModel` class there should be three required methods - `__init__`, `fit` and `infer`:
 * `__init__` method should initiate parameters for the model.
@@ -327,7 +475,7 @@ In the `CustomModel` class there should be three required methods - `__init__`,
   super().__init__(**kwargs)
   ``` 
   to initialize the base class each model derives from
-* `fit` method should contain the model training process.
+* `fit` method should contain the model training process. Please be aware that for `RollingModel` defining `fit` method is not needed, as the whole fit/infer process should be defined completely in `infer` method.
 * `infer` should return Pandas.DataFrame object with model's inferences.
 
 For the sake of simplicity, the model in this example will return one of two values of `anomaly_score` - 0 or 1 depending on input parameter `percentage`.
@@ -340,6 +488,7 @@ import scipy.stats as st
 import logging
 
 from model.model import Model
+# from model.model import RollingModel  # inherit from it for your model to be of rolling type
 logger = logging.getLogger(__name__)
 
 
@@ -348,6 +497,10 @@ class CustomModel(Model):
     Custom model implementation.
     """
 
+    # by default, each `Model` will be created as a univariate one
+    # uncomment line below for it to be of multivariate type
+    # is_multivariate = True
+
     def __init__(self, percentage: float = 0.95, **kwargs):
         super().__init__(**kwargs)
         self.percentage = percentage
@@ -362,7 +515,6 @@ class CustomModel(Model):
         if self._std == 0.0:
             self._std = 1 / 65536
 
-
     def infer(self, df: pd.DataFrame) -> np.array:
         # Inference process:
         y = df['y']
@@ -373,7 +525,6 @@ class CustomModel(Model):
         df_pred['anomaly_score'] = df_pred['anomaly_score'].astype('int32', errors='ignore')
 
         return df_pred
-
 ```
 
 
@@ -381,8 +532,8 @@ class CustomModel(Model):
 ### 2. Configuration file
 
 Next, we need to create `config.yaml` file with VM Anomaly Detection configuration and model input parameters.
-In the config file `model` section we need to put our model class `model.custom.CustomModel` and all parameters used in `__init__` method.
-You can find out more about configuration parameters in vmanomaly docs.
+In the config file `models` section we need to put our model class `model.custom.CustomModel` and all parameters used in `__init__` method.
+You can find out more about configuration parameters in [vmanomaly config docs](/anomaly-detection/components/).
 
 
 ```yaml
@@ -391,11 +542,12 @@ scheduler:
   fit_every: "1m"
   fit_window: "1d"
 
-model:
-  # note: every custom model should implement this exact path, specified in `class` field
-  class: "model.model.CustomModel"
-  # custom model params are defined here
-  percentage: 0.9
+models:
+  your_desired_alias_for_a_model:
+    # note: every custom model should implement this exact path, specified in `class` field
+    class: "model.model.CustomModel"
+    # custom model params are defined here
+    percentage: 0.9
 
 reader:
   datasource_url: "http://localhost:8428/"
@@ -426,15 +578,13 @@ monitoring:
 ### 3. Running custom model
 Let's pull the docker image for vmanomaly:
 
-
 ```sh 
-docker pull us-docker.pkg.dev/victoriametrics-test/public/vmanomaly-trial:latest
+docker pull victoriametrics/vmanomaly:latest
 ```
 
-
 Now we can run the docker container putting as volumes both config and model file:
 
-**Note**: place the model file to `/model/custom.py` path when copying
+> **Note**: place the model file to `/model/custom.py` path when copying
 
 ```sh
 docker run -it \
@@ -442,11 +592,10 @@ docker run -it \
 -v [YOUR_LICENSE_FILE_PATH]:/license.txt \
 -v $(PWD)/custom_model.py:/vmanomaly/src/model/custom.py \
 -v $(PWD)/custom.yaml:/config.yaml \
-us-docker.pkg.dev/victoriametrics-test/public/vmanomaly-trial:latest /config.yaml \
+victoriametrics/vmanomaly:latest /config.yaml \
 --license-file=/license.txt
 ```
 
-
 Please find more detailed instructions (license, etc.) [here](/anomaly-detection/overview.html#run-vmanomaly-docker-container)
 
 
diff --git a/docs/anomaly-detection/components/monitoring.md b/docs/anomaly-detection/components/monitoring.md
index 2f9e239c1..ebb15ca9d 100644
--- a/docs/anomaly-detection/components/monitoring.md
+++ b/docs/anomaly-detection/components/monitoring.md
@@ -128,6 +128,8 @@ monitoring:
 ### Models Behaviour Metrics
 Label names [description](#labelnames)
 
+> **Note**: There is a new label key `model_alias` introduced in multi-model support [v1.10.0](/anomaly-detection/changelog/#v1100). This label key adjustment was made to preserve unique label set production during writing produced metrics back to VictoriaMetrics.
+
 <table>
     <thead>
         <tr>
@@ -142,25 +144,25 @@ Label names [description](#labelnames)
             <td><code>vmanomaly_model_runs</code></td>
             <td>Counter</td>
             <td>How many times models ran (per model)</td>
-            <td><code>stage, query_key</code></td>
+            <td><code>stage, query_key, model_alias</code></td>
         </tr>
         <tr>
             <td><code>vmanomaly_model_run_duration_seconds</code></td>
             <td>Summary</td>
             <td>How much time (in seconds) model invocations took</td>
-            <td><code>stage, query_key</code></td>
+            <td><code>stage, query_key, model_alias</code></td>
         </tr>
         <tr>
             <td><code>vmanomaly_model_datapoints_accepted</code></td>
             <td>Counter</td>
             <td>How many datapoints did models accept</td>
-            <td><code>stage, query_key</code></td>
+            <td><code>stage, query_key, model_alias</code></td>
         </tr>
         <tr>
             <td><code>vmanomaly_model_datapoints_produced</code></td>
             <td>Counter</td>
             <td>How many datapoints were generated by models</td>
-            <td><code>stage, query_key</code></td>
+            <td><code>stage, query_key, model_alias</code></td>
         </tr>
         <tr>
             <td><code>vmanomaly_models_active</code></td>
@@ -172,7 +174,7 @@ Label names [description](#labelnames)
             <td><code>vmanomaly_model_runs_skipped</code></td>
             <td>Counter</td>
             <td>How many times a run was skipped (per model)</td>
-            <td><code>stage, query_key</code></td>
+            <td><code>stage, query_key, model_alias</code></td>
         </tr>
     </tbody>
 </table>
@@ -286,6 +288,8 @@ Label names [description](#labelnames)
 
 <code>query_key</code> - query alias from [`reader`](/anomaly-detection/components/reader.html) config section.
 
+<code>model_alias</code> - model alias from [`models`](/anomaly-detection/components/models.html) config section. **Introduced in [v1.10.0](/anomaly-detection/changelog/#v1100).**
+
 <code>url</code> - writer or reader url endpoint.
 
 <code>code</code> - response status code or `connection_error`, `timeout`.
diff --git a/docs/anomaly-detection/guides/guide-vmanomaly-vmalert.md b/docs/anomaly-detection/guides/guide-vmanomaly-vmalert.md
index 5c76ed461..ce5a2a51f 100644
--- a/docs/anomaly-detection/guides/guide-vmanomaly-vmalert.md
+++ b/docs/anomaly-detection/guides/guide-vmanomaly-vmalert.md
@@ -36,7 +36,7 @@ aliases:
 
 All the service parameters are defined in a config file.
 
-> **Note**: As of the time of writing, in the [1.9.2](https://docs.victoriametrics.com/anomaly-detection/changelog/#v192) release and earlier versions, each `vmanomaly` configuration file is limited to supporting only one model type. To utilize *different models* on your data, it is necessary to run multiple instances of the `vmanomaly` process. Each instance should operate with its own configuration file, differing in the `model` section.
+> **Note**: Starting from [1.10.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1100), each `vmanomaly` configuration file can support more that one model type. To utilize *different models* on your data, it is no longer necessary to run multiple instances of the `vmanomaly` process. Please refer to [model](/anomaly-detection/models/) config section for more details.
 
 
 **vmanomaly** does the following:
@@ -117,7 +117,7 @@ The configuration file for `vmanomaly` comprises 4 essential sections:
 
 1. [`scheduler`](/anomaly-detection/components/scheduler.html) - This section determines the frequency of model inferences and training, including the time range for model training.
 
-2. [`model`](/anomaly-detection/components/models.html) - Here, you define specific parameters and configurations for the model being used for anomaly detection.
+2. [`models`](/anomaly-detection/components/models.html) - Here, you define specific parameters and configurations for the models being used for anomaly detection.
 
 3. [`reader`](/anomaly-detection/components/reader.html) - This section outlines the methodology for data reading, including the data source location.
 
@@ -132,7 +132,7 @@ Detailed parameters in each section:
   * `fit_every` - Sets the frequency for retraining the models. A higher frequency ensures more updated models but requires more CPU resources. If omitted, models are retrained in each `infer_every` cycle. Format is similar to `infer_every`.
   * `fit_window` - Defines the data interval for training the models. Longer intervals allow for capturing extensive historical behavior and better seasonal pattern detection but may slow down the model's response to permanent metric changes and increase resource consumption. A minimum of two full seasonal cycles is recommended. Example format: 3h for three hours of data.
 
-* `model`
+* `models`
   * `class` - Specifies the model to be used. Options include custom models ([guide here](/anomaly-detection/components/models.html#custom-model-guide)) or a selection from [built-in models](/anomaly-detection/components/models.html#built-in-models), such as the [Facebook Prophet](/anomaly-detection/components/models.html#prophet) (`model.prophet.ProphetModel`).
   * `args` - Model-specific parameters, formatted as a YAML dictionary in the `key: value` structure. Parameters available in [FB Prophet](https://facebook.github.io/prophet/docs/quick_start.html) can be used as an example.
 
@@ -152,10 +152,11 @@ scheduler:
   fit_every: "2m"
   fit_window: "3h"
 
-model:
-  class: "model.prophet.ProphetModel"
-  args:
-    interval_width: 0.98
+models:
+  prophet:
+    class: "model.prophet.ProphetModel"
+    args:
+      interval_width: 0.98
 
 reader:
   datasource_url: "http://victoriametrics:8428/"
diff --git a/docs/vmctl.md b/docs/vmctl.md
index a8e2ef488..135939237 100644
--- a/docs/vmctl.md
+++ b/docs/vmctl.md
@@ -50,7 +50,7 @@ COMMANDS:
 Each mode has its own unique set of flags specific (e.g. prefixed with `influx` for influx mode)
 to the data source and common list of flags for destination (prefixed with `vm` for VictoriaMetrics):
 
-```
+```sh
 $ ./vmctl influx --help
 OPTIONS:
    --influx-addr value              InfluxDB server addr (default: "http://localhost:8086")
@@ -118,14 +118,14 @@ OpenTSDB migration works like so:
 
 This means that we must stream data from OpenTSDB to VictoriaMetrics in chunks. This is where concurrency for OpenTSDB comes in. We can query multiple chunks at once, but we shouldn't perform too many chunks at a time to avoid overloading the OpenTSDB cluster.
 
-```
+```sh
 $ ./vmctl opentsdb --otsdb-addr http://opentsdb:4242/ --otsdb-retentions sum-1m-avg:1h:1d --otsdb-filters system --otsdb-normalize --vm-addr http://victoria:8428/
 OpenTSDB import mode
 2021/04/09 11:52:50 Will collect data starting at TS 1617990770
 2021/04/09 11:52:50 Loading all metrics from OpenTSDB for filters:  [system]
 Found 9 metrics to import. Continue? [Y/n]
 2021/04/09 11:52:51 Starting work on system.load1
-23 / 402200 [>____________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________] 0.01% 2 p/s
+23 / 402200 [>____________________________________________________________________________________________] 0.01% 2 p/s
 ```
 Where `:8428` is Prometheus port of VictoriaMetrics.
 
@@ -197,7 +197,7 @@ The time range `30d` simply means we are asking for the last 30 days of data. Th
 
 The resultant queries that will be created, based on our example retention string of `sum-1m-avg:1h:30d` look like this:
 
-```
+```sh
 http://opentsdb:4242/api/query?start=1h-ago&end=now&m=sum:1m-avg-none:<series>
 http://opentsdb:4242/api/query?start=2h-ago&end=1h-ago&m=sum:1m-avg-none:<series>
 http://opentsdb:4242/api/query?start=3h-ago&end=2h-ago&m=sum:1m-avg-none:<series>
@@ -232,7 +232,7 @@ VM importer then accumulates received samples in batches and sends import reques
 The importing process example for local installation of InfluxDB(`http://localhost:8086`)
 and single-node VictoriaMetrics(`http://localhost:8428`):
 
-```
+```sh
 ./vmctl influx --influx-database benchmark
 InfluxDB import mode
 2020/01/18 20:47:11 Exploring scheme for database "benchmark"
@@ -240,7 +240,7 @@ InfluxDB import mode
 2020/01/18 20:47:11 found 10 fields
 2020/01/18 20:47:11 fetching series: command: "show series "; database: "benchmark"; retention: "autogen"
 Found 40000 timeseries to import. Continue? [Y/n] y
-40000 / 40000 [-----------------------------------------------------------------------------------------------------------------------------------------------] 100.00% 21 p/s
+40000 / 40000 [----------------------------------------------------------------------------------------] 100.00% 21 p/s
 2020/01/18 21:19:00 Import finished!
 2020/01/18 21:19:00 VictoriaMetrics importer stats:
   idle duration: 13m51.461434876s;
@@ -280,7 +280,11 @@ foo_field2{tag1="value1", tag2="value2"} 40
 
 ### Configuration
 
-The configuration flags should contain self-explanatory descriptions.
+Run the following command to get all configuration options:
+```sh
+./vmctl influx --help
+```
+
 
 ### Filtering
 
@@ -289,7 +293,7 @@ The first step of application is to select all available timeseries
 for given database and retention. User may specify additional filtering
 condition via `--influx-filter-series` flag. For example:
 
-```
+```sh
 ./vmctl influx --influx-database benchmark \
   --influx-filter-series "on benchmark from cpu where hostname='host_1703'"
 InfluxDB import mode
@@ -339,7 +343,7 @@ VM worker 2:↙ 121616 samples/s
 VM worker 3:↙ 59164 samples/s                                                                                                                                                                        
 VM worker 4:↙ 59220 samples/s                                                                                                                                                                        
 VM worker 5:↙ 102072 samples/s                                                                                                                                                                       
-Processing ranges: 1 / 1 [██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████] 100.00%
+Processing ranges: 1 / 1 [████████████████████████████████████████████████████████████████████████████████████] 100.00%
 2023/08/21 16:11:55 Import finished!
 2023/08/21 16:11:55 VictoriaMetrics importer stats:
   idle duration: 0s;
@@ -386,7 +390,7 @@ The data processed in chunks and then sent to VM.
 The importing process example for local installation of Prometheus
 and single-node VictoriaMetrics(`http://localhost:8428`):
 
-```
+```sh
 ./vmctl prometheus --prom-snapshot=/path/to/snapshot \
   --vm-concurrency=1 \
   --vm-batch-size=200000 \
@@ -421,7 +425,11 @@ So no data changes will be applied.
 
 ### Configuration
 
-The configuration flags should contain self-explanatory descriptions.
+Run the following command to get all configuration options:
+```sh
+./vmctl prometheus --help
+```
+
 
 ### Filtering
 
@@ -433,7 +441,7 @@ overlapping time range.
 
 Example of applying time filter:
 
-```
+```sh
 ./vmctl prometheus --prom-snapshot=/path/to/snapshot \
   --prom-filter-time-start=2020-02-07T00:07:01Z \
   --prom-filter-time-end=2020-02-11T00:07:01Z
@@ -459,7 +467,7 @@ Filtering by timeseries is configured with following flags:
 
 For example:
 
-```
+```sh
 ./vmctl prometheus --prom-snapshot=/path/to/snapshot \
   --prom-filter-label="__name__" \
   --prom-filter-label-value="promhttp.*" \
@@ -474,7 +482,7 @@ Prometheus snapshot stats:
   samples: 1657698;
   series: 3930.
 Found 2 blocks to import. Continue? [Y/n] y
-14 / 14 [------------------------------------------------------------------------------------------------------------------------------------------------------] 100.00% ? p/s
+14 / 14 [-----------------------------------------------------------------------------------------------] 100.00% ? p/s
 2020/02/23 15:51:07 Import finished!
 2020/02/23 15:51:07 VictoriaMetrics importer stats:
   idle duration: 0s;
@@ -512,7 +520,7 @@ To start the migration process configure the following flags:
 The importing process example for local installation of Prometheus
 and single-node VictoriaMetrics(`http://localhost:8428`):
 
-```
+```sh
 ./vmctl remote-read \
 --remote-read-src-addr=http://<prometheus>:9091 \
 --remote-read-filter-time-start=2021-10-18T00:00:00Z \
@@ -527,7 +535,7 @@ VM worker 2:↘ 151606 samples/s
 VM worker 3:↘ 130765 samples/s
 VM worker 4:↘ 131904 samples/s
 VM worker 5:↘ 132693 samples/s
-Processing ranges: 8798 / 8798 [█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████] 100.00%
+Processing ranges: 8798 / 8798 [██████████████████████████████████████████████████████████████████████████████] 100.00%
 2022/10/19 16:45:37 Import finished!
 2022/10/19 16:45:37 VictoriaMetrics importer stats:
   idle duration: 6m57.793987511s;
@@ -569,7 +577,7 @@ and that you have a separate Thanos Store installation.
 1. For now, keep your Thanos Sidecar and Thanos-related Prometheus configuration, but add this to also stream
     metrics to VictoriaMetrics:
 
-    ```
+    ```yaml
     remote_write:
     - url: http://victoria-metrics:8428/api/v1/write
     ```
@@ -577,7 +585,7 @@ and that you have a separate Thanos Store installation.
 1. Make sure VM is running, of course. Now check the logs to make sure that Prometheus is sending and VM is receiving.
     In Prometheus, make sure there are no errors. On the VM side, you should see messages like this:
 
-    ```
+    ```sh
     2020-04-27T18:38:46.474Z info VictoriaMetrics/lib/storage/partition.go:207 creating a partition "2020_04" with smallPartsPath="/victoria-metrics-data/data/small/2020_04", bigPartsPath="/victoria-metrics-data/data/big/2020_04"
     2020-04-27T18:38:46.506Z info VictoriaMetrics/lib/storage/partition.go:222 partition "2020_04" has been created
     ```
@@ -618,7 +626,7 @@ When you run thanos-remote-read proxy, it exposes port to serve HTTP on `10080 b
 The importing process example for local installation of Thanos
 and single-node VictoriaMetrics(`http://localhost:8428`):
 
-```
+```sh
 ./vmctl remote-read \
 --remote-read-src-addr=http://127.0.0.1:10080 \
 --remote-read-filter-time-start=2021-10-18T00:00:00Z \
@@ -628,14 +636,14 @@ and single-node VictoriaMetrics(`http://localhost:8428`):
 ```
 
 On the [thanos-remote-read](https://github.com/G-Research/thanos-remote-read) proxy side you will see logs like:
-```
+```sh
 ts=2022-10-19T15:05:04.193916Z caller=main.go:278 level=info traceID=00000000000000000000000000000000 msg="thanos request" request="min_time:1666180800000 max_time:1666184399999 matchers:<type:RE value:\".*\" > aggregates:RAW "
 ts=2022-10-19T15:05:04.468852Z caller=main.go:278 level=info traceID=00000000000000000000000000000000 msg="thanos request" request="min_time:1666184400000 max_time:1666187999999 matchers:<type:RE value:\".*\" > aggregates:RAW "
 ts=2022-10-19T15:05:04.553914Z caller=main.go:278 level=info traceID=00000000000000000000000000000000 msg="thanos request" request="min_time:1666188000000 max_time:1666191364863 matchers:<type:RE value:\".*\" > aggregates:RAW "
 ```
 
 And when process will finish you will see:
-```
+```sh
 Split defined times into 8799 ranges to import. Continue? [Y/n]
 VM worker 0:↓ 98183 samples/s
 VM worker 1:↓ 114640 samples/s
@@ -643,7 +651,7 @@ VM worker 2:↓ 131710 samples/s
 VM worker 3:↓ 114256 samples/s
 VM worker 4:↓ 105671 samples/s
 VM worker 5:↓ 124000 samples/s
-Processing ranges: 8799 / 8799 [█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████] 100.00%
+Processing ranges: 8799 / 8799 [██████████████████████████████████████████████████████████████████████████████] 100.00%
 2022/10/19 18:05:07 Import finished!
 2022/10/19 18:05:07 VictoriaMetrics importer stats:
   idle duration: 52m13.987637229s;
@@ -681,7 +689,7 @@ When you run Cortex, it exposes a port to serve HTTP on `9009 by default`.
 The importing process example for the local installation of Cortex
 and single-node VictoriaMetrics(`http://localhost:8428`):
 
-```
+```sh
 ./vmctl remote-read \ 
 --remote-read-src-addr=http://127.0.0.1:9009/prometheus \
 --remote-read-filter-time-start=2021-10-18T00:00:00Z \
@@ -691,7 +699,7 @@ and single-node VictoriaMetrics(`http://localhost:8428`):
 ```
 And when the process finishes, you will see the following:
 
-```
+```sh
 Split defined times into 8842 ranges to import. Continue? [Y/n]
 VM worker 0:↗ 3863 samples/s
 VM worker 1:↗ 2686 samples/s
@@ -699,7 +707,7 @@ VM worker 2:↗ 2620 samples/s
 VM worker 3:↗ 2705 samples/s
 VM worker 4:↗ 2643 samples/s
 VM worker 5:↗ 2593 samples/s
-Processing ranges: 8842 / 8842 [█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████] 100.00%
+Processing ranges: 8842 / 8842 [█████████████████████████████████████████████████████████████████████████████] 100.00%
 2022/10/21 12:09:49 Import finished!
 2022/10/21 12:09:49 VictoriaMetrics importer stats:
   idle duration: 0s;
@@ -754,7 +762,7 @@ and single-node VictoriaMetrics(`http://localhost:8428`):
 
 And when the process finishes, you will see the following:
 
-```
+```sh
 Split defined times into 8847 ranges to import. Continue? [Y/n]
 VM worker 0:→ 12176 samples/s
 VM worker 1:→ 11918 samples/s
@@ -762,7 +770,7 @@ VM worker 2:→ 11261 samples/s
 VM worker 3:→ 12861 samples/s
 VM worker 4:→ 11096 samples/s
 VM worker 5:→ 11575 samples/s
-Processing ranges: 8847 / 8847 [█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████] 100.00%
+Processing ranges: 8847 / 8847 [█████████████████████████████████████████████████████████████████████████████] 100.00%
 2022/10/21 17:22:23 Import finished!
 2022/10/21 17:22:23 VictoriaMetrics importer stats:
   idle duration: 0s;
@@ -794,7 +802,7 @@ Migration in `vm-native` mode takes two steps:
 1. Explore the list of the metrics to migrate via `api/v1/label/__name__/values` API;
 1. Migrate explored metrics one-by-one.
 
-```
+```sh
 ./vmctl vm-native \
     --vm-native-src-addr=http://127.0.0.1:8481/select/0/prometheus \ # migrate from
     --vm-native-dst-addr=http://localhost:8428 \                     # migrate to
@@ -906,7 +914,7 @@ VictoriaMetrics Native import mode
 2023/03/02 09:18:05 Exploring metrics...
 Found 9 metrics to import. Continue? [Y/n] 
 2023/03/02 09:18:07 Selected time range will be split into 5 ranges according to "month" step. Requests to make: 45.
-Requests to make: 45 / 45 [█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████] 100.00%
+Requests to make: 45 / 45 [██████████████████████████████████████████████████████████████████████████████████] 100.00%
 2023/03/02 09:18:12 Import finished!
 2023/03/02 09:18:12 VictoriaMetrics importer stats:
   time spent while importing: 7.111870667s;
@@ -941,7 +949,7 @@ VictoriaMetrics Native import mode
 2023/02/28 10:41:42 Exploring metrics...
 2023/02/28 10:41:42 Found 1 metrics to import 
 2023/02/28 10:41:42 Selected time range will be split into 28 ranges according to "day" step. 
-Requests to make for tenant 0:0: 28 / 28 [███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████] 100.00%
+Requests to make for tenant 0:0: 28 / 28 [████████████████████████████████████████████████████████████████████] 100.00%
 
 2023/02/28 10:41:45 Initing import process from "http://127.0.0.1:8481/select/1:0/prometheus/api/v1/export/native" to "http://127.0.0.1:8480/insert/1:0/prometheus/api/v1/import/native" with filter 
         filter: match[]={__name__="vm_app_uptime_seconds"}
@@ -949,7 +957,7 @@ Requests to make for tenant 0:0: 28 / 28 [████████████
 2023/02/28 10:41:45 Exploring metrics...
 2023/02/28 10:41:45 Found 1 metrics to import 
 2023/02/28 10:41:45 Selected time range will be split into 28 ranges according to "day" step. Requests to make: 28 
-Requests to make for tenant 1:0: 28 / 28 [████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████] 100.00%
+Requests to make for tenant 1:0: 28 / 28 [████████████████████████████████████████████████████████████████████] 100.00%
 
 ...
 
diff --git a/lib/httputils/duration.go b/lib/httputils/duration.go
index 4fdd05c4a..8a5e3887f 100644
--- a/lib/httputils/duration.go
+++ b/lib/httputils/duration.go
@@ -29,7 +29,7 @@ func GetDuration(r *http.Request, argKey string, defaultValue int64) (int64, err
 	}
 	msecs := int64(secs * 1e3)
 	if msecs <= 0 || msecs > maxDurationMsecs {
-		return 0, fmt.Errorf("%q=%dms is out of allowed range [%d ... %d]", argKey, msecs, 0, int64(maxDurationMsecs))
+		return 0, fmt.Errorf("%q=%dms is out of allowed range [%d ... %d]", argKey, msecs, 1, int64(maxDurationMsecs))
 	}
 	return msecs, nil
 }
diff --git a/lib/storage/table.go b/lib/storage/table.go
index 1dafbbc87..9a780d2ea 100644
--- a/lib/storage/table.go
+++ b/lib/storage/table.go
@@ -217,17 +217,34 @@ func (tb *table) NotifyReadWriteMode() {
 type TableMetrics struct {
 	partitionMetrics
 
+	// LastPartition contains metrics for the last partition.
+	// These metrics are important, since the majority of data ingestion
+	// and querying goes to the last partition.
+	LastPartition partitionMetrics
+
 	PartitionsRefCount uint64
 }
 
 // UpdateMetrics updates m with metrics from tb.
 func (tb *table) UpdateMetrics(m *TableMetrics) {
-	tb.ptwsLock.Lock()
-	for _, ptw := range tb.ptws {
+	ptws := tb.GetPartitions(nil)
+	defer tb.PutPartitions(ptws)
+
+	for _, ptw := range ptws {
 		ptw.pt.UpdateMetrics(&m.partitionMetrics)
 		m.PartitionsRefCount += atomic.LoadUint64(&ptw.refCount)
 	}
-	tb.ptwsLock.Unlock()
+
+	// Collect separate metrics for the last partition.
+	if len(ptws) > 0 {
+		ptwLast := ptws[0]
+		for _, ptw := range ptws[1:] {
+			if ptw.pt.tr.MinTimestamp > ptwLast.pt.tr.MinTimestamp {
+				ptwLast = ptw
+			}
+		}
+		ptwLast.pt.UpdateMetrics(&m.LastPartition)
+	}
 }
 
 // ForceMergePartitions force-merges partitions in tb with names starting from the given partitionNamePrefix.
diff --git a/lib/uint64set/uint64set.go b/lib/uint64set/uint64set.go
index 37c5417ac..ff827693d 100644
--- a/lib/uint64set/uint64set.go
+++ b/lib/uint64set/uint64set.go
@@ -814,21 +814,23 @@ func (b *bucket16) add(x uint16) bool {
 }
 
 func (b *bucket16) addMulti(a []uint64) int {
-	count := 0
 	if b.bits == nil {
+		if b.smallPoolLen+len(a) > len(b.smallPool) {
+			b.switchSmallPoolToBits()
+			goto fastPath
+		}
 		// Slow path
-		for i, x := range a {
-			if b.add(uint16(x)) {
+		count := 0
+		for _, x := range a {
+			if b.addToSmallPool(uint16(x)) {
 				count++
 			}
-			if b.bits != nil {
-				a = a[i+1:]
-				goto fastPath
-			}
 		}
 		return count
 	}
+
 fastPath:
+	count := 0
 	bits := b.bits
 	for _, x := range a {
 		wordNum, bitMask := getWordNumBitMask(uint16(x))
@@ -850,14 +852,19 @@ func (b *bucket16) addToSmallPool(x uint16) bool {
 		b.smallPoolLen++
 		return true
 	}
+	b.switchSmallPoolToBits()
+	b.add(x)
+	return true
+}
+
+func (b *bucket16) switchSmallPoolToBits() {
+	smallPoolLen := b.smallPoolLen
 	b.smallPoolLen = 0
 	var bits [wordsPerBucket]uint64
 	b.bits = &bits
-	for _, v := range sp[:] {
+	for _, v := range b.smallPool[:smallPoolLen] {
 		b.add(v)
 	}
-	b.add(x)
-	return true
 }
 
 func (b *bucket16) has(x uint16) bool {


Test patch:
diff --git a/app/vmctl/utils/time_test.go b/app/vmctl/time_test.go
similarity index 98%
rename from app/vmctl/utils/time_test.go
rename to app/vmctl/time_test.go
index fde14e1e2..989162078 100644
--- a/app/vmctl/utils/time_test.go
+++ b/app/vmctl/time_test.go
@@ -1,4 +1,4 @@
-package utils
+package main
 
 import (
 	"testing"
@@ -165,7 +165,7 @@ func TestGetTime(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := GetTime(tt.s)
+			got, err := parseTime(tt.s)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("ParseTime() error = %v, wantErr %v", err, tt.wantErr)
 				return
diff --git a/lib/uint64set/uint64set_timing_test.go b/lib/uint64set/uint64set_timing_test.go
index ebd981d01..5055fbc59 100644
--- a/lib/uint64set/uint64set_timing_test.go
+++ b/lib/uint64set/uint64set_timing_test.go
@@ -9,7 +9,7 @@ import (
 )
 
 func BenchmarkAddMulti(b *testing.B) {
-	for _, itemsCount := range []int{1e3, 1e4, 1e5, 1e6, 1e7} {
+	for _, itemsCount := range []int{1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7} {
 		start := uint64(time.Now().UnixNano())
 		sa := createRangeSet(start, itemsCount)
 		a := sa.AppendTo(nil)