diff --git a/go.mod b/go.mod index 5673807e28..90d16363fb 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( github.com/jellydator/ttlcache/v3 v3.4.1 github.com/jinzhu/now v1.1.5 github.com/justinas/alice v1.2.0 - github.com/kovidgoyal/imaging v1.8.23 + github.com/kovidgoyal/imaging v1.9.0 github.com/leonelquinteros/gotext v1.7.3-0.20260422134830-b012b4ccae69 github.com/libregraph/idm v0.5.0 github.com/libregraph/lico v0.67.0 @@ -59,7 +59,7 @@ require ( github.com/mna/pigeon v1.3.0 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/nats-io/nats-server/v2 v2.15.0 - github.com/nats-io/nats.go v1.54.0 + github.com/nats-io/nats.go v1.55.0 github.com/olekukonko/errors v1.3.0 github.com/olekukonko/tablewriter v1.1.5 github.com/onsi/ginkgo v1.16.5 @@ -73,7 +73,7 @@ require ( github.com/orcaman/concurrent-map v1.0.0 github.com/pkg/errors v0.9.1 github.com/pkg/xattr v0.4.12 - github.com/prometheus/client_golang v1.24.1 + github.com/prometheus/client_golang v1.25.0 github.com/prometheus/client_model v0.6.3 github.com/r3labs/sse/v2 v2.10.0 github.com/riandyrn/otelchi v0.12.3 @@ -319,8 +319,8 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/pquerna/cachecontrol v0.2.0 // indirect - github.com/prometheus/common v0.70.1 // indirect - github.com/prometheus/procfs v0.21.1 // indirect + github.com/prometheus/common v0.72.0 // indirect + github.com/prometheus/procfs v0.22.0 // indirect github.com/prometheus/statsd_exporter v0.22.8 // indirect github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect github.com/rs/xid v1.6.0 // indirect diff --git a/go.sum b/go.sum index b0a33323e5..c66e3cb7fe 100644 --- a/go.sum +++ b/go.sum @@ -597,8 +597,8 @@ github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811ui github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw= github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo= github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT6t3+VnZds= -github.com/kovidgoyal/imaging v1.8.23 h1:4WsboyQ/E8tic2kX49P93Rvscg33SPsCwTBG5EZZF54= -github.com/kovidgoyal/imaging v1.8.23/go.mod h1:k3Iot3H0v2rSWXIxQISfT3e9wIryqzAHLHCuhG7z9lM= +github.com/kovidgoyal/imaging v1.9.0 h1:TxHvhClPANvDW7eLm7PYDTZU1KE15xbqlFv95DA6DmY= +github.com/kovidgoyal/imaging v1.9.0/go.mod h1:t2vWMc3uISm/jMP87nBCcZUcAK6EDUdgumwaXSuz0Jc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -723,8 +723,8 @@ github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc= github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc= github.com/nats-io/nats-server/v2 v2.15.0 h1:M99yf0y05rTr46/qc/Is6ZAowI58Ryp2SjufLCUeVJc= github.com/nats-io/nats-server/v2 v2.15.0/go.mod h1:5qLF4CDGzZVFt//3fUrY1ePpwbi05r7QHPNroSUtolk= -github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= -github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nats.go v1.55.0 h1:Ng8QKHdvdHIWoaiq0+d/aH1BWkxvcJxWJ0Y/9BPKx3M= +github.com/nats-io/nats.go v1.55.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= @@ -815,8 +815,8 @@ github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqr github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= -github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= -github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= +github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKqbYtNOXuiK0= +github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.0.0-20170216185247-6f3806018612/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -831,8 +831,8 @@ github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9 github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.35.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= -github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= +github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= +github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= github.com/prometheus/procfs v0.0.0-20170703101242-e645f4e5aaa8/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= @@ -840,8 +840,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= -github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= +github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= +github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= github.com/prometheus/statsd_exporter v0.22.8 h1:Qo2D9ZzaQG+id9i5NYNGmbf1aa/KxKbB9aKfMS+Yib0= github.com/prometheus/statsd_exporter v0.22.8/go.mod h1:/DzwbTEaFTE0Ojz5PqcSk6+PFHOPWGxdXVr6yC8eFOM= diff --git a/vendor/github.com/kovidgoyal/imaging/apng/reader.go b/vendor/github.com/kovidgoyal/imaging/apng/reader.go index e1566b75f6..7ad5303d80 100644 --- a/vendor/github.com/kovidgoyal/imaging/apng/reader.go +++ b/vendor/github.com/kovidgoyal/imaging/apng/reader.go @@ -847,6 +847,10 @@ func (d *decoder) mergePassInto(dst image.Image, src image.Image, pass int) { srcPix = src.(*image.NRGBA64).Pix dstPix, stride, rect = target.Pix, target.Stride, target.Rect bytesPerPixel = 8 + case *nrgb.Image: + srcPix = src.(*nrgb.Image).Pix + dstPix, stride, rect = target.Pix, target.Stride, target.Rect + bytesPerPixel = 3 case *image.Paletted: source := src.(*image.Paletted) srcPix = source.Pix @@ -859,14 +863,14 @@ func (d *decoder) mergePassInto(dst image.Image, src image.Image, pass int) { // same adjustment here. target.Palette = source.Palette } - case *image.RGBA: - srcPix = src.(*image.RGBA).Pix - dstPix, stride, rect = target.Pix, target.Stride, target.Rect - bytesPerPixel = 4 case *image.RGBA64: srcPix = src.(*image.RGBA64).Pix dstPix, stride, rect = target.Pix, target.Stride, target.Rect bytesPerPixel = 8 + default: + // Every image type readImagePass can allocate must be handled above, + // otherwise the pass would be silently dropped. + panic(fmt.Sprintf("apng: mergePassInto: unsupported image type %T", dst)) } s, bounds := 0, src.Bounds() for y := bounds.Min.Y; y < bounds.Max.Y; y++ { diff --git a/vendor/github.com/kovidgoyal/imaging/color_space_conversion.go b/vendor/github.com/kovidgoyal/imaging/color_space_conversion.go index 3d9c376fb3..6ace2a25e1 100644 --- a/vendor/github.com/kovidgoyal/imaging/color_space_conversion.go +++ b/vendor/github.com/kovidgoyal/imaging/color_space_conversion.go @@ -35,6 +35,9 @@ func convert(tr *icc.Pipeline, image_any image.Image) (ans image.Image, err erro b := image_any.Bounds() width, height := b.Dx(), b.Dy() ans = image_any + if width < 1 || height < 1 { + return + } var f func(start, limit int) switch img := image_any.(type) { case *NRGB: @@ -203,8 +206,8 @@ func convert(tr *icc.Pipeline, image_any image.Image) (ans image.Image, err erro r, g, bb, _ := color.YCbCr{img.Y[iy], img.Cb[ic], img.Cr[ic]}.RGBA() fr, fg, fb := t(f16(uint16(r)), f16(uint16(g)), f16(uint16(bb))) rr[0], rr[1], rr[2] = f8i(fr), f8i(fg), f8i(fb) - row = row[4:] } + row = row[4:] } } } @@ -216,7 +219,7 @@ func convert(tr *icc.Pipeline, image_any image.Image) (ans image.Image, err erro if a16 != 0 { fr, fg, fb := unpremultiply(r16, a16), unpremultiply(g16, a16), unpremultiply(b16, a16) fr, fg, fb = t(fr, fg, fb) - img.Set(x, y, &color.NRGBA64{R: f16i(fr), G: f16i(fg), B: f16i(fb)}) + img.Set(x, y, &color.NRGBA64{R: f16i(fr), G: f16i(fg), B: f16i(fb), A: uint16(a16)}) } } } @@ -229,12 +232,12 @@ func convert(tr *icc.Pipeline, image_any image.Image) (ans image.Image, err erro row := d.Pix[d.Stride*y:] for x := range width { r16, g16, b16, a16 := img.At(x+b.Min.X, y+b.Min.Y).RGBA() + s := row[0:8:8] + row = row[8:] if a16 != 0 { fr, fg, fb := unpremultiply(r16, a16), unpremultiply(g16, a16), unpremultiply(b16, a16) fr, fg, fb = t(fr, fg, fb) r, g, b := f16i(fr), f16i(fg), f16i(fb) - s := row[0:8:8] - row = row[8:] s[0], s[1] = uint8(r>>8), uint8(r) s[2], s[3] = uint8(g>>8), uint8(g) s[4], s[5] = uint8(b>>8), uint8(b) @@ -248,3 +251,205 @@ func convert(tr *icc.Pipeline, image_any image.Image) (ans image.Image, err erro err = parallel.Run_in_parallel_over_range(0, f, 0, height) return } + +// round and clamp normalized values +func f8r(x float64) uint8 { return uint8(max(0, min(x, 1))*math.MaxUint8 + 0.5) } +func f16r(x float64) uint16 { return uint16(max(0, min(x, 1))*math.MaxUint16 + 0.5) } + +// luminance as used by color.GrayModel, it is exact for pixels with r == g == b +func gray8(r, g, b uint8) uint8 { + return uint8((19595*uint32(r) + 38470*uint32(g) + 7471*uint32(b) + 1<<15) >> 16) +} + +func gray16(r, g, b uint32) uint16 { + return uint16((19595*r + 38470*g + 7471*b + 1<<15) >> 16) +} + +func new_gray_lut8(tr *icc.Pipeline) (ans *[256][3]uint8) { + ans = &[256][3]uint8{} + for i := range ans { + r, g, b := tr.Transform(f8(uint8(i)), 0, 0) + ans[i] = [3]uint8{f8r(r), f8r(g), f8r(b)} + } + return +} + +// Return a function to map 16 bit gray values to 16 bit sRGB. Uses a lookup +// table when the number of pixels is large enough to make it worthwhile. +func new_gray_mapper16(tr *icc.Pipeline, num_pixels int) func(uint16) [3]uint16 { + direct := func(v uint16) [3]uint16 { + r, g, b := tr.Transform(f16(v), 0, 0) + return [3]uint16{f16r(r), f16r(g), f16r(b)} + } + if num_pixels < 1<<16 { + return direct + } + lut := make([][3]uint16, 1<<16) + _ = parallel.Run_in_parallel_over_range(0, func(start, limit int) { + for i := start; i < limit; i++ { + lut[i] = direct(uint16(i)) + } + }, 0, len(lut)) + return func(v uint16) [3]uint16 { return lut[v] } +} + +// Convert an image whose pixel values are in the device space of a +// monochrome profile to sRGB. tr must transform a single gray channel to +// sRGB. Any color information in the pixels is discarded, the gray value used +// is the luminance of the pixel. +func convert_gray(tr *icc.Pipeline, image_any image.Image) (ans image.Image, err error) { + b := image_any.Bounds() + width, height := b.Dx(), b.Dy() + if width < 1 || height < 1 { + return image_any, nil + } + ans = image_any + var f func(start, limit int) + switch img := image_any.(type) { + case *image.Gray: + lut := new_gray_lut8(tr) + d := nrgb.NewNRGB(b) + ans = d + f = func(start, limit int) { + for y := start; y < limit; y++ { + row := img.Pix[img.Stride*y : img.Stride*y+width] + drow := d.Pix[d.Stride*y : d.Stride*y+3*width] + for x, v := range row { + c := lut[v] + s := drow[3*x : 3*x+3 : 3*x+3] + s[0], s[1], s[2] = c[0], c[1], c[2] + } + } + } + case *image.Gray16: + m := new_gray_mapper16(tr, width*height) + d := image.NewNRGBA64(b) + ans = d + f = func(start, limit int) { + for y := start; y < limit; y++ { + row := img.Pix[img.Stride*y : img.Stride*y+2*width] + drow := d.Pix[d.Stride*y : d.Stride*y+8*width] + for x := range width { + c := m(uint16(row[2*x])<<8 | uint16(row[2*x+1])) + s := drow[8*x : 8*x+8 : 8*x+8] + s[0], s[1] = uint8(c[0]>>8), uint8(c[0]) + s[2], s[3] = uint8(c[1]>>8), uint8(c[1]) + s[4], s[5] = uint8(c[2]>>8), uint8(c[2]) + s[6], s[7] = 0xff, 0xff + } + } + } + case *NRGB: + lut := new_gray_lut8(tr) + f = func(start, limit int) { + for y := start; y < limit; y++ { + row := img.Pix[img.Stride*y : img.Stride*y+3*width] + for x := range width { + s := row[3*x : 3*x+3 : 3*x+3] + c := lut[gray8(s[0], s[1], s[2])] + s[0], s[1], s[2] = c[0], c[1], c[2] + } + } + } + case *image.NRGBA: + // this is what gray + alpha images are decoded as + lut := new_gray_lut8(tr) + f = func(start, limit int) { + for y := start; y < limit; y++ { + row := img.Pix[img.Stride*y : img.Stride*y+4*width] + for x := range width { + s := row[4*x : 4*x+3 : 4*x+3] + c := lut[gray8(s[0], s[1], s[2])] + s[0], s[1], s[2] = c[0], c[1], c[2] + } + } + } + case *image.NRGBA64: + // this is what 16 bit gray + alpha images are decoded as + m := new_gray_mapper16(tr, width*height) + f = func(start, limit int) { + for y := start; y < limit; y++ { + row := img.Pix[img.Stride*y : img.Stride*y+8*width] + for x := range width { + s := row[8*x : 8*x+6 : 8*x+6] + c := m(gray16(uint32(s[0])<<8|uint32(s[1]), uint32(s[2])<<8|uint32(s[3]), uint32(s[4])<<8|uint32(s[5]))) + s[0], s[1] = uint8(c[0]>>8), uint8(c[0]) + s[2], s[3] = uint8(c[1]>>8), uint8(c[1]) + s[4], s[5] = uint8(c[2]>>8), uint8(c[2]) + } + } + } + case *image.Paletted: + for i, c := range img.Palette { + r, g, b, a := c.RGBA() + if a != 0 { + v := gray16(uint32(unpremultiply(r, a)*math.MaxUint16+0.5), uint32(unpremultiply(g, a)*math.MaxUint16+0.5), uint32(unpremultiply(b, a)*math.MaxUint16+0.5)) + fr, fg, fb := tr.Transform(f16(v), 0, 0) + img.Palette[i] = color.NRGBA64{R: f16r(fr), G: f16r(fg), B: f16r(fb), A: uint16(a)} + } + } + return + case *image.YCbCr: + // Y is the gray value + lut := new_gray_lut8(tr) + d := nrgb.NewNRGB(b) + ans = d + f = func(start, limit int) { + for y := start; y < limit; y++ { + row := img.Y[img.YStride*y : img.YStride*y+width] + drow := d.Pix[d.Stride*y : d.Stride*y+3*width] + for x, v := range row { + c := lut[v] + s := drow[3*x : 3*x+3 : 3*x+3] + s[0], s[1], s[2] = c[0], c[1], c[2] + } + } + } + case *image.NYCbCrA: + lut := new_gray_lut8(tr) + d := image.NewNRGBA(b) + ans = d + f = func(start, limit int) { + for y := start; y < limit; y++ { + row := img.Y[img.YStride*y : img.YStride*y+width] + arow := img.A[img.AStride*y : img.AStride*y+width] + drow := d.Pix[d.Stride*y : d.Stride*y+4*width] + for x, v := range row { + c := lut[v] + s := drow[4*x : 4*x+4 : 4*x+4] + s[0], s[1], s[2], s[3] = c[0], c[1], c[2], arow[x] + } + } + } + case *image.CMYK: + return nil, fmt.Errorf("cannot apply a monochrome color profile to a CMYK image") + default: + // This handles premultiplied images like RGBA and RGBA64 as well + m := new_gray_mapper16(tr, width*height) + d := image.NewNRGBA64(b) + ans = d + f = func(start, limit int) { + for y := start; y < limit; y++ { + drow := d.Pix[d.Stride*y : d.Stride*y+8*width] + for x := range width { + r, g, bb, a := img.At(x+b.Min.X, y+b.Min.Y).RGBA() + if a == 0 { + continue + } + v := uint16(r) + if a != math.MaxUint16 || r != g || g != bb { + v = gray16(uint32(unpremultiply(r, a)*math.MaxUint16+0.5), uint32(unpremultiply(g, a)*math.MaxUint16+0.5), uint32(unpremultiply(bb, a)*math.MaxUint16+0.5)) + } + c := m(v) + s := drow[8*x : 8*x+8 : 8*x+8] + s[0], s[1] = uint8(c[0]>>8), uint8(c[0]) + s[2], s[3] = uint8(c[1]>>8), uint8(c[1]) + s[4], s[5] = uint8(c[2]>>8), uint8(c[2]) + s[6], s[7] = uint8(a>>8), uint8(a) + } + } + } + } + err = parallel.Run_in_parallel_over_range(0, f, 0, height) + return +} diff --git a/vendor/github.com/kovidgoyal/imaging/color_space_conversion_api.go b/vendor/github.com/kovidgoyal/imaging/color_space_conversion_api.go index 55b76872a7..1c2fb5c145 100644 --- a/vendor/github.com/kovidgoyal/imaging/color_space_conversion_api.go +++ b/vendor/github.com/kovidgoyal/imaging/color_space_conversion_api.go @@ -17,6 +17,13 @@ func ConvertToSRGB(p *icc.Profile, intent icc.RenderingIntent, use_blackpoint_co if p.IsSRGB() { return image_any, nil } + if p.Header.DataColorSpace == icc.ColorSpaceGray { + tr, err := p.CreateTransformerToSRGB(intent, use_blackpoint_compensation, 1, true, true, true) + if err != nil { + return nil, err + } + return convert_gray(tr, image_any) + } num_channels := 3 if _, is_cmyk := image_any.(*image.CMYK); is_cmyk { num_channels = 4 diff --git a/vendor/github.com/kovidgoyal/imaging/colorconv/colorconv.go b/vendor/github.com/kovidgoyal/imaging/colorconv/colorconv.go index 3aa3e7b14e..87f7af57ea 100644 --- a/vendor/github.com/kovidgoyal/imaging/colorconv/colorconv.go +++ b/vendor/github.com/kovidgoyal/imaging/colorconv/colorconv.go @@ -23,7 +23,56 @@ import ( type Vec3 [3]float64 type Mat3 [3][3]float64 -var whiteD65 = Vec3{0.95047, 1.00000, 1.08883} +// The sRGB primaries and white point chromaticities from IEC 61966-2-1, the +// matrices and D65 white point are derived from these at full precision, the +// same way lcms does in cmsCreate_sRGBProfile() +var srgb_primaries = [3][2]float64{{0.64, 0.33}, {0.30, 0.60}, {0.15, 0.06}} + +const d65_x, d65_y = 0.3127, 0.3290 + +var whiteD65 = xy_to_XYZ(d65_x, d65_y) + +// linear sRGB to XYZ (D65) and its inverse +var srgbToXYZMatrix, xyzToSRGBMatrix = srgb_matrices() + +func xy_to_XYZ(x, y float64) Vec3 { return Vec3{x / y, 1, (1 - x - y) / y} } + +func srgb_matrices() (to_xyz, from_xyz Mat3) { + // columns are the XYZ of the primaries scaled so that RGB = 1 gives the white point + var p Mat3 + for c, xy := range srgb_primaries { + v := xy_to_XYZ(xy[0], xy[1]) + for r := range 3 { + p[r][c] = v[r] + } + } + pi, ok := invertMat3(p) + if !ok { + panic("sRGB primaries matrix is not invertible") + } + sr, sg, sb := mulMat3Vec(pi, whiteD65) + for r := range 3 { + to_xyz[r] = [3]float64{p[r][0] * sr, p[r][1] * sg, p[r][2] * sb} + } + if from_xyz, ok = invertMat3(to_xyz); !ok { + panic("sRGB matrix is not invertible") + } + return +} + +func invertMat3(m Mat3) (ans Mat3, ok bool) { + det := m[0][0]*(m[1][1]*m[2][2]-m[1][2]*m[2][1]) - m[0][1]*(m[1][0]*m[2][2]-m[1][2]*m[2][0]) + m[0][2]*(m[1][0]*m[2][1]-m[1][1]*m[2][0]) + if det == 0 { + return ans, false + } + inv := 1 / det + ans = Mat3{ + {(m[1][1]*m[2][2] - m[1][2]*m[2][1]) * inv, (m[0][2]*m[2][1] - m[0][1]*m[2][2]) * inv, (m[0][1]*m[1][2] - m[0][2]*m[1][1]) * inv}, + {(m[1][2]*m[2][0] - m[1][0]*m[2][2]) * inv, (m[0][0]*m[2][2] - m[0][2]*m[2][0]) * inv, (m[0][2]*m[1][0] - m[0][0]*m[1][2]) * inv}, + {(m[1][0]*m[2][1] - m[1][1]*m[2][0]) * inv, (m[0][1]*m[2][0] - m[0][0]*m[2][1]) * inv, (m[0][0]*m[1][1] - m[0][1]*m[1][0]) * inv}, + } + return ans, true +} func (m *Mat3) String() string { return fmt.Sprintf("Matrix3{ %.6v %.6v %.6v }", m[0], m[1], m[2]) @@ -56,10 +105,11 @@ func NewConvertColor(whitepoint_x, whitepoint_y, whitepoint_z, scale float64) (a ans = &ConvertColor{whitepoint: Vec3{whitepoint_x, whitepoint_y, whitepoint_z}} adapt := chromaticAdaptationMatrix(ans.whitepoint, whiteD65) // sRGB (linear) transform matrix from CIE XYZ (D65) - var srgbFromXYZ = Mat3{ - {3.2406 * scale, -1.5372 * scale, -0.4986 * scale}, - {-0.9689 * scale, 1.8758 * scale, 0.0415 * scale}, - {0.0557 * scale, -0.2040 * scale, 1.0570 * scale}, + srgbFromXYZ := xyzToSRGBMatrix + for r := range 3 { + for c := range 3 { + srgbFromXYZ[r][c] *= scale + } } ans.combined_XYZ_to_linear_SRGB = mulMat3(srgbFromXYZ, adapt) ans.previous_matrices[0][0] = 1 @@ -253,11 +303,7 @@ func srgbToLinear(c float64) float64 { // Converts linear RGB to CIE XYZ using sRGB D65 matrix. // Input r,g,b must be linear-light (not gamma-encoded). func rgbToXYZ(r, g, b float64) (x, y, z float64) { - // sRGB (linear) to XYZ (D65), matrix from IEC 61966-2-1 - x = 0.4124564*r + 0.3575761*g + 0.1804375*b - y = 0.2126729*r + 0.7151522*g + 0.0721750*b - z = 0.0193339*r + 0.1191920*g + 0.9503041*b - return + return mulMat3Vec(srgbToXYZMatrix, Vec3{r, g, b}) } func SrgbToLab(r, g, b float64) (L, a, B float64) { diff --git a/vendor/github.com/kovidgoyal/imaging/local-agent.md b/vendor/github.com/kovidgoyal/imaging/local-agent.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vendor/github.com/kovidgoyal/imaging/magick/magick_js.go b/vendor/github.com/kovidgoyal/imaging/magick/magick_js.go new file mode 100644 index 0000000000..c6e39fafb3 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/magick/magick_js.go @@ -0,0 +1,9 @@ +//go:build js + +package magick + +import "os" + +func get_temp_dir() string { + return os.TempDir() +} \ No newline at end of file diff --git a/vendor/github.com/kovidgoyal/imaging/magick/magick_unix.go b/vendor/github.com/kovidgoyal/imaging/magick/magick_unix.go index c2f9213c64..dae471ac3c 100644 --- a/vendor/github.com/kovidgoyal/imaging/magick/magick_unix.go +++ b/vendor/github.com/kovidgoyal/imaging/magick/magick_unix.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build !windows && !js package magick diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/blackpoint.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/blackpoint.go index 70f694b7ae..8cf8d5173b 100644 --- a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/blackpoint.go +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/blackpoint.go @@ -19,11 +19,18 @@ func (p *Profile) IsMatrixShaper() bool { } func (p *Profile) BlackPoint(intent RenderingIntent, debug General_debug_callback) (ans XYZType) { - if q := p.blackpoints[intent]; q != nil { + // The lock is not held during the computation as it can recurse via + // CreateTransformerToDevice() + p.blackpoints_lock.Lock() + q := p.blackpoints[intent] + p.blackpoints_lock.Unlock() + if q != nil { return *q } defer func() { + p.blackpoints_lock.Lock() p.blackpoints[intent] = &ans + p.blackpoints_lock.Unlock() }() if p.Header.DeviceClass == DeviceClassLink || p.Header.DeviceClass == DeviceClassAbstract || p.Header.DeviceClass == DeviceClassNamedColor { return @@ -43,9 +50,28 @@ func (p *Profile) BlackPoint(intent RenderingIntent, debug General_debug_callbac return p.black_point_as_darker_colorant(intent, debug) } +// See cmsIsIntentSupported() in cmsio1.c +func (p *Profile) is_intent_supported_as_input(intent RenderingIntent) bool { + var sig Signature + switch intent { + case PerceptualRenderingIntent: + sig = AToB0TagSignature + case RelativeColorimetricRenderingIntent, AbsoluteColorimetricRenderingIntent: + sig = AToB1TagSignature + case SaturationRenderingIntent: + sig = AToB2TagSignature + default: + return false + } + return p.TagTable.Has(sig) || p.IsMatrixShaper() +} + func (p *Profile) black_point_as_darker_colorant(intent RenderingIntent, debug General_debug_callback) XYZType { + if !p.is_intent_supported_as_input(intent) { + return XYZType{} + } bp := p.Header.DataColorSpace.BlackPoint() - if bp == nil || (len(bp) != 3 && len(bp) != 4) { + if bp == nil || (len(bp) != 1 && len(bp) != 3 && len(bp) != 4) { return XYZType{} } tr, err := p.CreateTransformerToPCS(intent, len(bp), debug == nil) @@ -56,18 +82,18 @@ func (p *Profile) black_point_as_darker_colorant(intent RenderingIntent, debug G tr.Append(NewXYZtoLAB(p.PCSIlluminant)) } var l, a, b unit_float + var out, inp [4]unit_float + copy(inp[:], bp) if debug == nil { if len(bp) == 3 { l, a, b = tr.Transform(bp[0], bp[1], bp[2]) } else { - var x [4]unit_float - tr.TransformGeneral(x[:], bp) - l, a, b = x[0], x[1], x[2] + tr.TransformGeneral(out[:], inp[:]) + l, a, b = out[0], out[1], out[2] } } else { - var x [4]unit_float - tr.TransformGeneralDebug(x[:], bp, debug) - l, a, b = x[0], x[1], x[2] + tr.TransformGeneralDebug(out[:], inp[:], debug) + l, a, b = out[0], out[1], out[2] } a, b = 0, 0 if l < 0 || l > 50 { diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/gray.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/gray.go new file mode 100644 index 0000000000..497cc91f28 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/gray.go @@ -0,0 +1,90 @@ +package icc + +import ( + "fmt" +) + +var _ = fmt.Print + +// A transformer that expands a single achromatic channel into the three +// channels of the normalized PCS +type GrayToPCS struct { + name string + scale, offset [3]unit_float +} + +func (c *GrayToPCS) IOSig() (int, int) { return 1, 3 } +func (c *GrayToPCS) Iter(f func(ChannelTransformer) bool) { f(c) } +func (c *GrayToPCS) String() string { + return fmt.Sprintf("%s{scale: %.6v offset: %.6v}", c.name, c.scale, c.offset) +} +func (c *GrayToPCS) Transform(v, _, _ unit_float) (unit_float, unit_float, unit_float) { + return v*c.scale[0] + c.offset[0], v*c.scale[1] + c.offset[1], v*c.scale[2] + c.offset[2] +} +func (c *GrayToPCS) TransformGeneral(o, i []unit_float) { + o[0], o[1], o[2] = c.Transform(i[0], 0, 0) +} + +// The gray value is the luminance, Y relative to D50 in normalized XYZ +func NewGrayToXYZ() *GrayToPCS { + const s = MAX_ENCODEABLE_XYZ_INVERSE + return &GrayToPCS{name: "GrayToXYZ", scale: [3]unit_float{lcms_d50.X * s, lcms_d50.Y * s, lcms_d50.Z * s}} +} + +// The gray value is L* with a* = b* = 0 in normalized LAB +func NewGrayToLAB() *GrayToPCS { + return &GrayToPCS{name: "GrayToLAB", scale: [3]unit_float{1, 0, 0}, offset: [3]unit_float{0, 128. / 255, 128. / 255}} +} + +// A transformer that extracts a single achromatic channel from the three +// channels of the normalized PCS +type PCSToGray struct { + name string + weights [3]unit_float +} + +func (c *PCSToGray) IOSig() (int, int) { return 3, 1 } +func (c *PCSToGray) Iter(f func(ChannelTransformer) bool) { f(c) } +func (c *PCSToGray) String() string { return fmt.Sprintf("%s{%.6v}", c.name, c.weights) } +func (c *PCSToGray) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) { + return c.weights[0]*r + c.weights[1]*g + c.weights[2]*b, 0, 0 +} +func (c *PCSToGray) TransformGeneral(o, i []unit_float) { + o[0], _, _ = c.Transform(i[0], i[1], i[2]) +} + +// See BuildGrayOutputPipeline() in cmsio1.c +func NewXYZToGray() *PCSToGray { + return &PCSToGray{name: "XYZToGray", weights: [3]unit_float{0, MAX_ENCODEABLE_XYZ * lcms_d50.Y, 0}} +} + +func NewLABToGray() *PCSToGray { + return &PCSToGray{name: "LABToGray", weights: [3]unit_float{1, 0, 0}} +} + +// See section F.2 of ICC.1-2022-05.pdf and BuildGrayInputMatrixPipeline() and +// BuildGrayOutputPipeline() in cmsio1.c of lcms +// Note that the PCS illuminant, not the media white point, is used as per +// section F.2, adaptation to the media white point is done only for the +// absolute colorimetric intent, as for all other profiles. +func (p *Profile) create_gray_trc_transformer(forward bool, pipeline *Pipeline) (err error) { + c, err := p.TagTable.load_curve_tag(GrayTRCTagSignature) + if err != nil { + return err + } + var to_pcs, from_pcs ChannelTransformer + switch p.Header.ProfileConnectionSpace { + case ColorSpaceXYZ: + to_pcs, from_pcs = NewGrayToXYZ(), NewXYZToGray() + case ColorSpaceLab: + to_pcs, from_pcs = NewGrayToLAB(), NewLABToGray() + default: + return fmt.Errorf("monochrome matrix/TRC based profile using unsupported PCS color space: %v", p.Header.ProfileConnectionSpace) + } + if forward { + pipeline.Append(NewCurveTransformer("GrayTRC", c), to_pcs) + } else { + pipeline.Append(from_pcs, NewInverseCurveTransformer("GrayTRC", c)) + } + return nil +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/pcs.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/pcs.go index a3827900a6..8fd126b3ff 100644 --- a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/pcs.go +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/pcs.go @@ -8,6 +8,9 @@ import ( var _ = fmt.Println +// The D50 white point used by lcms as the PCS illuminant, see cmsD50_XYZ() +var lcms_d50 = XYZType{0.9642, 1.0, 0.8249} + const MAX_ENCODEABLE_XYZ = 1.0 + 32767.0/32768.0 const MAX_ENCODEABLE_XYZ_INVERSE = 1.0 / (MAX_ENCODEABLE_XYZ) const LAB_MFT2_ENCODING_CORRECTION = 65535.0 / 65280.0 diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/pipeline.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/pipeline.go index 315b162ef9..c6fe54a019 100644 --- a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/pipeline.go +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/pipeline.go @@ -109,6 +109,9 @@ func (p *Pipeline) insert(idx int, c ChannelTransformer) { } func (p *Pipeline) Insert(idx int, c ChannelTransformer) { + if is_nil(c) { + return + } s := slices.Collect(c.Iter) if idx > -1 { slices.Reverse(s) @@ -237,6 +240,10 @@ func (p *Pipeline) IsXYZSRGB() bool { } } if is_srgb { + if g, ok := p.transformers[1].(*GrayToPCS); ok { + // monochrome profile with the sRGB TRC, the gray values are already sRGB + return *g == *NewGrayToXYZ() + } if c, ok := p.transformers[1].(AsMatrix3); ok { q := c.AsMatrix3() var expected_matrix = Matrix3{{0.218036, 0.192576, 0.0715343}, {0.111246, 0.358442, 0.0303044}, {0.00695811, 0.0485389, 0.357053}} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profile.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profile.go index 9cbe2189ba..48326f609c 100644 --- a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profile.go +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profile.go @@ -5,6 +5,7 @@ import ( _ "embed" "fmt" "io" + "math" "os" "sync" ) @@ -29,6 +30,8 @@ type Profile struct { TagTable TagTable PCSIlluminant XYZType blackpoints map[RenderingIntent]*XYZType + + blackpoints_lock sync.Mutex } func (p *Profile) Description() (string, error) { @@ -43,36 +46,52 @@ func (p *Profile) DeviceModelDescription() (string, error) { return p.TagTable.getDeviceModelDescription() } -func (p *Profile) get_effective_chromatic_adaption(forward bool, intent RenderingIntent) (ans *Matrix3, err error) { - if intent != AbsoluteColorimetricRenderingIntent { // ComputeConversion() in lcms - return nil, nil +// See _cmsReadMediaWhitePoint() in cmsio1.c +func (p *Profile) media_white_point() XYZType { + // V2 display profiles should give D50 + if p.Header.Version.Major < 4 && p.Header.DeviceClass == DeviceClassDisplay { + return lcms_d50 } - pcs_whitepoint := p.Header.ParsedPCSIlluminant() x, err := p.TagTable.get_parsed(MediaWhitePointTagSignature, p.Header.DataColorSpace, p.Header.ProfileConnectionSpace) if err != nil { - return nil, err + return lcms_d50 } - wtpt, ok := x.(*XYZType) - if !ok { - return nil, fmt.Errorf("wtpt tag is not of XYZType") + if wtpt, ok := x.(*XYZType); ok { + return *wtpt } - if pcs_whitepoint == *wtpt { - return nil, nil - } - defer func() { - if err == nil && ans != nil && !forward { - m, ierr := ans.Inverted() - if ierr == nil { - ans = &m - } else { - ans, err = nil, ierr - } - } - }() - return p.TagTable.get_chromatic_adaption() + return lcms_d50 } -func (p *Profile) create_matrix_trc_transformer(forward bool, chromatic_adaptation *Matrix3, pipeline *Pipeline) (err error) { +// The transformers to convert relative colorimetric normalized PCS values to +// absolute colorimetric ones (or the reverse when forward is false). See +// ComputeAbsoluteIntent() in cmscnvrt.c, we use an adaptation state of 1 (the +// lcms default) and D50 as the white point of the other side of the +// conversion, which is the PCS or an sRGB profile. +func (p *Profile) absolute_colorimetric_adaptation(forward bool, intent RenderingIntent) []ChannelTransformer { + if intent != AbsoluteColorimetricRenderingIntent { + return nil + } + wp := p.media_white_point() + // v4 profiles have a media white point of D50 up to the precision of s15Fixed16Number + const threshold = 2. / 65536 + if math.Abs(wp.X-lcms_d50.X) < threshold && math.Abs(wp.Y-lcms_d50.Y) < threshold && math.Abs(wp.Z-lcms_d50.Z) < threshold { + return nil + } + m := &Matrix3{{wp.X / lcms_d50.X, 0, 0}, {0, wp.Y / lcms_d50.Y, 0}, {0, 0, wp.Z / lcms_d50.Z}} + if !forward { + m = &Matrix3{{lcms_d50.X / wp.X, 0, 0}, {0, lcms_d50.Y / wp.Y, 0}, {0, 0, lcms_d50.Z / wp.Z}} + } + if p.Header.ProfileConnectionSpace == ColorSpaceLab { + // the scaling must be done in XYZ space + return []ChannelTransformer{NewNormalizedToLAB(), NewLABtoXYZ(p.PCSIlluminant), m, NewXYZtoLAB(p.PCSIlluminant), NewLABToNormalized()} + } + return []ChannelTransformer{m} +} + +func (p *Profile) create_matrix_trc_transformer(forward bool, pipeline *Pipeline) (err error) { + if p.Header.DataColorSpace == ColorSpaceGray { + return p.create_gray_trc_transformer(forward, pipeline) + } if p.Header.ProfileConnectionSpace != ColorSpaceXYZ { return fmt.Errorf("matrix/TRC based profile using non XYZ PCS color space: %v", p.Header.ProfileConnectionSpace) } @@ -91,16 +110,10 @@ func (p *Profile) create_matrix_trc_transformer(forward bool, chromatic_adaptati if err != nil { return err } - var c Curves if forward { - c = NewCurveTransformer("TRC", rc, gc, bc) + pipeline.Append(NewCurveTransformer("TRC", rc, gc, bc), m) } else { - c = NewInverseCurveTransformer("TRC", rc, gc, bc) - } - if forward { - pipeline.Append(c, m, chromatic_adaptation) - } else { - pipeline.Append(chromatic_adaptation, m, NewInverseCurveTransformer("TRC", rc, gc, bc)) + pipeline.Append(m, NewInverseCurveTransformer("TRC", rc, gc, bc)) } return nil } @@ -118,7 +131,9 @@ func (p *Profile) find_conversion_tag(forward bool, rendering_intent RenderingIn case SaturationRenderingIntent: ans_sig = AToB2TagSignature case AbsoluteColorimetricRenderingIntent: - ans_sig = AToB3TagSignature + // absolute colorimetric uses the relative colorimetric table + // with adaptation to the media white point, see Device2PCS16 in cmsio1.c + ans_sig = AToB1TagSignature default: return nil, fmt.Errorf("unknown rendering intent: %v", rendering_intent) } @@ -137,7 +152,7 @@ func (p *Profile) find_conversion_tag(forward bool, rendering_intent RenderingIn case SaturationRenderingIntent: ans_sig = BToA2TagSignature case AbsoluteColorimetricRenderingIntent: - ans_sig = BToA3TagSignature + ans_sig = BToA1TagSignature default: return nil, fmt.Errorf("unknown rendering intent: %v", rendering_intent) } @@ -152,7 +167,7 @@ func (p *Profile) find_conversion_tag(forward bool, rendering_intent RenderingIn return nil, nil } // We rely on profile reader to error out if the PCS color space is not XYZ - // or LAB and the device colorspace is not RGB or CMYK + // or LAB and the device colorspace is not RGB, CMYK or Gray input_colorspace, output_colorspace := p.Header.DataColorSpace, p.Header.ProfileConnectionSpace if !forward { input_colorspace, output_colorspace = output_colorspace, input_colorspace @@ -194,6 +209,7 @@ func (p *Profile) CreateTransformerToDevice(rendering_intent RenderingIntent, us }() ans = &Pipeline{} + pcs_normalized := false if p.effective_bpc(rendering_intent, use_blackpoint_compensation) { var PCS_blackpoint XYZType // 0, 0, 0 output_blackpoint := p.BlackPoint(rendering_intent, nil) @@ -201,36 +217,47 @@ func (p *Profile) CreateTransformerToDevice(rendering_intent RenderingIntent, us is_lab := p.Header.ProfileConnectionSpace == ColorSpaceLab if is_lab { ans.Append(NewLABtoXYZ(p.PCSIlluminant)) - ans.Append(NewXYZToNormalized()) } + // black point correction works on normalized XYZ + ans.Append(NewXYZToNormalized()) ans.Append(NewBlackPointCorrection(p.PCSIlluminant, PCS_blackpoint, output_blackpoint)) if is_lab { ans.Append(NewNormalizedToXYZ()) ans.Append(NewXYZtoLAB(p.PCSIlluminant)) + } else { + pcs_normalized = true } } } - ans.Append(transform_for_pcs_colorspace(p.Header.ProfileConnectionSpace, false)) + if !pcs_normalized { + ans.Append(transform_for_pcs_colorspace(p.Header.ProfileConnectionSpace, false)) + } const forward = false b2a, err := p.find_conversion_tag(forward, rendering_intent) if err != nil { return nil, err } - chromatic_adaptation, err := p.get_effective_chromatic_adaption(forward, rendering_intent) - if err != nil { - return nil, err - } + ans.Append(p.absolute_colorimetric_adaptation(forward, rendering_intent)...) if b2a != nil { + is_legacy_lab := false + if mft, ok := b2a.(*MFT); ok && !mft.is8bit && p.Header.ProfileConnectionSpace == ColorSpaceLab { + // Need to scale to the legacy LAB encoding used by lut16type + // tags, see _cmsReadOutputLUT() in cmsio1.c + is_legacy_lab = true + ans.Append(NewLABToMFT2()) + } ans.Append(b2a) - ans.Append(chromatic_adaptation) + if is_legacy_lab && p.Header.DataColorSpace == ColorSpaceLab { + ans.Append(NewLABFromMFT2()) + } if p.Header.ProfileConnectionSpace == ColorSpaceLab { // For some reason, lcms prefers trilinear over tetrahedral in this // case, see _cmsReadOutputLUT() in cmsio1.c ans.UseTrilinearInsteadOfTetrahedral() } } else { - err = p.create_matrix_trc_transformer(forward, chromatic_adaptation, ans) + err = p.create_matrix_trc_transformer(forward, ans) } return } @@ -242,13 +269,8 @@ func (p *Profile) createTransformerToPCS(rendering_intent RenderingIntent) (ans if err != nil { return nil, err } - chromatic_adaptation, err := p.get_effective_chromatic_adaption(forward, rendering_intent) - if err != nil { - return nil, err - } if a2b != nil { ans.Append(a2b) - ans.Append(chromatic_adaptation) if ans.has_lut16type_tag && p.Header.ProfileConnectionSpace == ColorSpaceLab { // Need to scale the lut16type data for legacy LAB encoding in ICC profiles if p.Header.DataColorSpace == ColorSpaceLab { @@ -257,7 +279,10 @@ func (p *Profile) createTransformerToPCS(rendering_intent RenderingIntent) (ans ans.Append(NewLABFromMFT2()) } } else { - err = p.create_matrix_trc_transformer(forward, chromatic_adaptation, ans) + err = p.create_matrix_trc_transformer(forward, ans) + } + if err == nil { + ans.Append(p.absolute_colorimetric_adaptation(forward, rendering_intent)...) } return } diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profilereader.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profilereader.go index 899ab34415..703a79843e 100644 --- a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profilereader.go +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profilereader.go @@ -55,7 +55,7 @@ func (pr *ProfileReader) readHeader(header *Header) (err error) { if header.ProfileConnectionSpace != ColorSpaceXYZ && header.ProfileConnectionSpace != ColorSpaceLab { return fmt.Errorf("unsupported profile connection space colorspace: %s", header.ProfileConnectionSpace) } - if header.DataColorSpace != ColorSpaceRGB && header.DataColorSpace != ColorSpaceCMYK { + if header.DataColorSpace != ColorSpaceRGB && header.DataColorSpace != ColorSpaceCMYK && header.DataColorSpace != ColorSpaceGray { return fmt.Errorf("unsupported device colorspace: %s", header.DataColorSpace) } } diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_clut.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_clut.go index 91db428690..3787126af5 100644 --- a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_clut.go +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_clut.go @@ -58,6 +58,8 @@ func decode_clut_table(raw []byte, bytes_per_channel, OutputChannels int, grid_p switch output_colorspace { case ColorSpaceCMYK: expected_num_of_output_channels = 4 + case ColorSpaceGray: + expected_num_of_output_channels = 1 } if expected_num_of_output_channels != OutputChannels { return nil, 0, fmt.Errorf("CLUT table number of output channels %d inappropriate for output_colorspace: %s", OutputChannels, output_colorspace) @@ -147,7 +149,12 @@ func (c *TetrahedralInterpolate) Transform(r, g, b unit_float) (unit_float, unit } func (m *TetrahedralInterpolate) TransformGeneral(o, i []unit_float) { - m.d.tetrahedral_interpolation4(i[0], i[1], i[2], i[3], o[:m.d.num_outputs:m.d.num_outputs]) + o = o[:m.d.num_outputs:m.d.num_outputs] + if m.d.num_inputs == 3 { + m.d.tetrahedral_interpolation(i[0], i[1], i[2], o) + } else { + m.d.tetrahedral_interpolation4(i[0], i[1], i[2], i[3], o) + } } func clamp01(v unit_float) unit_float { diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_curve.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_curve.go index 9955b3f397..4aaebc538a 100644 --- a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_curve.go +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_curve.go @@ -51,6 +51,9 @@ func (c CurveTransformer) IOSig() (int, int) { } func (c CurveTransformer) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) { + if len(c.curves) == 1 { + return c.curves[0].Transform(r), g, b + } return c.curves[0].Transform(r), c.curves[1].Transform(g), c.curves[2].Transform(b) } func (c CurveTransformer) TransformGeneral(o, i []unit_float) { @@ -64,11 +67,14 @@ func (c InverseCurveTransformer) IOSig() (int, int) { } func (c InverseCurveTransformer) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) { // we need to clamp as per spec section F.3 of ICC.1-2202-05.pdf + if len(c.curves) == 1 { + return c.curves[0].InverseTransform(clamp01(r)), g, b + } return c.curves[0].InverseTransform(clamp01(r)), c.curves[1].InverseTransform(clamp01(g)), c.curves[2].InverseTransform(clamp01(b)) } func (c InverseCurveTransformer) TransformGeneral(o, i []unit_float) { for n, c := range c.curves { - o[n] = c.InverseTransform(i[n]) + o[n] = c.InverseTransform(clamp01(i[n])) } } @@ -129,10 +135,11 @@ type Curves interface { func NewCurveTransformer(name string, curves ...Curve1D) Curves { all_identity := true - for _, c := range curves { + for i, c := range curves { if c == nil { ident := IdentityCurve(0) c = &ident + curves[i] = c } if _, is_ident := c.(*IdentityCurve); !is_ident { all_identity = false @@ -150,10 +157,11 @@ func NewCurveTransformer(name string, curves ...Curve1D) Curves { } func NewInverseCurveTransformer(name string, curves ...Curve1D) Curves { all_identity := true - for _, c := range curves { + for i, c := range curves { if c == nil { ident := IdentityCurve(0) c = &ident + curves[i] = c } if _, is_ident := c.(*IdentityCurve); !is_ident { all_identity = false diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_mft.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_mft.go index 51d6e1d1fc..0aba3a364f 100644 --- a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_mft.go +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_mft.go @@ -23,20 +23,22 @@ func (c MFT) String() string { func (c *MFT) IOSig() (int, int) { return c.in_channels, c.out_channels } func (c *MFT) Iter(f func(ChannelTransformer) bool) { - if mo, ok := c.matrix.(*MatrixWithOffset); ok { - if _, ok := mo.m.(*IdentityMatrix); !ok { - if !f(mo.m) { - return + // the matrix is only used for three input channels, see Type_LUT16_Read() in cmstypes.c + if c.in_channels == 3 { + if mo, ok := c.matrix.(*MatrixWithOffset); ok { + if _, ok := mo.m.(*IdentityMatrix); !ok { + if !f(mo.m) { + return + } } - } - if tt := mo.Translation(); tt != nil { - if !f(tt) { - return + if tt := mo.Translation(); tt != nil { + if !f(tt) { + return + } } + } else if !f(c.matrix) { + return } - - } else if !f(c.matrix) { - return } if !f(c.input_curve) { return diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_modular.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_modular.go index 03eaf32a6f..f7bd42e6f5 100644 --- a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_modular.go +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_modular.go @@ -91,7 +91,7 @@ func modularDecoder(raw []byte, _, output_colorspace ColorSpace) (ans any, err e block := raw[offset:] var c any var consumed int - for range inputCh { + for range num_curves_reqd { if len(block) < 4 { return nil, errors.New("modular (mAB/mBA) tag too short") } @@ -121,7 +121,8 @@ func modularDecoder(raw []byte, _, output_colorspace ColorSpace) (ans any, err e if mt.a_curves, err = read_curves(a, IfElse(is_a_to_b, inputCh, outputCh)); err != nil { return nil, err } - if mt.m_curves, err = read_curves(m, outputCh); err != nil { + // M curves are always on the PCS side + if mt.m_curves, err = read_curves(m, IfElse(is_a_to_b, outputCh, inputCh)); err != nil { return nil, err } var temp any diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tagtable.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tagtable.go index f552f6d428..c35253fc96 100644 --- a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tagtable.go +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tagtable.go @@ -1,7 +1,6 @@ package icc import ( - "errors" "fmt" "sync" ) @@ -263,22 +262,6 @@ func array_to_matrix(a []unit_float) *Matrix3 { return &m } -func (p *TagTable) get_chromatic_adaption() (*Matrix3, error) { - x, err := p.get_parsed(ChromaticAdaptationTagSignature, ColorSpaceRGB, ColorSpaceXYZ) - if err != nil { - var nf *not_found - if errors.As(err, &nf) { - return nil, nil - } - return nil, err - } - a, ok := x.([]unit_float) - if !ok { - return nil, fmt.Errorf("chad tag is not an ArrayType") - } - return array_to_matrix(a), nil -} - func emptyTagTable() TagTable { return TagTable{ entries: make(map[Signature]raw_tag_entry), diff --git a/vendor/github.com/kovidgoyal/imaging/publish.py b/vendor/github.com/kovidgoyal/imaging/publish.py index 587dc4cff5..1f3c2f9666 100644 --- a/vendor/github.com/kovidgoyal/imaging/publish.py +++ b/vendor/github.com/kovidgoyal/imaging/publish.py @@ -5,7 +5,7 @@ import os import subprocess -VERSION = "1.8.23" +VERSION = "1.9.0" def run(*args: str): diff --git a/vendor/github.com/nats-io/nats.go/Makefile b/vendor/github.com/nats-io/nats.go/Makefile index 070a6e6f47..a2d504ff42 100644 --- a/vendor/github.com/nats-io/nats.go/Makefile +++ b/vendor/github.com/nats-io/nats.go/Makefile @@ -72,9 +72,7 @@ tester-net: # No host port publishing, for sibling-container mode (make test-tester). # Publishing here would break server bring-up: docker-proxy holds 0.0.0.0: # inside the container's netns, racing the tester's port-reservation handover -# ("bind: address already in use"). The ephemeral range is left at the kernel -# default so TIME_WAIT churn cannot crowd it; narrowing it only matters when -# ports must match a published range (see tester-up-host). +# ("bind: address already in use"). tester-up: tester-net docker run -d \ --name $(TESTER_NAME) \ @@ -85,7 +83,7 @@ tester-up: tester-net @echo "Tester up as '$(TESTER_NAME)'. Run the suite: make test-tester" # Publishes ports so `go test` on the host reaches the spawned servers; -# ip_local_port_range is narrowed to match the published range. On macOS +# NATS_PORT_RANGE keeps them inside the published range. On macOS # docker-proxy races the port handover, failing ~5-10% of server creations in # heavy suites — rerun, or use sibling-container mode, which is immune. tester-up-host: tester-net @@ -93,10 +91,10 @@ tester-up-host: tester-net --name $(TESTER_NAME) \ --network $(TESTER_NETWORK) \ --restart unless-stopped \ - --sysctl net.ipv4.ip_local_port_range="30000 31000" \ -p 4222:4222 \ -p 30000-31000:30000-31000 \ -e NATS_ADVERTISE=localhost \ + -e NATS_PORT_RANGE=30000-31000 \ $(TESTER_IMAGE) serve @echo "Tester up as '$(TESTER_NAME)'. Run: make test-docker [T=...] [PKG=...]" @echo "(plain 'make test' ignores this container and uses the in-process tester)" diff --git a/vendor/github.com/nats-io/nats.go/README.md b/vendor/github.com/nats-io/nats.go/README.md index b5bef9042e..abe670e05c 100644 --- a/vendor/github.com/nats-io/nats.go/README.md +++ b/vendor/github.com/nats-io/nats.go/README.md @@ -23,7 +23,7 @@ A [Go](http://golang.org) client for the [NATS messaging system](https://nats.io go get github.com/nats-io/nats.go@latest # To get a specific version: -go get github.com/nats-io/nats.go@v1.54.0 +go get github.com/nats-io/nats.go@v1.55.0 # Note that the latest major version for NATS Server is v2: go get github.com/nats-io/nats-server/v2@latest diff --git a/vendor/github.com/nats-io/nats.go/context.go b/vendor/github.com/nats-io/nats.go/context.go index b3dfa4f06d..55c978e5cd 100644 --- a/vendor/github.com/nats-io/nats.go/context.go +++ b/vendor/github.com/nats-io/nats.go/context.go @@ -67,7 +67,10 @@ func (nc *Conn) requestWithContext(ctx context.Context, subj string, hdr, data [ select { case m, ok = <-mch: if !ok { - return nil, ErrConnectionClosed + if nc.IsClosed() { + return nil, ErrConnectionClosed + } + return nil, ErrDisconnected } case <-ctx.Done(): nc.mu.Lock() diff --git a/vendor/github.com/nats-io/nats.go/dependencies.md b/vendor/github.com/nats-io/nats.go/dependencies.md index a2e85ba5a3..c12170a466 100644 --- a/vendor/github.com/nats-io/nats.go/dependencies.md +++ b/vendor/github.com/nats-io/nats.go/dependencies.md @@ -2,14 +2,12 @@ This file lists the dependencies used in this repository. -| Dependency | License | -|-------------------------------------------|--------------| -| Go | BSD 3-Clause | -| github.com/golang/protobuf/proto | BSD-3-Clause | -| github.com/klauspost/compress | BSD-3-Clause | -| github.com/nats-io/nkeys | Apache-2.0 | -| github.com/nats-io/nuid | Apache-2.0 | -| github.com/synadia-io/orbit.go/ntf-client | Apache-2.0 | -| go.uber.org/goleak | MIT | -| golang.org/x/text | BSD-3-Clause | -| google.golang.org/protobuf | BSD-3-Clause | +| Dependency | License | +|------------|---------| +| Go | BSD-3-Clause | +| github.com/klauspost/compress | BSD-3-Clause | +| github.com/nats-io/nkeys | Apache-2.0 | +| github.com/nats-io/nuid | Apache-2.0 | +| golang.org/x/crypto | BSD-3-Clause | +| golang.org/x/sys/cpu | BSD-3-Clause | +| google.golang.org/protobuf | BSD-3-Clause | diff --git a/vendor/github.com/nats-io/nats.go/dependencies.tpl b/vendor/github.com/nats-io/nats.go/dependencies.tpl index 41cd3741a1..cd5600f4a3 100644 --- a/vendor/github.com/nats-io/nats.go/dependencies.tpl +++ b/vendor/github.com/nats-io/nats.go/dependencies.tpl @@ -2,8 +2,9 @@ This file lists the dependencies used in this repository. -{{/* compress has actually a BSD 3-Clause license, but the License file in the repo confuses go-license tooling, hence the manual exception */}} -| Dependency | License | -|--------------------------------------------------|-----------------------------------------| -{{ range . }}| {{ .Name }} | {{ if eq .Name "github.com/klauspost/compress/flate" }}BSD 3-Clause{{ else }}{{ .LicenseName }}{{ end }} | -{{ end }} +{{/* compress has actually a BSD 3-Clause license, but the License file in the repo confuses go-license tooling, hence the manual exception. Its reported name depends on which of its packages are imported. */ -}} +| Dependency | License | +|------------|---------| +| Go | BSD-3-Clause | +{{ range . }}| {{ .Name }} | {{ if or (eq .Name "github.com/klauspost/compress") (eq .Name "github.com/klauspost/compress/flate") }}BSD-3-Clause{{ else }}{{ .LicenseName }}{{ end }} | +{{ end -}} diff --git a/vendor/github.com/nats-io/nats.go/go_test.mod b/vendor/github.com/nats-io/nats.go/go_test.mod index 146ba5f53e..5af5546eab 100644 --- a/vendor/github.com/nats-io/nats.go/go_test.mod +++ b/vendor/github.com/nats-io/nats.go/go_test.mod @@ -8,7 +8,7 @@ require ( github.com/nats-io/jwt/v2 v2.8.2 github.com/nats-io/nkeys v0.4.16 github.com/nats-io/nuid v1.0.1 - github.com/synadia-io/orbit.go/ntf v0.0.1 + github.com/synadia-io/orbit.go/ntf v0.0.2 github.com/synadia-io/orbit.go/ntf-client v0.0.3 google.golang.org/protobuf v1.36.12 ) diff --git a/vendor/github.com/nats-io/nats.go/go_test.sum b/vendor/github.com/nats-io/nats.go/go_test.sum index 6ec2bfea7b..88dd4ca3b4 100644 --- a/vendor/github.com/nats-io/nats.go/go_test.sum +++ b/vendor/github.com/nats-io/nats.go/go_test.sum @@ -18,8 +18,8 @@ github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/synadia-io/orbit.go/ntf v0.0.1 h1:eTL4Dln8ha8ELjVNVxnSpsVG1zHxr6g/eMwhFL98r1Q= -github.com/synadia-io/orbit.go/ntf v0.0.1/go.mod h1:a+ZZMkFv7dikn9hmE1PH/9FysauhZEFk6MBjCVwGn/s= +github.com/synadia-io/orbit.go/ntf v0.0.2 h1:2dWNVOiGQ77h95EEz+ToLQlI2e+3qHrDf13MyeCAVRI= +github.com/synadia-io/orbit.go/ntf v0.0.2/go.mod h1:bqN0tG8Qy12AMwyoy/bJqxQ1NmNEPZYy4+pMGFpLWfE= github.com/synadia-io/orbit.go/ntf-client v0.0.3 h1:FoFjSd+idhy10BqDadGpUk4OcpBu/7V/Y8sHWhbSjdc= github.com/synadia-io/orbit.go/ntf-client v0.0.3/go.mod h1:P745h+KxJPtoHF6csj2PQUB+uf72XR49o2krTaTQ8Ls= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= diff --git a/vendor/github.com/nats-io/nats.go/internal/parser/parse.go b/vendor/github.com/nats-io/nats.go/internal/parser/parse.go index 7eab8adddf..27e98d03ff 100644 --- a/vendor/github.com/nats-io/nats.go/internal/parser/parse.go +++ b/vendor/github.com/nats-io/nats.go/internal/parser/parse.go @@ -16,6 +16,7 @@ package parser import ( "errors" "fmt" + "strings" ) const ( @@ -30,6 +31,8 @@ const ( AckNumPendingTokenPos ) +const v1TokenCounts, v2TokenCounts = 9, 12 + var ErrInvalidSubjectFormat = errors.New("invalid format of ACK subject") // Quick parser for positive numbers in ack reply encoding. @@ -54,18 +57,8 @@ func ParseNum(d string) (n uint64) { return } -func GetMetadataFields(subject string) ([]string, error) { - v1TokenCounts, v2TokenCounts := 9, 12 - - var start int - tokens := make([]string, 0, v2TokenCounts) - for i := 0; i < len(subject); i++ { - if subject[i] == '.' { - tokens = append(tokens, subject[start:i]) - start = i + 1 - } - } - tokens = append(tokens, subject[start:]) +func GetMetadataFields(subject string) ([AckNumPendingTokenPos + 1]string, error) { + var tokens [AckNumPendingTokenPos + 1]string // // Newer server will include the domain name and account hash in the subject, // and a token at the end. @@ -80,24 +73,24 @@ func GetMetadataFields(subject string) ([]string, error) { // it may be removed in the future. Also, the library has no use for it. // The point is that a v2 ACK subject is valid if it has at least 11 tokens. // - tokensLen := len(tokens) + tokensLen := strings.Count(subject, ".") + 1 // If lower than 9 or more than 9 but less than 11, report an error if tokensLen < v1TokenCounts || (tokensLen > v1TokenCounts && tokensLen < v2TokenCounts-1) { - return nil, ErrInvalidSubjectFormat + return tokens, ErrInvalidSubjectFormat } - if tokens[0] != "$JS" || tokens[1] != "ACK" { - return nil, fmt.Errorf("%w: subject should start with $JS.ACK", ErrInvalidSubjectFormat) + if !strings.HasPrefix(subject, "$JS.ACK.") { + return tokens, fmt.Errorf("%w: subject should start with $JS.ACK", ErrInvalidSubjectFormat) } - // For v1 style, we insert 2 empty tokens (domain and hash) so that the - // rest of the library references known fields at a constant location. - if tokensLen == v1TokenCounts { - // Extend the array (we know the backend is big enough) - tokens = append(tokens[:AckDomainTokenPos+2], tokens[AckDomainTokenPos:]...) - // Clear the domain and hash tokens - tokens[AckDomainTokenPos], tokens[AckAccHashTokenPos] = "", "" - - } else if tokens[AckDomainTokenPos] == "_" { - // If domain is "_", replace with empty value. + for i := range tokens { + // For v1 style, leave the domain and hash tokens empty so that the + // rest of the library references known fields at a constant location. + if tokensLen == v1TokenCounts && (i == AckDomainTokenPos || i == AckAccHashTokenPos) { + continue + } + tokens[i], subject, _ = strings.Cut(subject, ".") + } + // If domain is "_", replace with empty value. + if tokens[AckDomainTokenPos] == "_" { tokens[AckDomainTokenPos] = "" } return tokens, nil diff --git a/vendor/github.com/nats-io/nats.go/jetstream/consumer.go b/vendor/github.com/nats-io/nats.go/jetstream/consumer.go index ec196b62e0..55a8abf236 100644 --- a/vendor/github.com/nats-io/nats.go/jetstream/consumer.go +++ b/vendor/github.com/nats-io/nats.go/jetstream/consumer.go @@ -54,6 +54,11 @@ type ( // defaults to 30 seconds and can be configured using FetchMaxWait // option. // + // Use FetchMaxBytes to also limit the total size of delivered messages. + // The request completes when either limit is reached, or the timeout + // expires. For example, Fetch(500, FetchMaxBytes(32<<20)) retrieves up to + // 500 messages and up to 32 MiB, including subjects, reply subjects and headers. + // // By default, Fetch uses a 5s idle heartbeat for requests longer than // 10 seconds. For shorter requests, the idle heartbeat is disabled. // This can be configured using FetchHeartbeat option. If a client does @@ -206,7 +211,7 @@ func (p *pullConsumer) Info(ctx context.Context) (*ConsumerInfo, error) { return nil, ErrConsumerNotFound } - p.info = resp.ConsumerInfo + p.info.Store(resp.ConsumerInfo) return resp.ConsumerInfo, nil } @@ -214,7 +219,7 @@ func (p *pullConsumer) Info(ctx context.Context) (*ConsumerInfo, error) { // This method does not perform any network requests. The cached // ConsumerInfo is updated on every call to Info and Update. func (p *pullConsumer) CachedInfo() *ConsumerInfo { - return p.info + return p.info.Load() } // Info fetches current ConsumerInfo from the server. @@ -239,7 +244,7 @@ func (p *pushConsumer) Info(ctx context.Context) (*ConsumerInfo, error) { return nil, ErrConsumerNotFound } - p.info = resp.ConsumerInfo + p.info.Store(resp.ConsumerInfo) return resp.ConsumerInfo, nil } @@ -247,7 +252,7 @@ func (p *pushConsumer) Info(ctx context.Context) (*ConsumerInfo, error) { // This method does not perform any network requests. The cached // ConsumerInfo is updated on every call to Info and Update. func (p *pushConsumer) CachedInfo() *ConsumerInfo { - return p.info + return p.info.Load() } func upsertPullConsumer(ctx context.Context, js *jetStream, stream string, cfg ConsumerConfig, action string) (Consumer, error) { @@ -256,14 +261,15 @@ func upsertPullConsumer(ctx context.Context, js *jetStream, stream string, cfg C return nil, err } - return &pullConsumer{ + c := &pullConsumer{ js: js, stream: stream, name: resp.Name, durable: cfg.Durable != "", - info: resp.ConsumerInfo, subs: syncx.Map[string, *pullSubscription]{}, - }, nil + } + c.info.Store(resp.ConsumerInfo) + return c, nil } func upsertPushConsumer(ctx context.Context, js *jetStream, stream string, cfg ConsumerConfig, action string) (PushConsumer, error) { @@ -276,12 +282,13 @@ func upsertPushConsumer(ctx context.Context, js *jetStream, stream string, cfg C return nil, err } - return &pushConsumer{ + c := &pushConsumer{ js: js, stream: stream, name: resp.Name, - info: resp.ConsumerInfo, - }, nil + } + c.info.Store(resp.ConsumerInfo) + return c, nil } func upsertConsumer(ctx context.Context, js *jetStream, stream string, cfg ConsumerConfig, action string) (*consumerInfoResponse, error) { @@ -380,9 +387,9 @@ func getConsumer(ctx context.Context, js *jetStream, stream, name string) (Consu stream: stream, name: name, durable: info.Config.Durable != "", - info: info, subs: syncx.Map[string, *pullSubscription]{}, } + cons.info.Store(info) return cons, nil } @@ -401,8 +408,8 @@ func getPushConsumer(ctx context.Context, js *jetStream, stream, name string) (P js: js, stream: stream, name: name, - info: info, } + cons.info.Store(info) return cons, nil } diff --git a/vendor/github.com/nats-io/nats.go/jetstream/errors.go b/vendor/github.com/nats-io/nats.go/jetstream/errors.go index 36b8d7c33c..ff8274189e 100644 --- a/vendor/github.com/nats-io/nats.go/jetstream/errors.go +++ b/vendor/github.com/nats-io/nats.go/jetstream/errors.go @@ -52,8 +52,9 @@ const ( JSErrCodeMessageNotFound ErrorCode = 10037 JSErrCodeJetStreamNotEnabledForAccount ErrorCode = 10039 - JSErrCodeStreamNameInUse ErrorCode = 10058 - JSErrCodeStreamNotFound ErrorCode = 10059 + JSErrCodeStreamMessageExceedsMaximum ErrorCode = 10054 + JSErrCodeStreamNameInUse ErrorCode = 10058 + JSErrCodeStreamNotFound ErrorCode = 10059 JSErrCodeStreamWrongLastSequence ErrorCode = 10071 JSErrCodeJetStreamNotEnabled ErrorCode = 10076 @@ -98,6 +99,10 @@ var ( // not enabled for an account. ErrJetStreamNotEnabledForAccount JetStreamError = &jsError{apiErr: &APIError{ErrorCode: JSErrCodeJetStreamNotEnabledForAccount, Description: "jetstream not enabled for account", Code: 503}} + // ErrStreamMessageExceedsMaximum is an error returned when a published + // message exceeds the maximum message size allowed by the stream. + ErrStreamMessageExceedsMaximum JetStreamError = &jsError{apiErr: &APIError{ErrorCode: JSErrCodeStreamMessageExceedsMaximum, Description: "message size exceeds maximum allowed", Code: 400}} + // ErrStreamNotFound is an error returned when stream with given name does // not exist. ErrStreamNotFound JetStreamError = &jsError{apiErr: &APIError{ErrorCode: JSErrCodeStreamNotFound, Description: "stream not found", Code: 404}} diff --git a/vendor/github.com/nats-io/nats.go/jetstream/jetstream.go b/vendor/github.com/nats-io/nats.go/jetstream/jetstream.go index 75654dd9ea..c7b32ddc57 100644 --- a/vendor/github.com/nats-io/nats.go/jetstream/jetstream.go +++ b/vendor/github.com/nats-io/nats.go/jetstream/jetstream.go @@ -108,6 +108,10 @@ type ( // PublishAsyncComplete returns a channel that will be closed when all // outstanding asynchronously published messages are acknowledged by the // server. + // + // Callers blocked in PublishMsgAsync waiting for a free slot under + // PublishAsyncMaxPending are not outstanding yet, so the channel may + // be closed while such a caller is still about to publish. PublishAsyncComplete() <-chan struct{} // CleanupPublisher will clean up the publishing side of JetStreamContext. @@ -657,11 +661,12 @@ func (js *jetStream) CreateStream(ctx context.Context, cfg StreamConfig) (Stream } } - return &stream{ + s := &stream{ js: js, name: cfg.Name, - info: resp.StreamInfo, - }, nil + } + s.info.Store(resp.StreamInfo) + return s, nil } // If we have a Domain, convert to the appropriate ext.APIPrefix. @@ -783,11 +788,12 @@ func (js *jetStream) UpdateStream(ctx context.Context, cfg StreamConfig) (Stream } } - return &stream{ + s := &stream{ js: js, name: cfg.Name, - info: resp.StreamInfo, - }, nil + } + s.info.Store(resp.StreamInfo) + return s, nil } // CreateOrUpdateStream creates a stream with the given config. If stream @@ -830,11 +836,12 @@ func (js *jetStream) Stream(ctx context.Context, name string) (Stream, error) { if resp.StreamInfo == nil { return nil, ErrInvalidJetStreamResponse } - return &stream{ + s := &stream{ js: js, name: name, - info: resp.StreamInfo, - }, nil + } + s.info.Store(resp.StreamInfo) + return s, nil } // DeleteStream removes a stream with given name diff --git a/vendor/github.com/nats-io/nats.go/jetstream/jetstream_options.go b/vendor/github.com/nats-io/nats.go/jetstream/jetstream_options.go index 1e30f38cf0..45690e6585 100644 --- a/vendor/github.com/nats-io/nats.go/jetstream/jetstream_options.go +++ b/vendor/github.com/nats-io/nats.go/jetstream/jetstream_options.go @@ -521,6 +521,27 @@ func FetchPriorityGroup(group string) FetchOpt { } } +// FetchMaxBytes limits the total size of messages returned by [Consumer.Fetch], +// including subjects, reply subjects and headers. Fetch completes when either +// the message count or byte limit is reached, or the request expires. +// If the next message does not fit in the remaining byte budget, it is left +// available for a subsequent request. +// +// The limit must be greater than zero. This option is not supported by +// [Consumer.FetchBytes] or [Consumer.Next]. +func FetchMaxBytes(maxBytes int) FetchOpt { + return func(req *pullRequest) error { + if maxBytes <= 0 { + return fmt.Errorf("%w: max bytes must be greater than 0", ErrInvalidOption) + } + if !req.allowMaxBytes { + return fmt.Errorf("%w: FetchMaxBytes is only supported by Fetch", ErrInvalidOption) + } + req.MaxBytes = maxBytes + return nil + } +} + // FetchMaxWait sets custom timeout for fetching predefined batch of messages. // // If not provided, a default of 30 seconds will be used. diff --git a/vendor/github.com/nats-io/nats.go/jetstream/kv.go b/vendor/github.com/nats-io/nats.go/jetstream/kv.go index a94f09f7a1..4a87af717b 100644 --- a/vendor/github.com/nats-io/nats.go/jetstream/kv.go +++ b/vendor/github.com/nats-io/nats.go/jetstream/kv.go @@ -709,6 +709,7 @@ func (js *jetStream) prepareKeyValueConfig(ctx context.Context, cfg KeyValueConf scfg.Sources = append(scfg.Sources, ss) continue } + ss = ss.copy() var sourceBucketName string if strings.HasPrefix(ss.Name, kvBucketNamePre) { sourceBucketName = ss.Name[len(kvBucketNamePre):] diff --git a/vendor/github.com/nats-io/nats.go/jetstream/message.go b/vendor/github.com/nats-io/nats.go/jetstream/message.go index e8e1b5014c..aa0814e45a 100644 --- a/vendor/github.com/nats-io/nats.go/jetstream/message.go +++ b/vendor/github.com/nats-io/nats.go/jetstream/message.go @@ -312,13 +312,9 @@ var ( // Metadata returns [MsgMetadata] for a JetStream message. func (m *jetStreamMsg) Metadata() (*MsgMetadata, error) { - if err := m.checkReply(); err != nil { - return nil, err - } - - tokens, err := parser.GetMetadataFields(m.msg.Reply) + tokens, err := m.metadataFields() if err != nil { - return nil, fmt.Errorf("%w: %s", ErrNotJSMessage, err) + return nil, err } meta := &MsgMetadata{ @@ -334,6 +330,16 @@ func (m *jetStreamMsg) Metadata() (*MsgMetadata, error) { return meta, nil } +func (m *jetStreamMsg) metadataFields() (tokens [parser.AckNumPendingTokenPos + 1]string, err error) { + if err = m.checkReply(); err != nil { + return tokens, err + } + if tokens, err = parser.GetMetadataFields(m.msg.Reply); err != nil { + return tokens, fmt.Errorf("%w: %s", ErrNotJSMessage, err) + } + return tokens, nil +} + // Data returns the message body. func (m *jetStreamMsg) Data() []byte { return m.msg.Data diff --git a/vendor/github.com/nats-io/nats.go/jetstream/object.go b/vendor/github.com/nats-io/nats.go/jetstream/object.go index 8fa9bebd77..5d538d5213 100644 --- a/vendor/github.com/nats-io/nats.go/jetstream/object.go +++ b/vendor/github.com/nats-io/nats.go/jetstream/object.go @@ -1138,7 +1138,7 @@ func (obs *obs) PutFile(ctx context.Context, file string) (*ObjectInfo, error) { // GetFile is a convenience function to pull and object and place in a file. func (obs *obs) GetFile(ctx context.Context, name, file string, opts ...GetObjectOpt) error { // Expect file to be new. - f, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE, 0600) + f, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } @@ -1294,19 +1294,19 @@ func (obs *obs) Watch(ctx context.Context, opts ...WatchOpt) (ObjectWatcher, err if err := json.Unmarshal(m.Data, &info); err != nil { return // TODO(dlc) - Communicate this upwards? } - meta, err := m.Metadata() + tokens, err := parser.GetMetadataFields(m.Reply) if err != nil { return } if !o.ignoreDeletes || !info.Deleted { - info.ModTime = meta.Timestamp + info.ModTime = time.Unix(0, int64(parser.ParseNum(tokens[parser.AckTimestampSeqTokenPos]))) w.updates <- &info } // if UpdatesOnly is set, no not send nil to the channel // as it would always be triggered after initializing the watcher - if !initDoneMarker && meta.NumPending == 0 { + if !initDoneMarker && parser.ParseNum(tokens[parser.AckNumPendingTokenPos]) == 0 { initDoneMarker = true w.updates <- nil } diff --git a/vendor/github.com/nats-io/nats.go/jetstream/ordered.go b/vendor/github.com/nats-io/nats.go/jetstream/ordered.go index 1e89a4de8c..0856c2ad40 100644 --- a/vendor/github.com/nats-io/nats.go/jetstream/ordered.go +++ b/vendor/github.com/nats-io/nats.go/jetstream/ordered.go @@ -17,6 +17,7 @@ import ( "context" "errors" "fmt" + "slices" "strconv" "strings" "sync" @@ -24,6 +25,7 @@ import ( "time" "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/internal/parser" ) type ( @@ -40,10 +42,10 @@ type ( doReset chan struct{} resetInProgress atomic.Uint32 userErrHandler ConsumeErrHandler - stopAfter int - stopAfterMsgsLeft chan int - withStopAfter bool + stopAfter int // StopAfter of Consume or Messages, 0 if not set + deliveredMessages int // messages handed to the caller, across resets runningFetch *fetchResult + fetchInProgress bool subscription *orderedSubscription sync.Mutex } @@ -99,15 +101,18 @@ func (c *orderedConsumer) Consume(handler MessageHandler, opts ...PullConsumeOpt return nil, fmt.Errorf("%w: %s", ErrInvalidOption, err) } c.userErrHandler = consumeOpts.ErrHandler - opts = append(opts, consumeReconnectNotify(), - ConsumeErrHandler(c.errHandler(c.serial))) - if consumeOpts.StopAfter > 0 { - c.withStopAfter = true - c.stopAfter = consumeOpts.StopAfter - } - c.stopAfterMsgsLeft = make(chan int, 1) - if c.stopAfter > 0 { - opts = append(opts, consumeStopAfterNotify(c.stopAfter, c.stopAfterMsgsLeft)) + c.stopAfter = max(consumeOpts.StopAfter, 0) + c.deliveredMessages = 0 + // Preserve the resolved heartbeat when creating the pull consumer. + opts = append(opts, PullHeartbeat(consumeOpts.Heartbeat), consumeReconnectNotify()) + // innerOpts returns the options for the inner pull subscription of the + // current serial. Lock should be held when calling it. + innerOpts := func() []PullConsumeOpt { + o := append(slices.Clip(opts), ConsumeErrHandler(c.errHandler(c.serial))) + if c.stopAfter > 0 { + o = append(o, StopAfter(c.stopAfter-c.deliveredMessages)) + } + return o } sub := &orderedSubscription{ consumer: c, @@ -122,27 +127,36 @@ func (c *orderedConsumer) Consume(handler MessageHandler, opts ...PullConsumeOpt c.Unlock() return } - meta, err := msg.Metadata() + tokens, err := msg.(*jetStreamMsg).metadataFields() if err != nil { currentSub := c.currentSub c.Unlock() c.errHandler(serial)(currentSub, err) return } - dseq := meta.Sequence.Consumer + dseq := parser.ParseNum(tokens[parser.AckConsumerSeqTokenPos]) if dseq != c.cursor.deliverSeq+1 { c.Unlock() c.errHandler(serial)(sub, errOrderedSequenceMismatch) return } + if c.stopAfter > 0 && c.deliveredMessages >= c.stopAfter { + c.Unlock() + return + } c.cursor.deliverSeq = dseq - c.cursor.streamSeq = meta.Sequence.Stream + c.cursor.streamSeq = parser.ParseNum(tokens[parser.AckStreamSeqTokenPos]) + c.deliveredMessages++ + last := c.stopAfter > 0 && c.deliveredMessages == c.stopAfter c.Unlock() handler(msg) + if last { + sub.Stop() + } } } - cc, err := c.currentConsumer.Consume(internalHandler(c.serial), opts...) + cc, err := c.currentConsumer.Consume(internalHandler(c.serial), innerOpts()...) if err != nil { return nil, err } @@ -152,35 +166,25 @@ func (c *orderedConsumer) Consume(handler MessageHandler, opts ...PullConsumeOpt for { select { case <-c.doReset: + c.Lock() + done := c.stopAfter > 0 && c.deliveredMessages >= c.stopAfter + c.Unlock() + if done { + sub.Stop() + return + } if err := c.reset(); err != nil { if errors.Is(err, errOrderedConsumerClosed) { continue } c.errHandler(c.serial)(c.currentSub, err) } - if c.withStopAfter { - select { - case c.stopAfter = <-c.stopAfterMsgsLeft: - default: - } - if c.stopAfter <= 0 { - sub.Stop() - return - } - } - if c.stopAfter > 0 { - opts = opts[:len(opts)-2] - } else { - opts = opts[:len(opts)-1] - } - - // overwrite the previous err handler to use the new serial - opts = append(opts, ConsumeErrHandler(c.errHandler(c.serial))) - if c.withStopAfter { - opts = append(opts, consumeStopAfterNotify(c.stopAfter, c.stopAfterMsgsLeft)) - } - if cc, err := c.currentConsumer.Consume(internalHandler(c.serial), opts...); err != nil { - c.errHandler(c.serial)(cc, err) + c.Lock() + serial := c.serial + consumeOpts := innerOpts() + c.Unlock() + if cc, err := c.currentConsumer.Consume(internalHandler(serial), consumeOpts...); err != nil { + c.errHandler(serial)(cc, err) } else { c.Lock() c.currentSub = cc.(*pullSubscription) @@ -194,12 +198,6 @@ func (c *orderedConsumer) Consume(handler MessageHandler, opts ...PullConsumeOpt sub.consumer.Unlock() } return - case msgsLeft, ok := <-c.stopAfterMsgsLeft: - if !ok { - close(sub.done) - } - c.stopAfter = msgsLeft - return } } }() @@ -260,18 +258,14 @@ func (c *orderedConsumer) Messages(opts ...PullMessagesOpt) (MessagesContext, er if err != nil { return nil, fmt.Errorf("%w: %s", ErrInvalidOption, err) } + // Preserve the resolved heartbeat when creating the pull consumer. opts = append(opts, + PullHeartbeat(consumeOpts.Heartbeat), WithMessagesErrOnMissingHeartbeat(true), messagesReconnectNotify()) - c.stopAfterMsgsLeft = make(chan int, 1) - if consumeOpts.StopAfter > 0 { - c.withStopAfter = true - c.stopAfter = consumeOpts.StopAfter - } + c.stopAfter = max(consumeOpts.StopAfter, 0) + c.deliveredMessages = 0 c.userErrHandler = consumeOpts.ErrHandler - if c.stopAfter > 0 { - opts = append(opts, messagesStopAfterNotify(c.stopAfter, c.stopAfterMsgsLeft)) - } cc, err := c.currentConsumer.Messages(opts...) if err != nil { return nil, err @@ -307,60 +301,65 @@ func (s *orderedSubscription) Next(opts ...NextOpt) (Msg, error) { s.Stop() return nil, err } - if s.consumer.withStopAfter { - select { - case s.consumer.stopAfter = <-s.consumer.stopAfterMsgsLeft: - default: - } - if s.consumer.stopAfter <= 0 { - s.Stop() - return nil, ErrMsgIteratorClosed - } - s.opts[len(s.opts)-1] = StopAfter(s.consumer.stopAfter) - } - if err := s.consumer.reset(); err != nil { - if errors.Is(err, errOrderedConsumerClosed) { - return nil, ErrMsgIteratorClosed - } + if err := s.resetMessages(); err != nil { return nil, err } - cc, err := s.consumer.currentConsumer.Messages(s.opts...) - if err != nil { - return nil, err - } - s.consumer.currentSub = cc.(*pullSubscription) continue } - meta, err := msg.Metadata() + tokens, err := msg.(*jetStreamMsg).metadataFields() if err != nil { return nil, err } - serial := serialNumberFromConsumer(meta.Consumer) + serial := serialNumberFromConsumer(tokens[parser.AckConsumerTokenPos]) if serial != s.consumer.serial { continue } - dseq := meta.Sequence.Consumer + dseq := parser.ParseNum(tokens[parser.AckConsumerSeqTokenPos]) if dseq != s.consumer.cursor.deliverSeq+1 { - if err := s.consumer.reset(); err != nil { - if errors.Is(err, errOrderedConsumerClosed) { - return nil, ErrMsgIteratorClosed - } + if err := s.resetMessages(); err != nil { return nil, err } - cc, err := s.consumer.currentConsumer.Messages(s.opts...) - if err != nil { - return nil, err - } - s.consumer.currentSub = cc.(*pullSubscription) continue } s.consumer.cursor.deliverSeq = dseq - s.consumer.cursor.streamSeq = meta.Sequence.Stream + s.consumer.cursor.streamSeq = parser.ParseNum(tokens[parser.AckStreamSeqTokenPos]) + s.consumer.deliveredMessages++ return msg, nil } } +// resetMessages replaces the inner iterator with a new one, created on a +// new consumer which resumes after the last message returned by Next. +// With StopAfter set, the new iterator is limited to the messages that +// are left to deliver. +func (s *orderedSubscription) resetMessages() error { + c := s.consumer + opts := s.opts + if c.stopAfter > 0 { + remaining := c.stopAfter - c.deliveredMessages + if remaining <= 0 { + s.Stop() + return ErrMsgIteratorClosed + } + opts = append(slices.Clip(opts), StopAfter(remaining)) + } + if err := c.reset(); err != nil { + if errors.Is(err, errOrderedConsumerClosed) { + return ErrMsgIteratorClosed + } + return err + } + cc, err := c.currentConsumer.Messages(opts...) + if err != nil { + return err + } + c.Lock() + c.currentSub = cc.(*pullSubscription) + c.Unlock() + return nil +} + func (s *orderedSubscription) Stop() { if !s.closed.CompareAndSwap(0, 1) { return @@ -418,13 +417,28 @@ func (s *orderedSubscription) Closed() <-chan struct{} { // reset the consumer for each subsequent Fetch call. // Consider using [Consumer.Consume] or [Consumer.Messages] instead. func (c *orderedConsumer) Fetch(batch int, opts ...FetchOpt) (MessageBatch, error) { + return c.fetchWithOpts(batch, true, opts...) +} + +func (c *orderedConsumer) fetchWithOpts(batch int, allowMaxBytes bool, opts ...FetchOpt) (MessageBatch, error) { + return c.fetch(func(p *pullConsumer) (MessageBatch, error) { + return p.fetchWithOpts(batch, allowMaxBytes, opts...) + }) +} + +func (c *orderedConsumer) fetch(f func(*pullConsumer) (MessageBatch, error)) (MessageBatch, error) { c.Lock() if c.consumerType == consumerTypeConsume { c.Unlock() return nil, ErrOrderConsumerUsedAsConsume } + if c.fetchInProgress { + c.Unlock() + return nil, ErrOrderedConsumerConcurrentRequests + } if c.runningFetch != nil { if !c.runningFetch.closed() { + c.Unlock() return nil, ErrOrderedConsumerConcurrentRequests } if c.runningFetch.sseq != 0 { @@ -437,12 +451,16 @@ func (c *orderedConsumer) Fetch(batch int, opts ...FetchOpt) (MessageBatch, erro done: make(chan struct{}), } c.subscription = &sub + c.fetchInProgress = true c.Unlock() err := c.reset() - if err != nil { - return nil, err + var msgs MessageBatch + if err == nil { + msgs, err = f(c.currentConsumer) } - msgs, err := c.currentConsumer.Fetch(batch, opts...) + c.Lock() + defer c.Unlock() + c.fetchInProgress = false if err != nil { return nil, err } @@ -458,36 +476,9 @@ func (c *orderedConsumer) Fetch(batch int, opts ...FetchOpt) (MessageBatch, erro // reset the consumer for each subsequent Fetch call. // Consider using [Consumer.Consume] or [Consumer.Messages] instead. func (c *orderedConsumer) FetchBytes(maxBytes int, opts ...FetchOpt) (MessageBatch, error) { - c.Lock() - if c.consumerType == consumerTypeConsume { - c.Unlock() - return nil, ErrOrderConsumerUsedAsConsume - } - if c.runningFetch != nil { - if !c.runningFetch.closed() { - return nil, ErrOrderedConsumerConcurrentRequests - } - if c.runningFetch.sseq != 0 { - c.cursor.streamSeq = c.runningFetch.sseq - } - } - c.consumerType = consumerTypeFetch - sub := orderedSubscription{ - consumer: c, - done: make(chan struct{}), - } - c.subscription = &sub - c.Unlock() - err := c.reset() - if err != nil { - return nil, err - } - msgs, err := c.currentConsumer.FetchBytes(maxBytes, opts...) - if err != nil { - return nil, err - } - c.runningFetch = msgs.(*fetchResult) - return msgs, nil + return c.fetch(func(p *pullConsumer) (MessageBatch, error) { + return p.FetchBytes(maxBytes, opts...) + }) } // FetchNoWait is used to retrieve up to a provided number of messages @@ -499,23 +490,9 @@ func (c *orderedConsumer) FetchBytes(maxBytes int, opts ...FetchOpt) (MessageBat // reset the consumer for each subsequent Fetch call. // Consider using [Consumer.Consume] or [Consumer.Messages] instead. func (c *orderedConsumer) FetchNoWait(batch int) (MessageBatch, error) { - if c.consumerType == consumerTypeConsume { - return nil, ErrOrderConsumerUsedAsConsume - } - if c.runningFetch != nil && !c.runningFetch.done { - return nil, ErrOrderedConsumerConcurrentRequests - } - c.consumerType = consumerTypeFetch - sub := orderedSubscription{ - consumer: c, - done: make(chan struct{}), - } - c.subscription = &sub - err := c.reset() - if err != nil { - return nil, err - } - return c.currentConsumer.FetchNoWait(batch) + return c.fetch(func(p *pullConsumer) (MessageBatch, error) { + return p.FetchNoWait(batch) + }) } // Next is used to retrieve the next message from the stream. This @@ -526,7 +503,7 @@ func (c *orderedConsumer) FetchNoWait(batch int) (MessageBatch, error) { // reset the consumer for each subsequent Fetch call. // Consider using [Consumer.Consume] or [Consumer.Messages] instead. func (c *orderedConsumer) Next(opts ...FetchOpt) (Msg, error) { - res, err := c.Fetch(1, opts...) + res, err := c.fetchWithOpts(1, false, opts...) if err != nil { return nil, err } @@ -541,14 +518,11 @@ func (c *orderedConsumer) Next(opts ...FetchOpt) (Msg, error) { } func serialNumberFromConsumer(name string) int { - if len(name) == 0 { + i := strings.LastIndexByte(name, '_') + if i < 0 { return 0 } - parts := strings.Split(name, "_") - if len(parts) < 2 { - return 0 - } - serial, err := strconv.Atoi(parts[len(parts)-1]) + serial, err := strconv.Atoi(name[i+1:]) if err != nil { return 0 } @@ -674,22 +648,6 @@ func (c *orderedConsumer) getConsumerConfig() *ConsumerConfig { return cfg } -func consumeStopAfterNotify(numMsgs int, msgsLeftAfterStop chan int) PullConsumeOpt { - return pullOptFunc(func(opts *consumeOpts) error { - opts.StopAfter = numMsgs - opts.stopAfterMsgsLeft = msgsLeftAfterStop - return nil - }) -} - -func messagesStopAfterNotify(numMsgs int, msgsLeftAfterStop chan int) PullMessagesOpt { - return pullOptFunc(func(opts *consumeOpts) error { - opts.StopAfter = numMsgs - opts.stopAfterMsgsLeft = msgsLeftAfterStop - return nil - }) -} - func consumeReconnectNotify() PullConsumeOpt { return pullOptFunc(func(opts *consumeOpts) error { opts.notifyOnReconnect = true @@ -729,7 +687,7 @@ func (c *orderedConsumer) Info(ctx context.Context) (*ConsumerInfo, error) { return nil, ErrConsumerNotFound } - c.currentConsumer.info = resp.ConsumerInfo + c.currentConsumer.info.Store(resp.ConsumerInfo) return resp.ConsumerInfo, nil } @@ -742,7 +700,7 @@ func (c *orderedConsumer) CachedInfo() *ConsumerInfo { if c.currentConsumer == nil { return nil } - return c.currentConsumer.info + return c.currentConsumer.info.Load() } type backoffOpts struct { diff --git a/vendor/github.com/nats-io/nats.go/jetstream/publish.go b/vendor/github.com/nats-io/nats.go/jetstream/publish.go index 235ef25ba4..601d38f332 100644 --- a/vendor/github.com/nats-io/nats.go/jetstream/publish.go +++ b/vendor/github.com/nats-io/nats.go/jetstream/publish.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The NATS Authors +// Copyright 2022-2026 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -21,7 +21,6 @@ import ( "fmt" "math/rand" "strconv" - "strings" "sync" "time" @@ -159,6 +158,9 @@ const ( // Default number of retries DefaultPubRetryAttempts = 2 + + // Default max wait for an async pub ack if PublishAsyncMaxPending is exceeded. + defaultAsyncStallWait = 200 * time.Millisecond ) const ( @@ -175,32 +177,28 @@ func (js *jetStream) Publish(ctx context.Context, subj string, data []byte, opts return js.PublishMsg(ctx, &nats.Msg{Subject: subj, Data: data}, opts...) } -// PublishMsg performs a synchronous publish to a stream and waits for -// ack from server. It accepts subject name (which must be bound to a -// stream) and nats.Message. -func (js *jetStream) PublishMsg(ctx context.Context, m *nats.Msg, opts ...PublishOpt) (*PubAck, error) { - ctx, cancel := js.wrapContextWithoutDeadline(ctx) - if cancel != nil { - defer cancel() - } - o := pubOpts{ +// applyPublishOpts parses opts into a fresh pubOpts. +func applyPublishOpts(m *nats.Msg, opts []PublishOpt) (*pubOpts, error) { + o := &pubOpts{ retryWait: DefaultPubRetryWait, retryAttempts: DefaultPubRetryAttempts, } - if len(opts) > 0 { - if m.Header == nil { - m.Header = nats.Header{} - } - for _, opt := range opts { - if err := opt(&o); err != nil { - return nil, err - } - } - } - if o.stallWait > 0 { - return nil, fmt.Errorf("%w: stall wait cannot be set to sync publish", ErrInvalidOption) + + if m.Header == nil { + m.Header = nats.Header{} } + for _, opt := range opts { + if err := opt(o); err != nil { + return nil, err + } + } + + return o, nil +} + +// applyPublishHeaders translates o into message headers on m. +func applyPublishHeaders(m *nats.Msg, o *pubOpts) { if o.id != "" { m.Header.Set(MsgIDHeader, o.id) } @@ -241,18 +239,50 @@ func (js *jetStream) PublishMsg(ctx context.Context, m *nats.Msg, opts ...Publis if o.scheduleRollup != "" { m.Header.Set(ScheduleRollupHeader, o.scheduleRollup) } +} +// PublishMsg performs a synchronous publish to a stream and waits for +// ack from server. It accepts subject name (which must be bound to a +// stream) and nats.Message. +func (js *jetStream) PublishMsg(ctx context.Context, m *nats.Msg, opts ...PublishOpt) (*PubAck, error) { + ctx, cancel := js.wrapContextWithoutDeadline(ctx) + if cancel != nil { + defer cancel() + } + if len(opts) == 0 { + return js.publishMsg(ctx, m, DefaultPubRetryWait, DefaultPubRetryAttempts) + } + + o, err := applyPublishOpts(m, opts) + if err != nil { + return nil, err + } + + // stallWait isn't valid for a synchronous publish; check before + // applyPublishHeaders writes anything to m. + if o.stallWait > 0 { + return nil, fmt.Errorf("%w: stall wait cannot be set to sync publish", ErrInvalidOption) + } + + applyPublishHeaders(m, o) + + return js.publishMsg(ctx, m, o.retryWait, o.retryAttempts) +} + +// publishMsg sends m and waits for the JetStream ack, retrying on no +// responders per retryWait/retryAttempts. +func (js *jetStream) publishMsg(ctx context.Context, m *nats.Msg, retryWait time.Duration, retryAttempts int) (*PubAck, error) { var resp *nats.Msg var err error resp, err = js.conn.RequestMsgWithContext(ctx, m) if err != nil { - for r := 0; errors.Is(err, nats.ErrNoResponders) && (r < o.retryAttempts || o.retryAttempts < 0); r++ { + for r := 0; errors.Is(err, nats.ErrNoResponders) && (r < retryAttempts || retryAttempts < 0); r++ { // To protect against small blips in leadership changes etc, if we get a no responders here retry. select { case <-ctx.Done(): - case <-time.After(o.retryWait): + case <-time.After(retryWait): } resp, err = js.conn.RequestMsgWithContext(ctx, m) } @@ -288,69 +318,30 @@ func (js *jetStream) PublishAsync(subj string, data []byte, opts ...PublishOpt) // returns [PubAckFuture] interface. It accepts subject name (which must // be bound to a stream) and nats.Message. func (js *jetStream) PublishMsgAsync(m *nats.Msg, opts ...PublishOpt) (PubAckFuture, error) { - o := pubOpts{ - retryWait: DefaultPubRetryWait, - retryAttempts: DefaultPubRetryAttempts, + if len(opts) == 0 { + return js.publishMsgAsync(m, DefaultPubRetryWait, DefaultPubRetryAttempts, defaultAsyncStallWait, nil) } - if len(opts) > 0 { - if m.Header == nil { - m.Header = nats.Header{} - } - for _, opt := range opts { - if err := opt(&o); err != nil { - return nil, err - } - } - } - defaultStallWait := 200 * time.Millisecond - stallWait := defaultStallWait + o, err := applyPublishOpts(m, opts) + if err != nil { + return nil, err + } + + applyPublishHeaders(m, o) + + stallWait := defaultAsyncStallWait if o.stallWait > 0 { stallWait = o.stallWait } - if o.id != "" { - m.Header.Set(MsgIDHeader, o.id) - } - if o.lastMsgID != "" { - m.Header.Set(ExpectedLastMsgIDHeader, o.lastMsgID) - } - if o.stream != "" { - m.Header.Set(ExpectedStreamHeader, o.stream) - } - if o.lastSeq != nil { - m.Header.Set(ExpectedLastSeqHeader, strconv.FormatUint(*o.lastSeq, 10)) - } - if o.lastSubjectSeq != nil { - m.Header.Set(ExpectedLastSubjSeqHeader, strconv.FormatUint(*o.lastSubjectSeq, 10)) - } - if o.lastSubject != "" { - m.Header.Set(ExpectedLastSubjSeqSubjHeader, o.lastSubject) - m.Header.Set(ExpectedLastSubjSeqHeader, strconv.FormatUint(*o.lastSubjectSeq, 10)) - } - if o.ttl > 0 { - m.Header.Set(MsgTTLHeader, o.ttl.String()) - } - if o.schedule != "" { - m.Header.Set(ScheduleHeader, o.schedule) - } - if o.scheduleTarget != "" { - m.Header.Set(ScheduleTargetHeader, o.scheduleTarget) - } - if o.scheduleSource != "" { - m.Header.Set(ScheduleSourceHeader, o.scheduleSource) - } - if o.scheduleTTL != "" { - m.Header.Set(ScheduleTTLHeader, o.scheduleTTL) - } - if o.scheduleTZ != "" { - m.Header.Set(ScheduleTimeZoneHeader, o.scheduleTZ) - } - if o.scheduleRollup != "" { - m.Header.Set(ScheduleRollupHeader, o.scheduleRollup) - } + return js.publishMsgAsync(m, o.retryWait, o.retryAttempts, stallWait, o.pafRetry) +} - paf := o.pafRetry +// publishMsgAsync performs the actual async publish and paf bookkeeping. +// pafRetry is non-nil only when retrying an already-registered paf after +// a no-responders error. +func (js *jetStream) publishMsgAsync(m *nats.Msg, retryWait time.Duration, retryAttempts int, stallWait time.Duration, pafRetry *pubAckFuture) (PubAckFuture, error) { + paf := pafRetry if paf == nil && m.Reply != "" { return nil, ErrAsyncPublishReplySubjectSet } @@ -360,23 +351,16 @@ func (js *jetStream) PublishMsgAsync(m *nats.Msg, opts ...PublishOpt) (PubAckFut // register new paf if not retrying if paf == nil { - var err error - reply, err = js.newAsyncReply() - if err != nil { + if err := js.newAsyncReply(); err != nil { return nil, fmt.Errorf("nats: error creating async reply handler: %s", err) } - id = reply[js.opts.replyPrefixLen:] - paf = &pubAckFuture{msg: m, jsClient: js.publisher, maxRetries: o.retryAttempts, retryWait: o.retryWait, reply: reply} - numPending, maxPending := js.registerPAF(id, paf) - - if maxPending > 0 && numPending > maxPending { - select { - case <-js.asyncStall(): - case <-time.After(stallWait): - js.clearPAF(id) - return nil, ErrTooManyStalledMsgs - } + paf = &pubAckFuture{msg: m, jsClient: js.publisher, maxRetries: retryAttempts, retryWait: retryWait} + var err error + reply, err = js.registerPAF(paf, stallWait) + if err != nil { + return nil, err } + id = reply[js.opts.replyPrefixLen:] if js.publisher.ackTimeout > 0 { paf.timeout = time.AfterFunc(js.publisher.ackTimeout, func() { js.publisher.Lock() @@ -392,10 +376,7 @@ func (js *jetStream) PublishMsgAsync(m *nats.Msg, opts ...PublishOpt) (PubAckFut delete(js.publisher.acks, id) // check on anyone stalled and waiting. - if js.publisher.stallCh != nil && len(js.publisher.acks) < js.publisher.maxpa { - close(js.publisher.stallCh) - js.publisher.stallCh = nil - } + js.maybeUnstall() // send error to user paf.err = ErrAsyncPublishTimeout @@ -443,8 +424,12 @@ const ( aReplyTokensize = 6 ) -func (js *jetStream) newAsyncReply() (string, error) { +// newAsyncReply ensures the async reply subscription, prefix, RNG, and +// reconnect listener are set up. Reply tokens are allocated in registerPAF +// under the same lock that inserts into acks. +func (js *jetStream) newAsyncReply() error { js.publisher.Lock() + defer js.publisher.Unlock() if js.publisher.replySub == nil { // Create our wildcard reply subject. sha := sha256.New() @@ -456,8 +441,7 @@ func (js *jetStream) newAsyncReply() (string, error) { js.publisher.replyPrefix = fmt.Sprintf("%s%s.", js.opts.replyPrefix, b[:aReplyTokensize]) sub, err := js.conn.Subscribe(fmt.Sprintf("%s*", js.publisher.replyPrefix), js.handleAsyncReply) if err != nil { - js.publisher.Unlock() - return "", err + return err } js.publisher.replySub = sub js.publisher.rr = rand.New(rand.NewSource(time.Now().UnixNano())) @@ -466,25 +450,7 @@ func (js *jetStream) newAsyncReply() (string, error) { js.publisher.connStatusCh = js.conn.StatusChanged(nats.RECONNECTING, nats.CLOSED) go js.resetPendingAcksOnReconnect() } - var sb strings.Builder - sb.Grow(len(js.publisher.replyPrefix) + aReplyTokensize) - sb.WriteString(js.publisher.replyPrefix) - for { - rn := js.publisher.rr.Int63() - var b [aReplyTokensize]byte - for i, l := 0, rn; i < len(b); i++ { - b[i] = rdigits[l%base] - l /= base - } - if _, ok := js.publisher.acks[string(b[:])]; ok { - continue - } - sb.Write(b[:]) - break - } - - js.publisher.Unlock() - return sb.String(), nil + return nil } // Handle an async reply from PublishAsync. @@ -502,14 +468,6 @@ func (js *jetStream) handleAsyncReply(m *nats.Msg) { return } - closeStc := func() { - // Check on anyone stalled and waiting. - if js.publisher.stallCh != nil && len(js.publisher.acks) < js.publisher.maxpa { - close(js.publisher.stallCh) - js.publisher.stallCh = nil - } - } - closeDchFn := func() func() { var dch chan struct{} // Check on anyone one waiting on done status. @@ -555,7 +513,7 @@ func (js *jetStream) handleAsyncReply(m *nats.Msg) { return } delete(js.publisher.acks, id) - closeStc() + js.maybeUnstall() defer closeDchFn()() js.handleAsyncErrAndUnlock(paf, ErrNoStreamResponse) return @@ -563,7 +521,7 @@ func (js *jetStream) handleAsyncReply(m *nats.Msg) { // Remove delete(js.publisher.acks, id) - closeStc() + js.maybeUnstall() defer closeDchFn()() var pa pubAckResponse @@ -615,35 +573,85 @@ func (js *jetStream) resetPendingAcksOnReconnect() { } js.publisher.Lock() errCb := js.publisher.asyncPublisherOpts.aecb + var msgs []*nats.Msg for id, paf := range js.publisher.acks { paf.err = nats.ErrDisconnected if paf.errCh != nil { paf.errCh <- paf.err } if errCb != nil { - defer errCb(js, paf.msg, nats.ErrDisconnected) + msgs = append(msgs, paf.msg) } delete(js.publisher.acks, id) } - if js.publisher.doneCh != nil { - close(js.publisher.doneCh) - js.publisher.doneCh = nil - } + js.maybeUnstall() + dch := js.publisher.doneCh + js.publisher.doneCh = nil js.publisher.Unlock() + for _, msg := range msgs { + errCb(js, msg, nats.ErrDisconnected) + } + if dch != nil { + close(dch) + } } } -// registerPAF will register for a PubAckFuture. -func (js *jetStream) registerPAF(id string, paf *pubAckFuture) (int, int) { - js.publisher.Lock() - if js.publisher.acks == nil { - js.publisher.acks = make(map[string]*pubAckFuture) +// registerPAF will register for a PubAckFuture. If max pending is set and +// the publisher is already at the cap, it stalls until a slot frees or +// stallWait elapses. The reply token is drawn and inserted under the same +// lock hold so pending cannot exceed maxPending and ids cannot collide +// across stalled callers. +func (js *jetStream) registerPAF(paf *pubAckFuture, stallWait time.Duration) (string, error) { + var timer *time.Timer + defer func() { + if timer != nil { + timer.Stop() + } + }() + for { + js.publisher.Lock() + if js.publisher.acks == nil { + js.publisher.acks = make(map[string]*pubAckFuture) + } + maxpa := js.publisher.asyncPublisherOpts.maxpa + if maxpa > 0 && len(js.publisher.acks) >= maxpa { + if js.publisher.stallCh == nil { + js.publisher.stallCh = make(chan struct{}) + } + stc := js.publisher.stallCh + js.publisher.Unlock() + if timer == nil { + timer = time.NewTimer(stallWait) + } + select { + case <-stc: + continue + case <-timer.C: + return "", ErrTooManyStalledMsgs + } + } + // Allocate a unique reply token under the insert lock. + var id string + for { + rn := js.publisher.rr.Int63() + var b [aReplyTokensize]byte + for i, l := 0, rn; i < len(b); i++ { + b[i] = rdigits[l%base] + l /= base + } + if _, ok := js.publisher.acks[string(b[:])]; ok { + continue + } + id = string(b[:]) + break + } + reply := js.publisher.replyPrefix + id + paf.reply = reply + js.publisher.acks[id] = paf + js.publisher.Unlock() + return reply, nil } - js.publisher.acks[id] = paf - np := len(js.publisher.acks) - maxpa := js.publisher.asyncPublisherOpts.maxpa - js.publisher.Unlock() - return np, maxpa } // Lock should be held. @@ -654,23 +662,23 @@ func (js *jetStream) getPAF(id string) *pubAckFuture { return js.publisher.acks[id] } +// maybeUnstall closes stallCh when under the max pending cap so a waiter +// in registerPAF can proceed. Lock should be held. +func (js *jetStream) maybeUnstall() { + if js.publisher.stallCh != nil && len(js.publisher.acks) < js.publisher.maxpa { + close(js.publisher.stallCh) + js.publisher.stallCh = nil + } +} + // clearPAF will remove a PubAckFuture that was registered. func (js *jetStream) clearPAF(id string) { js.publisher.Lock() delete(js.publisher.acks, id) + js.maybeUnstall() js.publisher.Unlock() } -func (js *jetStream) asyncStall() <-chan struct{} { - js.publisher.Lock() - if js.publisher.stallCh == nil { - js.publisher.stallCh = make(chan struct{}) - } - stc := js.publisher.stallCh - js.publisher.Unlock() - return stc -} - func (paf *pubAckFuture) Ok() <-chan *PubAck { paf.jsClient.Lock() defer paf.jsClient.Unlock() @@ -716,6 +724,10 @@ func (js *jetStream) PublishAsyncPending() int { // PublishAsyncComplete returns a channel that will be closed when all // outstanding asynchronously published messages are acknowledged by the // server. +// +// Callers blocked in PublishMsgAsync waiting for a free slot under +// PublishAsyncMaxPending are not outstanding yet, so the channel may be +// closed while such a caller is still about to publish. func (js *jetStream) PublishAsyncComplete() <-chan struct{} { js.publisher.Lock() defer js.publisher.Unlock() diff --git a/vendor/github.com/nats-io/nats.go/jetstream/pull.go b/vendor/github.com/nats-io/nats.go/jetstream/pull.go index cb68c634dd..76390ff575 100644 --- a/vendor/github.com/nats-io/nats.go/jetstream/pull.go +++ b/vendor/github.com/nats-io/nats.go/jetstream/pull.go @@ -25,6 +25,7 @@ import ( "time" "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/internal/parser" "github.com/nats-io/nats.go/internal/syncx" "github.com/nats-io/nuid" ) @@ -89,7 +90,7 @@ type ( stream string durable bool name string - info *ConsumerInfo + info atomic.Pointer[ConsumerInfo] subs syncx.Map[string, *pullSubscription] pinID string } @@ -107,6 +108,7 @@ type ( Priority uint8 `json:"priority,omitempty"` ctx context.Context `json:"-"` maxWaitSet bool `json:"-"` + allowMaxBytes bool `json:"-"` } consumeOpts struct { @@ -124,7 +126,6 @@ type ( ThresholdMessages int ThresholdBytes int StopAfter int - stopAfterMsgsLeft chan int notifyOnReconnect bool } @@ -172,7 +173,8 @@ type ( FetchOpt func(*pullRequest) error hbMonitor struct { - timer *time.Timer + timer *time.Timer + closed bool sync.Mutex } @@ -208,12 +210,14 @@ func (p *pullConsumer) Consume(handler MessageHandler, opts ...PullConsumeOpt) ( return nil, fmt.Errorf("%w: %s", ErrInvalidOption, err) } - if len(p.info.Config.PriorityGroups) != 0 { + info := p.info.Load() + priorityGroups := info.Config.PriorityGroups + if len(priorityGroups) != 0 { if consumeOpts.Group == "" { return nil, fmt.Errorf("%w: %s", ErrInvalidOption, "priority group is required for priority consumer") } - if !slices.Contains(p.info.Config.PriorityGroups, consumeOpts.Group) { + if !slices.Contains(priorityGroups, consumeOpts.Group) { return nil, fmt.Errorf("%w: %s", ErrInvalidOption, "invalid priority group") } } else if consumeOpts.Group != "" { @@ -301,6 +305,11 @@ func (p *pullConsumer) Consume(handler MessageHandler, opts ...PullConsumeOpt) ( inbox := p.js.conn.NewInbox() sub.subscription, err = p.js.conn.Subscribe(inbox, internalHandler) if err != nil { + p.subs.Delete(sub.id) + p.js.conn.RemoveStatusListener(sub.connStatusChanged) + if sub.hbMonitor != nil { + sub.hbMonitor.Close() + } return nil, err } sub.subscription.SetClosedHandler(func(sid string) func(string) { @@ -319,13 +328,9 @@ func (p *pullConsumer) Consume(handler MessageHandler, opts ...PullConsumeOpt) ( sub.Lock() // initial pull sub.resetPendingMsgs() - batchSize := sub.consumeOpts.MaxMessages - if sub.consumeOpts.StopAfter > 0 { - batchSize = min(batchSize, sub.consumeOpts.StopAfter-sub.delivered) - } if err := sub.pull(&pullRequest{ Expires: consumeOpts.Expires, - Batch: batchSize, + Batch: sub.pending.msgCount, MaxBytes: consumeOpts.MaxBytes, Heartbeat: consumeOpts.Heartbeat, MinPending: consumeOpts.MinPending, @@ -363,9 +368,10 @@ func (p *pullConsumer) Consume(handler MessageHandler, opts ...PullConsumeOpt) ( sub.errs <- errConnected } - sub.fetchNext <- &pullRequest{ + sub.resetPendingMsgs() + sub.requestNext(&pullRequest{ Expires: sub.consumeOpts.Expires, - Batch: sub.consumeOpts.MaxMessages, + Batch: sub.pending.msgCount, MaxBytes: sub.consumeOpts.MaxBytes, Heartbeat: sub.consumeOpts.Heartbeat, MinPending: sub.consumeOpts.MinPending, @@ -373,11 +379,10 @@ func (p *pullConsumer) Consume(handler MessageHandler, opts ...PullConsumeOpt) ( Priority: sub.consumeOpts.Priority, Group: sub.consumeOpts.Group, PinID: p.getPinID(), - } + }) if sub.hbMonitor != nil { sub.hbMonitor.Reset(2 * sub.consumeOpts.Heartbeat) } - sub.resetPendingMsgs() } sub.Unlock() @@ -387,13 +392,10 @@ func (p *pullConsumer) Consume(handler MessageHandler, opts ...PullConsumeOpt) ( case err := <-sub.errs: sub.Lock() if errors.Is(err, ErrNoHeartbeat) { - batchSize := sub.consumeOpts.MaxMessages - if sub.consumeOpts.StopAfter > 0 { - batchSize = min(batchSize, sub.consumeOpts.StopAfter-sub.delivered) - } - sub.fetchNext <- &pullRequest{ + sub.resetPendingMsgs() + sub.requestNext(&pullRequest{ Expires: sub.consumeOpts.Expires, - Batch: batchSize, + Batch: sub.pending.msgCount, MaxBytes: sub.consumeOpts.MaxBytes, Heartbeat: sub.consumeOpts.Heartbeat, MinPending: sub.consumeOpts.MinPending, @@ -401,11 +403,10 @@ func (p *pullConsumer) Consume(handler MessageHandler, opts ...PullConsumeOpt) ( Priority: sub.consumeOpts.Priority, Group: sub.consumeOpts.Group, PinID: p.getPinID(), - } + }) if sub.hbMonitor != nil { sub.hbMonitor.Reset(2 * sub.consumeOpts.Heartbeat) } - sub.resetPendingMsgs() } sub.Unlock() if sub.consumeOpts.ErrHandler != nil { @@ -426,10 +427,13 @@ func (p *pullConsumer) Consume(handler MessageHandler, opts ...PullConsumeOpt) ( } // resetPendingMsgs resets pending message count and byte count -// to the values set in consumeOpts +// to the values set in consumeOpts, capped by the remaining StopAfter budget. // lock should be held before calling this method func (s *pullSubscription) resetPendingMsgs() { s.pending.msgCount = s.consumeOpts.MaxMessages + if s.consumeOpts.StopAfter > 0 { + s.pending.msgCount = min(s.pending.msgCount, s.consumeOpts.StopAfter-s.delivered) + } s.pending.byteCount = s.consumeOpts.MaxBytes } @@ -478,7 +482,7 @@ func (s *pullSubscription) checkPending() { if s.consumer != nil { pinID = s.consumer.getPinID() } - s.fetchNext <- &pullRequest{ + s.requestNext(&pullRequest{ Expires: s.consumeOpts.Expires, Batch: batchSize, MaxBytes: maxBytes, @@ -488,14 +492,27 @@ func (s *pullSubscription) checkPending() { MinPending: s.consumeOpts.MinPending, MinAckPending: s.consumeOpts.MinAckPending, Priority: s.consumeOpts.Priority, - } + }) - s.pending.msgCount = s.consumeOpts.MaxMessages + if s.consumeOpts.StopAfter > 0 { + s.pending.msgCount += batchSize + } else { + s.pending.msgCount = s.consumeOpts.MaxMessages + } s.pending.byteCount = s.consumeOpts.MaxBytes } } } +// requestNext queues req for pullMessages, giving up once the +// subscription is done. +func (s *pullSubscription) requestNext(req *pullRequest) { + select { + case s.fetchNext <- req: + case <-s.done: + } +} + // Messages returns MessagesContext, allowing continuously iterating // over messages on a stream. Messages cannot be used concurrently // when using ordered consumer. @@ -507,12 +524,14 @@ func (p *pullConsumer) Messages(opts ...PullMessagesOpt) (MessagesContext, error return nil, fmt.Errorf("%w: %s", ErrInvalidOption, err) } - if len(p.info.Config.PriorityGroups) != 0 { + info := p.info.Load() + priorityGroups := info.Config.PriorityGroups + if len(priorityGroups) != 0 { if consumeOpts.Group == "" { return nil, fmt.Errorf("%w: %s", ErrInvalidOption, "priority group is required for priority consumer") } - if !slices.Contains(p.info.Config.PriorityGroups, consumeOpts.Group) { + if !slices.Contains(priorityGroups, consumeOpts.Group) { return nil, fmt.Errorf("%w: %s", ErrInvalidOption, "invalid priority group") } } else if consumeOpts.Group != "" { @@ -539,6 +558,7 @@ func (p *pullConsumer) Messages(opts ...PullMessagesOpt) (MessagesContext, error sub.subscription, err = p.js.conn.ChanSubscribe(inbox, sub.msgs) if err != nil { p.Unlock() + p.js.conn.RemoveStatusListener(sub.connStatusChanged) return nil, err } sub.subscription.SetClosedHandler(func(sid string) func(string) { @@ -637,8 +657,9 @@ func (s *pullSubscription) Next(opts ...NextOpt) (Msg, error) { s.Lock() defer s.Unlock() - drainMode := s.draining.Load() == 1 + // closed has to be read before draining, see Drain. closed := s.closed.Load() == 1 + drainMode := s.draining.Load() == 1 if closed && !drainMode { // Check if iterator was closed due to connection closure if s.consumer.js.conn.IsClosed() { @@ -664,6 +685,10 @@ func (s *pullSubscription) Next(opts ...NextOpt) (Msg, error) { case msg, ok := <-s.msgs: if !ok { // if msgs channel is closed, it means that subscription was either drained or stopped + // or that the connection was closed. In the latter case + // nothing called Stop, so stop here to let pullMessages + // and the status goroutine exit. It is a no-op otherwise. + s.Stop() s.consumer.subs.Delete(s.id) s.draining.CompareAndSwap(1, 0) // Check if iterator was closed due to connection closure @@ -774,9 +799,25 @@ func (hb *hbMonitor) Stop() { hb.Unlock() } +// Reset re-arms the heartbeat timer. It is a no-op once the monitor +// has been closed. func (hb *hbMonitor) Reset(dur time.Duration) { hb.Lock() - hb.timer.Reset(dur) + if !hb.closed { + hb.timer.Reset(dur) + } + hb.Unlock() +} + +// Close stops the heartbeat timer for good. Unlike Stop, which only +// pauses the timer, a closed monitor can no longer be re-armed by Reset, +// so a message handler or status goroutine still in flight when the +// subscription ends cannot schedule the timer again. A pending timer +// keeps the subscription (and through it the connection) reachable. +func (hb *hbMonitor) Close() { + hb.Lock() + hb.closed = true + hb.timer.Stop() hb.Unlock() } @@ -788,13 +829,6 @@ func (s *pullSubscription) Stop() { return } close(s.done) - if s.consumeOpts.stopAfterMsgsLeft != nil { - if s.delivered >= s.consumeOpts.StopAfter { - close(s.consumeOpts.stopAfterMsgsLeft) - } else { - s.consumeOpts.stopAfterMsgsLeft <- s.consumeOpts.StopAfter - s.delivered - } - } } // Drain unsubscribes from the stream and cancels subscription. All @@ -802,18 +836,17 @@ func (s *pullSubscription) Stop() { // subsequent calls to Next. After the buffer is drained, Next will // return ErrMsgIteratorClosed error. func (s *pullSubscription) Drain() { + if s.closed.Load() == 1 { + return + } + // draining is set before closed so that anyone who sees the + // subscription closed by Drain also sees it draining, rather than + // treating it as stopped and discarding buffered messages. + s.draining.Store(1) if !s.closed.CompareAndSwap(0, 1) { return } - s.draining.Store(1) close(s.done) - if s.consumeOpts.stopAfterMsgsLeft != nil { - if s.delivered >= s.consumeOpts.StopAfter { - close(s.consumeOpts.stopAfterMsgsLeft) - } else { - s.consumeOpts.stopAfterMsgsLeft <- s.consumeOpts.StopAfter - s.delivered - } - } } // Closed returns a channel that is closed when consuming is @@ -837,10 +870,15 @@ func (s *pullSubscription) Closed() <-chan struct{} { // Fetch sends a single request to retrieve given number of messages. // It will wait up to provided expiry time if not all messages are available. func (p *pullConsumer) Fetch(batch int, opts ...FetchOpt) (MessageBatch, error) { + return p.fetchWithOpts(batch, true, opts...) +} + +func (p *pullConsumer) fetchWithOpts(batch int, allowMaxBytes bool, opts ...FetchOpt) (MessageBatch, error) { req := &pullRequest{ - Batch: batch, - Expires: DefaultExpires, - Heartbeat: unset, + Batch: batch, + Expires: DefaultExpires, + Heartbeat: unset, + allowMaxBytes: allowMaxBytes, } for _, opt := range opts { if err := opt(req); err != nil { @@ -978,14 +1016,14 @@ func (p *pullConsumer) fetch(req *pullRequest) (MessageBatch, error) { p.setPinID(pinId) } res.msgs <- p.js.toJSMsg(msg) - meta, err := msg.Metadata() + tokens, err := parser.GetMetadataFields(msg.Reply) if err != nil { res.err = fmt.Errorf("parsing message metadata: %s", err) res.done = true res.Unlock() return } - res.sseq = meta.Sequence.Stream + res.sseq = parser.ParseNum(tokens[parser.AckStreamSeqTokenPos]) receivedMsgs++ if req.MaxBytes != 0 { receivedBytes += msg.Size() @@ -1041,7 +1079,7 @@ func (fr *fetchResult) closed() bool { // method will block until the message is retrieved or timeout is // reached. func (p *pullConsumer) Next(opts ...FetchOpt) (Msg, error) { - res, err := p.Fetch(1, opts...) + res, err := p.fetchWithOpts(1, false, opts...) if err != nil { return nil, err } @@ -1066,7 +1104,12 @@ func (s *pullSubscription) pullMessages(subject string) { s.cleanup() return } - s.errs <- err + // Do not block forever on a full errs channel once + // nothing reads it; the next iteration will clean up. + select { + case s.errs <- err: + case <-s.done: + } } s.fetchInProgress.Store(0) case <-s.done: @@ -1088,7 +1131,10 @@ func (s *pullSubscription) scheduleHeartbeatCheck(dur time.Duration) *hbMonitor } return &hbMonitor{ timer: time.AfterFunc(2*dur, func() { - s.errs <- ErrNoHeartbeat + select { + case s.errs <- ErrNoHeartbeat: + case <-s.done: + } }), } } @@ -1099,15 +1145,22 @@ func (s *pullSubscription) cleanup() { // is already holding the lock and waiting. // The fields that are read (subscription, hbMonitor) // are read only (Only written on creation of pullSubscription). - if s.subscription == nil || !s.subscription.IsValid() { - return - } + + // The status listener and the heartbeat timer have to be released + // even if the subscription is no longer valid (e.g. the connection + // was closed before Stop or Drain was called). Otherwise the armed + // timer keeps the subscription and the connection reachable until it + // fires, and the listener stays registered on the connection. if s.consumer != nil { nc := s.consumer.js.conn nc.RemoveStatusListener(s.connStatusChanged) } if s.hbMonitor != nil { - s.hbMonitor.Stop() + s.hbMonitor.Close() + } + if s.subscription == nil || !s.subscription.IsValid() { + s.closed.Store(1) + return } drainMode := s.draining.Load() == 1 if drainMode { diff --git a/vendor/github.com/nats-io/nats.go/jetstream/push.go b/vendor/github.com/nats-io/nats.go/jetstream/push.go index 0ba279d4f2..346669dca5 100644 --- a/vendor/github.com/nats-io/nats.go/jetstream/push.go +++ b/vendor/github.com/nats-io/nats.go/jetstream/push.go @@ -17,7 +17,7 @@ type ( js *jetStream stream string name string - info *ConsumerInfo + info atomic.Pointer[ConsumerInfo] started atomic.Bool } @@ -25,6 +25,7 @@ type ( sync.Mutex id string errs chan error + conn *nats.Conn subscription *nats.Subscription connStatusChanged chan nats.Status closedCh chan struct{} @@ -58,7 +59,8 @@ func (p *pushConsumer) Consume(handler MessageHandler, opts ...PushConsumeOpt) ( p.Lock() defer p.Unlock() - if p.info == nil { + info := p.info.Load() + if info == nil { return nil, ErrConsumerNotFound } @@ -70,10 +72,11 @@ func (p *pushConsumer) Consume(handler MessageHandler, opts ...PushConsumeOpt) ( sub := &pushSubscription{ id: consumeID, errs: make(chan error, 1), + conn: p.js.conn, done: make(chan struct{}, 1), consumeOpts: consumeOpts, connStatusChanged: p.js.conn.StatusChanged(nats.CONNECTED, nats.RECONNECTING), - idleHeartbeat: p.info.Config.IdleHeartbeat, + idleHeartbeat: info.Config.IdleHeartbeat, } sub.hbMonitor = sub.scheduleHeartbeatCheck(sub.idleHeartbeat) @@ -105,12 +108,13 @@ func (p *pushConsumer) Consume(handler MessageHandler, opts ...PushConsumeOpt) ( } var err error - if p.info.Config.DeliverGroup != "" { - sub.subscription, err = p.js.conn.QueueSubscribe(p.info.Config.DeliverSubject, p.info.Config.DeliverGroup, internalHandler) + if info.Config.DeliverGroup != "" { + sub.subscription, err = p.js.conn.QueueSubscribe(info.Config.DeliverSubject, info.Config.DeliverGroup, internalHandler) } else { - sub.subscription, err = p.js.conn.Subscribe(p.info.Config.DeliverSubject, internalHandler) + sub.subscription, err = p.js.conn.Subscribe(info.Config.DeliverSubject, internalHandler) } if err != nil { + sub.release() return nil, err } @@ -216,9 +220,7 @@ func (s *pushSubscription) Stop() { defer s.Unlock() close(s.done) s.subscription.Unsubscribe() - if s.hbMonitor != nil { - s.hbMonitor.Stop() - } + s.release() } // Drain unsubscribes from the stream and cancels subscription. @@ -231,9 +233,18 @@ func (s *pushSubscription) Drain() { defer s.Unlock() close(s.done) s.subscription.Drain() + s.release() +} + +// release closes the heartbeat monitor, so that messages still being +// delivered while draining cannot re-arm it, and removes the connection +// status listener, which would otherwise stay registered on the +// connection for its lifetime. +func (s *pushSubscription) release() { if s.hbMonitor != nil { - s.hbMonitor.Stop() + s.hbMonitor.Close() } + s.conn.RemoveStatusListener(s.connStatusChanged) } // Closed returns a channel that is closed when consuming is diff --git a/vendor/github.com/nats-io/nats.go/jetstream/stream.go b/vendor/github.com/nats-io/nats.go/jetstream/stream.go index 019546768e..e1e6720d9d 100644 --- a/vendor/github.com/nats-io/nats.go/jetstream/stream.go +++ b/vendor/github.com/nats-io/nats.go/jetstream/stream.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "strconv" + "sync/atomic" "time" "github.com/nats-io/nats.go" @@ -169,7 +170,7 @@ type ( stream struct { name string - info *StreamInfo + info atomic.Pointer[StreamInfo] js *jetStream } @@ -495,7 +496,7 @@ func (s *stream) Info(ctx context.Context, opts ...StreamInfoOpt) (*StreamInfo, info.State.Subjects = nil // we don't want to store subjects in cache cached := *info - s.info = &cached + s.info.Store(&cached) info.State.Subjects = subjectMap break } @@ -508,7 +509,7 @@ func (s *stream) Info(ctx context.Context, opts ...StreamInfoOpt) (*StreamInfo, // This method does not perform any network requests. The cached // StreamInfo is updated on every call to Info and Update. func (s *stream) CachedInfo() *StreamInfo { - return s.info + return s.info.Load() } // Purge removes messages from a stream. It is a destructive operation. @@ -574,7 +575,9 @@ func (s *stream) getMsg(ctx context.Context, mreq *apiMsgGetRequest) (*RawStream var gmSubj string // handle direct gets - if s.info.Config.AllowDirect { + info := s.info.Load() + allowDirect := info != nil && info.Config.AllowDirect + if allowDirect { if mreq.LastFor != "" { gmSubj = fmt.Sprintf(apiDirectMsgGetLastBySubjectT, s.name, mreq.LastFor) r, err := s.js.apiRequest(ctx, gmSubj, nil) diff --git a/vendor/github.com/nats-io/nats.go/jetstream/stream_config.go b/vendor/github.com/nats-io/nats.go/jetstream/stream_config.go index a889e0a074..aeea4ac518 100644 --- a/vendor/github.com/nats-io/nats.go/jetstream/stream_config.go +++ b/vendor/github.com/nats-io/nats.go/jetstream/stream_config.go @@ -36,6 +36,9 @@ type ( // bytes, etc. State StreamState `json:"state"` + // Domain is the JetStream domain the stream is in. + Domain string `json:"domain,omitempty"` + // Cluster contains information about the cluster to which this stream // belongs (if applicable). Cluster *ClusterInfo `json:"cluster,omitempty"` @@ -50,6 +53,11 @@ type ( // data. Sources []*StreamSourceInfo `json:"sources,omitempty"` + // Alternates is a list of mirrors and alternate copies of this stream, + // along with the stream itself, that the client may use to read + // messages. Ordered by proximity to the requesting client. + Alternates []StreamAlternate `json:"alternates,omitempty"` + // TimeStamp indicates when the info was gathered by the server. TimeStamp time.Time `json:"ts"` } @@ -225,6 +233,10 @@ type ( // Name is the name of the stream that is being replicated. Name string `json:"name"` + // External contains the API and delivery prefixes used to reach the + // stream when it lives in another account or JetStream domain. + External *ExternalStream `json:"external,omitempty"` + // Lag informs how many messages behind the source/mirror operation is. // This will only show correctly if there is active communication // with stream/mirror. @@ -238,6 +250,10 @@ type ( // Value will be -1 when there has been no activity. Active time.Duration `json:"active"` + // Error is the error reported by the server if the source/mirror + // replication is failing. It is nil when replication is healthy. + Error *APIError `json:"error,omitempty"` + // FilterSubject is the subject filter defined for this source/mirror. FilterSubject string `json:"filter_subject,omitempty"` @@ -288,6 +304,34 @@ type ( // with message count per subject. This field will only be returned if // the stream has been fetched with the SubjectFilter option. Subjects map[string]uint64 `json:"subjects"` + + // Lost contains information about messages and bytes the server has + // lost, for example as a result of file store corruption. It is nil + // when no data has been lost. + Lost *LostStreamData `json:"lost,omitempty"` + } + + // LostStreamData contains information about data lost by the server for + // a stream. + LostStreamData struct { + // Msgs is the list of sequence numbers of the lost messages. + Msgs []uint64 `json:"msgs"` + + // Bytes is the number of bytes lost. + Bytes uint64 `json:"bytes"` + } + + // StreamAlternate is an alternate location of a stream, either a mirror + // or the stream itself. + StreamAlternate struct { + // Name is the name of the alternate stream. + Name string `json:"name"` + + // Domain is the JetStream domain the alternate stream belongs to. + Domain string `json:"domain,omitempty"` + + // Cluster is the name of the cluster hosting the alternate stream. + Cluster string `json:"cluster"` } // ClusterInfo shows information about the underlying set of servers that diff --git a/vendor/github.com/nats-io/nats.go/js.go b/vendor/github.com/nats-io/nats.go/js.go index 08720240dc..912ce1721c 100644 --- a/vendor/github.com/nats-io/nats.go/js.go +++ b/vendor/github.com/nats-io/nats.go/js.go @@ -94,6 +94,10 @@ type JetStream interface { // * If Bind() option is provided, the library will attempt to lookup the // consumer with the given name, and if successful, bind to it. If the lookup fails, // then the Subscribe() call will return an error. + // * If the library creates the consumer and MaxAckPending() is not set, the sync, + // channel and pull variants set it to the subscription channel capacity + // (SyncQueueLen(), or the capacity of the channel passed to ChanSubscribe()). + // Use MaxAckPending() if that exceeds a server, account or stream limit. Subscribe(subj string, cb MsgHandler, opts ...SubOpt) (*Subscription, error) // SubscribeSync creates a Subscription that can be used to process messages synchronously. @@ -756,21 +760,26 @@ func (js *js) resetPendingAcksOnReconnect() { } js.mu.Lock() errCb := js.opts.aecb + var msgs []*Msg for id, paf := range js.pafs { paf.err = ErrDisconnected if paf.errCh != nil { paf.errCh <- paf.err } if errCb != nil { - defer errCb(js, paf.msg, ErrDisconnected) + msgs = append(msgs, paf.msg) } delete(js.pafs, id) } - if js.dch != nil { - close(js.dch) - js.dch = nil - } + dch := js.dch + js.dch = nil js.mu.Unlock() + for _, msg := range msgs { + errCb(js, msg, ErrDisconnected) + } + if dch != nil { + close(dch) + } } } @@ -813,7 +822,7 @@ func (js *js) cleanupReplySub() { js.rsub = nil } if js.connStatusCh != nil { - close(js.connStatusCh) + js.nc.RemoveStatusListener(js.connStatusCh) js.connStatusCh = nil } js.mu.Unlock() @@ -2782,7 +2791,7 @@ func MaxDeliver(n int) SubOpt { } // MaxAckPending sets the number of outstanding acks that are allowed before -// message delivery is halted. +// message delivery is halted. See JetStream.Subscribe() for the default. func MaxAckPending(n int) SubOpt { return subOptFn(func(opts *subOpts) error { opts.cfg.MaxAckPending = n diff --git a/vendor/github.com/nats-io/nats.go/kv.go b/vendor/github.com/nats-io/nats.go/kv.go index e992d8737e..f4894c378e 100644 --- a/vendor/github.com/nats-io/nats.go/kv.go +++ b/vendor/github.com/nats-io/nats.go/kv.go @@ -790,7 +790,11 @@ func (kv *kvs) update(key string, value []byte, revision uint64) (uint64, error) if kv.useJSPfx { b.WriteString(kv.js.opts.pre) } - b.WriteString(kv.pre) + if kv.putPre != _EMPTY_ { + b.WriteString(kv.putPre) + } else { + b.WriteString(kv.pre) + } b.WriteString(key) m := Msg{Subject: b.String(), Header: Header{}, Data: value} diff --git a/vendor/github.com/nats-io/nats.go/nats.go b/vendor/github.com/nats-io/nats.go/nats.go index b909d42e27..1a4f463f32 100644 --- a/vendor/github.com/nats-io/nats.go/nats.go +++ b/vendor/github.com/nats-io/nats.go/nats.go @@ -15,7 +15,6 @@ package nats import ( - "bufio" "bytes" "crypto/tls" "crypto/x509" @@ -50,7 +49,7 @@ import ( // Default Constants const ( - Version = "1.54.0" + Version = "1.55.0" DefaultURL = "nats://127.0.0.1:4222" DefaultPort = 4222 DefaultMaxReconnect = 60 @@ -96,6 +95,13 @@ const ( // MAX_SUBSCRIPTIONS_ERR is for when nats server denies the connection due to server subscriptions limit. MAX_SUBSCRIPTIONS_ERR = "maximum subscriptions exceeded" + + // unknownProtoOpErr is sent by the server when it cannot parse what the + // client sent, after which it closes the connection. + unknownProtoOpErr = "unknown protocol operation" + + // maxControlLineErr is sent by the server when a protocol line is too long. + maxControlLineErr = "maximum control line exceeded" ) // Errors @@ -710,13 +716,20 @@ type natsReader struct { type natsWriter struct { w io.Writer + conn net.Conn bufs []byte limit int pending *bytes.Buffer plimit int + // Error of a partial write that caused conn to be closed. + partialWriteErr error } // Subscription represents interest in a given subject. +// +// A Subscription is safe for concurrent use by multiple goroutines, for +// example several goroutines may call NextMsg on the same Subscription. +// Each message is delivered to only one of the callers. type Subscription struct { mu sync.Mutex @@ -796,6 +809,11 @@ func (s SubStatus) String() string { // Msg represents a message delivered by NATS. This structure is used // by Subscribers and PublishMsg(). // +// A new Msg is allocated for every delivered message and is not reused by +// the library, so it is safe to keep a reference to it, and to its Data, +// after a message handler returns, for example to process it asynchronously. +// There is no need to copy the message first. +// // # Types of Acknowledgements // // In case using JetStream, there are multiple ways to ack a Msg: @@ -860,35 +878,43 @@ func (m *Msg) Size() int { if m.wsz != 0 { return m.wsz } - hdr, _ := m.headerBytes() - return len(m.Subject) + len(m.Reply) + len(hdr) + len(m.Data) + hdrSize, _ := m.headerSize() + return len(m.Subject) + len(m.Reply) + hdrSize + len(m.Data) +} + +// pendingSize returns the data and encoded header size of a received message. +func (m *Msg) pendingSize() int { + return m.wsz - len(m.Subject) - len(m.Reply) +} + +// Validate the keys and calculate the exact total encoded size. +// +// We don't perform any special validation on header values because we +// consider all strings to be valid values (including Unicode strings, +// special characters, etc.). +func (m *Msg) headerSize() (int, error) { + if len(m.Header) == 0 { + return 0, nil + } + + hdrSize := len(hdrLine) + len(crlf) + for k, vs := range m.Header { + if !isHeaderKeyValid(k) { + return 0, ErrBadHeaderMsg + } + for _, v := range vs { + hdrSize += len(k) + len(textproto.TrimString(v)) + 4 // 4 is for the colon, space, CR, and LF + } + } + + return hdrSize, nil } func (m *Msg) headerBytes() ([]byte, error) { var hdr []byte - if len(m.Header) == 0 { - return hdr, nil - } - - // Validate the keys and calculate an upper bound for the total encoded - // size. - // - // The size calculation may not be exact if any values contain - // leading/trailing whitespace (as those will be trimmed later), but a - // slight over-estimation here is more preferable than an under-estimation - // (which could lead to unnecessary memory allocations). - // - // We don't perform any special validation on header values because we - // consider all strings to be valid values (including Unicode strings, - // special characters, etc.). - hdrSize := len(hdrLine) + len(crlf) - for k, vs := range m.Header { - if !isHeaderKeyValid(k) { - return nil, ErrBadHeaderMsg - } - for _, v := range vs { - hdrSize += len(k) + len(v) + 4 // 4 is for the colon, space, CR, and LF - } + hdrSize, err := m.headerSize() + if hdrSize == 0 || err != nil { + return hdr, err } // NOTE: bytes.Buffer.WriteString() returns an error because bytes.Buffer @@ -1068,6 +1094,9 @@ type ServerInfo struct { // IsSystemAccount indicates whether the connected client's account // is the system account. IsSystemAccount bool `json:"acc_is_sys,omitempty"` + // RemoteAccount is the name of the account the connected client is + // bound to. + RemoteAccount string `json:"remote_account,omitempty"` // JSApiLevel is the JetStream API level advertised by the server. // Requires nats-server v2.12.0 or later; older servers will report 0. JSApiLevel int `json:"api_lvl,omitempty"` @@ -1940,7 +1969,7 @@ func processUrlString(url string) []string { urls := strings.Split(url, ",") var j int for _, s := range urls { - u := strings.TrimSuffix(strings.TrimSpace(s), "/") + u := strings.TrimSpace(s) if len(u) > 0 { urls[j] = u j++ @@ -2218,31 +2247,20 @@ func (nc *Conn) parseServerURL(sURL string, implicit, saveTLSName bool) (*Server if !strings.Contains(sURL, "://") { sURL = fmt.Sprintf("%s://%s", nc.connScheme(), sURL) } - var ( - u *url.URL - err error - ) - for i := 0; i < 2; i++ { - u, err = url.Parse(sURL) - if err != nil { - return nil, err - } - if u.Port() != "" { - break - } - // In case given URL is of the form "localhost:", just add - // the port number at the end, otherwise, add ":4222". - if sURL[len(sURL)-1] != ':' { - sURL += ":" - } + u, err := url.Parse(sURL) + if err != nil { + return nil, err + } + if u.Port() == "" { + port := defaultPortString switch u.Scheme { case wsScheme: - sURL += defaultWSPortString + port = defaultWSPortString case wsSchemeTLS: - sURL += defaultWSSPortString - default: - sURL += defaultPortString + port = defaultWSSPortString } + // "host:" has an empty port, so trim the colon before adding the default. + u.Host = strings.TrimSuffix(u.Host, ":") + ":" + port } var tlsName string @@ -2315,7 +2333,7 @@ func (nc *Conn) newReaderWriter() { func (nc *Conn) bindToNewConn() { bw := nc.bw - bw.w, bw.bufs = nc.newWriter(), nil + bw.w, bw.conn, bw.bufs, bw.partialWriteErr = nc.newWriter(), nc.conn, nil, nil br := nc.br br.r, br.n, br.off = nc.conn, 0, -1 } @@ -2368,7 +2386,13 @@ func (w *natsWriter) flush() error { // Do not skip calling w.w.Write() here if len(w.bufs) is 0 because // the actual writer (if websocket for instance) may have things // to do such as sending control frames, etc.. - _, err := w.w.Write(w.bufs) + n, err := w.w.Write(w.bufs) + // A write that fails part way leaves a truncated protocol op on the + // wire, so the connection can no longer be used. + if err != nil && n > 0 && w.conn != nil { + w.partialWriteErr = err + w.conn.Close() + } w.bufs = w.bufs[:0] return err } @@ -2856,6 +2880,26 @@ func (nc *Conn) IsSystemAccount() bool { return nc.info.IsSystemAccount } +// RemoteAccount returns the name of the account the connected client is +// bound to, e.g. "$G" when the server has no accounts configured. The +// server sends it in an INFO right after the PONG to the connection's +// first PING, which is processed only after Connect returns, so call +// Flush first when the value is needed immediately. The same applies +// after a reconnect. +func (nc *Conn) RemoteAccount() string { + if nc == nil { + return _EMPTY_ + } + + nc.mu.RLock() + defer nc.mu.RUnlock() + + if nc.status != CONNECTED { + return _EMPTY_ + } + return nc.info.RemoteAccount +} + // Low level setup for structs, etc func (nc *Conn) setup() { nc.subs = make(map[int64]*Subscription) @@ -3459,6 +3503,7 @@ func (nc *Conn) doReconnect(err error, forceReconnect bool) { rt.Stop() nc.mu.Lock() nc.rqch = make(chan struct{}) + rqch = nc.rqch nc.mu.Unlock() case <-rt.C: } @@ -3491,9 +3536,12 @@ func (nc *Conn) doReconnect(err error, forceReconnect bool) { rt.Stop() // we need to reset the rqch channel to avoid - // closing a closed channel in the next iteration + // closing a closed channel in the next iteration, + // and keep selecting on the new channel so the + // delay is honored again on the next iteration. nc.mu.Lock() nc.rqch = make(chan struct{}) + rqch = nc.rqch nc.mu.Unlock() case <-rt.C: } @@ -3727,6 +3775,12 @@ func (nc *Conn) readLoop() { err = nc.parse(buf) } if err != nil { + // Report the write error rather than the read error it caused. + nc.mu.Lock() + if nc.bw.partialWriteErr != nil { + err = nc.bw.partialWriteErr + } + nc.mu.Unlock() if shouldClose := nc.processOpErr(err, false); shouldClose { nc.close(CLOSED, true, nil) } @@ -3778,7 +3832,7 @@ func (nc *Conn) waitForMsgs(s *Subscription) { } continue } - msgLen = len(m.Data) + msgLen = m.pendingSize() } mcb := s.mcb max = s.max @@ -3952,7 +4006,7 @@ func (nc *Conn) processMsg(data []byte) { if sub.pMsgs > sub.pMsgsMax { sub.pMsgsMax = sub.pMsgs } - sub.pBytes += len(m.Data) + sub.pBytes += m.pendingSize() if sub.pBytes > sub.pBytesMax { sub.pBytesMax = sub.pBytes } @@ -4101,7 +4155,7 @@ slowConsumer: func (sub *Subscription) releaseReserved(m *Msg) { if sub.typ != ChanSubscription { sub.pMsgs-- - sub.pBytes -= len(m.Data) + sub.pBytes -= m.pendingSize() } } @@ -4344,8 +4398,14 @@ func (nc *Conn) processInfo(info string) error { // protection. func (nc *Conn) processAsyncInfo(info []byte) { nc.mu.Lock() + remoteAcc, isSysAcc := nc.info.RemoteAccount, nc.info.IsSystemAccount // Ignore errors, we will simply not update the server pool... nc.processInfo(string(info)) + // Only the INFO following the first PONG carries the account; later + // ones (cluster topology, lame duck mode) must not clear it. + if nc.info.RemoteAccount == _EMPTY_ { + nc.info.RemoteAccount, nc.info.IsSystemAccount = remoteAcc, isSysAcc + } nc.mu.Unlock() } @@ -4397,6 +4457,17 @@ func (nc *Conn) processErr(ie string) { close = nc.processOpErr(ErrMaxConnectionsExceeded, false) } else if e == MAX_ACCOUNT_CONNECTIONS_ERR { close = nc.processOpErr(ErrMaxAccountConnectionsExceeded, false) + } else if e == unknownProtoOpErr { + close = nc.processOpErr(errors.New("nats: "+ne), false) + } else if e == maxControlLineErr { + close = true + err := errors.New("nats: " + ne) + nc.mu.Lock() + nc.err = err + if asyncErrorCB := nc.Opts.AsyncErrorCB; asyncErrorCB != nil { + nc.ach.push(func() { asyncErrorCB(nc, nil, err) }) + } + nc.mu.Unlock() } else if strings.HasPrefix(e, PERMISSIONS_ERR) { nc.processTransientError(fmt.Errorf("%w: %s", ErrPermissionViolation, ne)) } else if strings.HasPrefix(e, MAX_SUBSCRIPTIONS_ERR) { @@ -4500,16 +4571,29 @@ const ( statusLen = 3 // e.g. 20x, 40x, 50x ) +// nextHeaderLine returns the next line in s mirroring textproto.Reader +func nextHeaderLine(s string) (line, rest string, ok bool) { + i := strings.IndexByte(s, '\n') + if i < 0 { + return "", "", false + } + line = s[:i] + if len(line) > 0 && line[len(line)-1] == '\r' { + line = line[:len(line)-1] + } + return line, s[i+1:], true +} + // DecodeHeadersMsg will decode and headers. func DecodeHeadersMsg(data []byte) (Header, error) { - br := bufio.NewReaderSize(bytes.NewReader(data), 128) - tp := textproto.NewReader(br) - l, err := tp.ReadLine() - if err != nil || len(l) < hdrPreEnd || l[:hdrPreEnd] != hdrLine[:hdrPreEnd] { + // Copy the header block once; keys and values are substrings of it. + s := string(data) + l, s, ok := nextHeaderLine(s) + if !ok || len(l) < hdrPreEnd || l[:hdrPreEnd] != hdrLine[:hdrPreEnd] { return nil, ErrBadHeaderMsg } - mh, err := readMIMEHeader(tp) + h, err := readMIMEHeader(s) if err != nil { return nil, err } @@ -4525,25 +4609,27 @@ func DecodeHeadersMsg(data []byte) (Header, error) { description = strings.TrimSpace(status[statusLen:]) status = status[:statusLen] } - mh.Add(statusHdr, status) + h.Add(statusHdr, status) if len(description) > 0 { - mh.Add(descrHdr, description) + h.Add(descrHdr, description) } } - return Header(mh), nil + return h, nil } // readMIMEHeader returns a MIMEHeader that preserves the -// original case of the MIME header, based on the implementation -// of textproto.ReadMIMEHeader. -// -// https://golang.org/pkg/net/textproto/#Reader.ReadMIMEHeader -func readMIMEHeader(tp *textproto.Reader) (textproto.MIMEHeader, error) { - m := make(textproto.MIMEHeader) +// original case of the MIME header +func readMIMEHeader(s string) (Header, error) { + m := make(Header) for { - kv, err := tp.ReadLine() + kv, rest, ok := nextHeaderLine(s) + if !ok { + return nil, ErrBadHeaderMsg + } + s = rest if len(kv) == 0 { - return m, err + // Blank line marks the end of the header block. + return m, nil } // Process key fetching original case. @@ -4561,9 +4647,6 @@ func readMIMEHeader(tp *textproto.Reader) (textproto.MIMEHeader, error) { i++ } m[key] = append(m[key], kv[i:]) - if err != nil { - return m, err - } } } @@ -4877,7 +4960,10 @@ func (nc *Conn) newRequest(subj string, hdr, data []byte, timeout time.Duration) select { case msg, ok = <-mch: if !ok { - return nil, ErrConnectionClosed + if nc.IsClosed() { + return nil, ErrConnectionClosed + } + return nil, ErrDisconnected } case <-t.C: nc.mu.Lock() @@ -5767,7 +5853,7 @@ func (s *Subscription) processNextMsgDelivered(msg *Msg) error { if s.typ == SyncSubscription { s.pMsgs-- - s.pBytes -= len(msg.Data) + s.pBytes -= msg.pendingSize() } if s.jsi != nil && s.jsi.resetPending { s.tryResetOrderedConsumer() @@ -5805,6 +5891,7 @@ func (s *Subscription) QueuedMsgs() (int, error) { } // Pending returns the number of queued messages and queued bytes in the client for this subscription. +// The byte count includes data and encoded headers. func (s *Subscription) Pending() (int, int, error) { if s == nil { return -1, -1, ErrBadSubscription @@ -5880,6 +5967,7 @@ func (s *Subscription) PendingLimits() (int, int, error) { } // SetPendingLimits sets the limits for pending msgs and bytes for this subscription. +// The byte limit includes data and encoded headers. // Zero is not allowed. Any negative value means that the given metric is not limited. func (s *Subscription) SetPendingLimits(msgLimit, bytesLimit int) error { if s == nil { @@ -6349,6 +6437,11 @@ func (nc *Conn) drainConnection() { min = 0 } for time.Now().Before(timeout) { + // Close clears all subscriptions, including respMux, so min + // would never be reached. + if nc.IsClosed() { + return + } if nc.NumSubscriptions() == min { break } @@ -6375,8 +6468,16 @@ func (nc *Conn) drainConnection() { pushErr(ErrDrainTimeout) } - // Flip State + // Flip State, unless the connection was closed while we were + // draining subscriptions (for example by a concurrent Close call). + // In that case abort the publish drain: the connection is already + // terminal, and flipping the state here would resurrect it and + // start a new flush on a closed connection. nc.mu.Lock() + if nc.isClosed() { + nc.mu.Unlock() + return + } nc.changeConnStatus(DRAINING_PUBS) nc.mu.Unlock() @@ -6721,6 +6822,9 @@ func (nc *Conn) GetClientID() (uint64, error) { // StatusChanged returns a channel on which given list of connection status changes will be reported. // If no statuses are provided, defaults will be used: CONNECTED, RECONNECTING, DISCONNECTED, CLOSED. +// The channel buffers up to 10 events; further events are dropped while it is full. +// It stays registered, and is not closed when the connection closes, until it is +// removed with RemoveStatusListener. func (nc *Conn) StatusChanged(statuses ...Status) chan Status { if len(statuses) == 0 { statuses = []Status{CONNECTED, RECONNECTING, DISCONNECTED, CLOSED} @@ -6734,25 +6838,23 @@ func (nc *Conn) StatusChanged(statuses ...Status) chan Status { return ch } -// RemoveStatusListener removes a status change listener. -// If the channel is not closed, it will be closed. -// Listeners will be removed automatically on status change -// as well, but this is a way to remove them manually. +// RemoveStatusListener unregisters ch from all statuses and closes it. +// Events already buffered can still be received. Use this instead of +// closing the channel yourself. func (nc *Conn) RemoveStatusListener(ch chan (Status)) { nc.mu.Lock() defer nc.mu.Unlock() - select { - case <-ch: - default: - close(ch) - } - for _, listeners := range nc.statListeners { - for range listeners { - delete(listeners, ch) - } + delete(listeners, ch) } + closeStatusListener(ch) +} + +// closeStatusListener closes ch unless it is already closed. +func closeStatusListener(ch chan Status) { + defer func() { _ = recover() }() + close(ch) } // registerStatusChangeListener registers a channel waiting for a specific status change event. @@ -6773,24 +6875,29 @@ func (nc *Conn) registerStatusChangeListener(status Status, ch chan Status) { // If channel is closed, or there is no listener, sendStatusEvent // will not block. Lock should be held entering. func (nc *Conn) sendStatusEvent(s Status) { -Loop: for ch := range nc.statListeners[s] { - // make sure channel is not closed - select { - case <-ch: - // if chan is closed, remove it + if !notifyStatusListener(ch, s) { delete(nc.statListeners[s], ch) - continue Loop - default: - } - // only send event if someone's listening - select { - case ch <- s: - default: } } } +// notifyStatusListener sends s on ch unless its buffer is full, and reports +// whether ch is still open. Receiving from ch to detect a closed channel +// would consume an event the listener has not read yet. +func notifyStatusListener(ch chan Status, s Status) (open bool) { + defer func() { + if recover() != nil { + open = false + } + }() + select { + case ch <- s: + default: + } + return true +} + // changeConnStatus changes connections status and sends events // to all listeners. Lock should be held entering. func (nc *Conn) changeConnStatus(status Status) { diff --git a/vendor/github.com/nats-io/nats.go/object.go b/vendor/github.com/nats-io/nats.go/object.go index a5c15150b0..ca3c8a01b4 100644 --- a/vendor/github.com/nats-io/nats.go/object.go +++ b/vendor/github.com/nats-io/nats.go/object.go @@ -893,7 +893,7 @@ func (obs *obs) PutFile(file string, opts ...ObjectOpt) (*ObjectInfo, error) { // GetFile is a convenience function to pull and object and place in a file. func (obs *obs) GetFile(name, file string, opts ...GetObjectOpt) error { // Expect file to be new. - f, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE, 0600) + f, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } @@ -1081,19 +1081,19 @@ func (obs *obs) Watch(opts ...WatchOpt) (ObjectWatcher, error) { if err := json.Unmarshal(m.Data, &info); err != nil { return // TODO(dlc) - Communicate this upwards? } - meta, err := m.Metadata() + tokens, err := parser.GetMetadataFields(m.Reply) if err != nil { return } if !o.ignoreDeletes || !info.Deleted { - info.ModTime = meta.Timestamp + info.ModTime = time.Unix(0, int64(parser.ParseNum(tokens[parser.AckTimestampSeqTokenPos]))) w.updates <- &info } // if UpdatesOnly is set, no not send nil to the channel // as it would always be triggered after initializing the watcher - if !initDoneMarker && meta.NumPending == 0 { + if !initDoneMarker && parser.ParseNum(tokens[parser.AckNumPendingTokenPos]) == 0 { initDoneMarker = true w.updates <- nil } diff --git a/vendor/github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header/header.go b/vendor/github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header/header.go index 820bf436ab..2cd16f13a4 100644 --- a/vendor/github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header/header.go +++ b/vendor/github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header/header.go @@ -39,7 +39,7 @@ func init() { // token = 1* // qdtext = > - for c := 0; c < 256; c++ { + for c := range 256 { var t octetType isCtl := c <= 31 || c == 127 isChar := 0 <= c && c <= 127 diff --git a/vendor/github.com/prometheus/client_golang/prometheus/build_info_collector.go b/vendor/github.com/prometheus/client_golang/prometheus/build_info_collector.go index 450189f35e..b80b964c9c 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/build_info_collector.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/build_info_collector.go @@ -32,7 +32,8 @@ func NewBuildInfoCollector() Collector { "Build information about the main Go module.", nil, Labels{"path": path, "version": version, "checksum": sum}, ), - GaugeValue, 1)} + GaugeValue, 1, + )} c.init(c.self) return c } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/desc.go b/vendor/github.com/prometheus/client_golang/prometheus/desc.go index a3c92e7a4c..881d91dee0 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/desc.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/desc.go @@ -21,7 +21,6 @@ import ( "github.com/cespare/xxhash/v2" dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/model" - "google.golang.org/protobuf/proto" "github.com/prometheus/client_golang/prometheus/internal" ) @@ -181,8 +180,8 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const d.constLabelPairs = make([]*dto.LabelPair, 0, len(constLabels)) for n, v := range constLabels { d.constLabelPairs = append(d.constLabelPairs, &dto.LabelPair{ - Name: proto.String(n), - Value: proto.String(v), + Name: new(n), + Value: new(v), }) } sort.Sort(internal.LabelPairSorter(d.constLabelPairs)) diff --git a/vendor/github.com/prometheus/client_golang/prometheus/get_pid.go b/vendor/github.com/prometheus/client_golang/prometheus/get_pid.go index 614fd61be9..c7be2a0ca7 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/get_pid.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/get_pid.go @@ -12,7 +12,6 @@ // limitations under the License. //go:build !js || wasm -// +build !js wasm package prometheus diff --git a/vendor/github.com/prometheus/client_golang/prometheus/get_pid_gopherjs.go b/vendor/github.com/prometheus/client_golang/prometheus/get_pid_gopherjs.go index eaf8059ee1..0ddc8c6eed 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/get_pid_gopherjs.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/get_pid_gopherjs.go @@ -12,7 +12,6 @@ // limitations under the License. //go:build js && !wasm -// +build js,!wasm package prometheus diff --git a/vendor/github.com/prometheus/client_golang/prometheus/go_collector.go b/vendor/github.com/prometheus/client_golang/prometheus/go_collector.go index 520cbd7d41..a4725849f4 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/go_collector.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/go_collector.go @@ -211,23 +211,28 @@ func newBaseGoCollector() baseGoCollector { goroutinesDesc: NewDesc( "go_goroutines", "Number of goroutines that currently exist.", - nil, nil), + nil, nil, + ), threadsDesc: NewDesc( "go_threads", "Number of OS threads created.", - nil, nil), + nil, nil, + ), gcDesc: NewDesc( "go_gc_duration_seconds", "A summary of the wall-time pause (stop-the-world) duration in garbage collection cycles.", - nil, nil), + nil, nil, + ), gcLastTimeDesc: NewDesc( "go_memstats_last_gc_time_seconds", "Number of seconds since 1970 of last garbage collection.", - nil, nil), + nil, nil, + ), goInfoDesc: NewDesc( "go_info", "Information about the Go environment.", - nil, Labels{"version": runtime.Version()}), + nil, Labels{"version": runtime.Version()}, + ), } } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/go_collector_latest.go b/vendor/github.com/prometheus/client_golang/prometheus/go_collector_latest.go index 1db1c4be09..23091e1774 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/go_collector_latest.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/go_collector_latest.go @@ -11,9 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build go1.17 -// +build go1.17 - package prometheus import ( @@ -27,7 +24,6 @@ import ( "github.com/prometheus/client_golang/prometheus/internal" dto "github.com/prometheus/client_model/go" - "google.golang.org/protobuf/proto" ) const ( @@ -573,14 +569,14 @@ func (h *batchHistogram) Write(out *dto.Metric) error { // by obtaining the next float64 value down, in order. upperBound := math.Nextafter(h.buckets[i+1], h.buckets[i]) dtoBuckets = append(dtoBuckets, &dto.Bucket{ - CumulativeCount: proto.Uint64(totalCount), - UpperBound: proto.Float64(upperBound), + CumulativeCount: new(totalCount), + UpperBound: new(upperBound), }) } out.Histogram = &dto.Histogram{ Bucket: dtoBuckets, - SampleCount: proto.Uint64(totalCount), - SampleSum: proto.Float64(sum), + SampleCount: new(totalCount), + SampleSum: new(sum), } return nil } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/histogram.go b/vendor/github.com/prometheus/client_golang/prometheus/histogram.go index 88bae3b32c..1d6ca1614f 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/histogram.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/histogram.go @@ -803,8 +803,8 @@ func (h *histogram) Write(out *dto.Metric) error { his := &dto.Histogram{ Bucket: make([]*dto.Bucket, len(h.upperBounds)), - SampleCount: proto.Uint64(count), - SampleSum: proto.Float64(math.Float64frombits(atomic.LoadUint64(&coldCounts.sumBits))), + SampleCount: new(count), + SampleSum: new(math.Float64frombits(atomic.LoadUint64(&coldCounts.sumBits))), CreatedTimestamp: timestamppb.New(h.lastResetTime), } out.Histogram = his @@ -814,8 +814,8 @@ func (h *histogram) Write(out *dto.Metric) error { for i, upperBound := range h.upperBounds { cumCount += atomic.LoadUint64(&coldCounts.buckets[i]) his.Bucket[i] = &dto.Bucket{ - CumulativeCount: proto.Uint64(cumCount), - UpperBound: proto.Float64(upperBound), + CumulativeCount: new(cumCount), + UpperBound: new(upperBound), } if e := h.exemplars[i].Load(); e != nil { his.Bucket[i].Exemplar = e.(*dto.Exemplar) @@ -824,15 +824,15 @@ func (h *histogram) Write(out *dto.Metric) error { // If there is an exemplar for the +Inf bucket, we have to add that bucket explicitly. if e := h.exemplars[len(h.upperBounds)].Load(); e != nil { b := &dto.Bucket{ - CumulativeCount: proto.Uint64(count), - UpperBound: proto.Float64(math.Inf(1)), + CumulativeCount: new(count), + UpperBound: new(math.Inf(1)), Exemplar: e.(*dto.Exemplar), } his.Bucket = append(his.Bucket, b) } if h.nativeHistogramSchema > math.MinInt32 { - his.ZeroThreshold = proto.Float64(math.Float64frombits(atomic.LoadUint64(&coldCounts.nativeHistogramZeroThresholdBits))) - his.Schema = proto.Int32(atomic.LoadInt32(&coldCounts.nativeHistogramSchema)) + his.ZeroThreshold = new(math.Float64frombits(atomic.LoadUint64(&coldCounts.nativeHistogramZeroThresholdBits))) + his.Schema = new(atomic.LoadInt32(&coldCounts.nativeHistogramSchema)) zeroBucket := atomic.LoadUint64(&coldCounts.nativeHistogramZeroBucket) defer func() { @@ -840,7 +840,7 @@ func (h *histogram) Write(out *dto.Metric) error { coldCounts.nativeHistogramBucketsNegative.Range(addAndReset(&hotCounts.nativeHistogramBucketsNegative, &hotCounts.nativeHistogramBucketsNumber)) }() - his.ZeroCount = proto.Uint64(zeroBucket) + his.ZeroCount = new(zeroBucket) his.NegativeSpan, his.NegativeDelta = makeBuckets(&coldCounts.nativeHistogramBucketsNegative) his.PositiveSpan, his.PositiveDelta = makeBuckets(&coldCounts.nativeHistogramBucketsPositive) @@ -1331,12 +1331,12 @@ func (h *constHistogram) Write(out *dto.Metric) error { buckets := make([]*dto.Bucket, 0, len(h.buckets)) - his.SampleCount = proto.Uint64(h.count) - his.SampleSum = proto.Float64(h.sum) + his.SampleCount = new(h.count) + his.SampleSum = new(h.sum) for upperBound, count := range h.buckets { buckets = append(buckets, &dto.Bucket{ - CumulativeCount: proto.Uint64(count), - UpperBound: proto.Float64(upperBound), + CumulativeCount: new(count), + UpperBound: new(upperBound), }) } @@ -1481,7 +1481,7 @@ func pickSchema(bucketFactor float64) int32 { func makeBuckets(buckets *sync.Map) ([]*dto.BucketSpan, []int64) { var ii []int - buckets.Range(func(k, v any) bool { + buckets.Range(func(k, _ any) bool { ii = append(ii, k.(int)) return true }) @@ -1518,13 +1518,13 @@ func makeBuckets(buckets *sync.Map) ([]*dto.BucketSpan, []int64) { // at the very beginning, or because we have found a gap // of more than two buckets. spans = append(spans, &dto.BucketSpan{ - Offset: proto.Int32(iDelta), + Offset: new(iDelta), Length: proto.Uint32(0), }) } else { // We have found a small gap (or no gap at all). // Insert empty buckets as needed. - for j := int32(0); j < iDelta; j++ { + for range iDelta { appendDelta(0) } } @@ -1570,7 +1570,7 @@ func addAndReset(hotBuckets *sync.Map, bucketNumber *uint32) func(k, v any) bool } func deleteSyncMap(m *sync.Map) { - m.Range(func(k, v any) bool { + m.Range(func(k, _ any) bool { m.Delete(k) return true }) @@ -1578,7 +1578,7 @@ func deleteSyncMap(m *sync.Map) { func findSmallestKey(m *sync.Map) int { result := math.MaxInt32 - m.Range(func(k, v any) bool { + m.Range(func(k, _ any) bool { key := k.(int) if key < result { result = key @@ -1956,7 +1956,7 @@ func NewConstNativeHistogram( PositiveSpan: PositiveSpan, PositiveDelta: PositiveDelta, - ZeroCount: proto.Uint64(zeroBucket), + ZeroCount: new(zeroBucket), }, labelPairs: MakeLabelPairs(desc, labelValues), } @@ -2044,13 +2044,13 @@ func makeBucketsFromMap(buckets map[int]int64) ([]*dto.BucketSpan, []int64) { // at the very beginning, or because we have found a gap // of more than two buckets. spans = append(spans, &dto.BucketSpan{ - Offset: proto.Int32(iDelta), + Offset: new(iDelta), Length: proto.Uint32(0), }) } else { // We have found a small gap (or no gap at all). // Insert empty buckets as needed. - for j := int32(0); j < iDelta; j++ { + for range iDelta { appendDelta(0) } } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/internal/go_runtime_metrics.go b/vendor/github.com/prometheus/client_golang/prometheus/internal/go_runtime_metrics.go index d273b6640e..e38dfe0adc 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/internal/go_runtime_metrics.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/internal/go_runtime_metrics.go @@ -11,9 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build go1.17 -// +build go1.17 - package internal import ( diff --git a/vendor/github.com/prometheus/client_golang/prometheus/metric.go b/vendor/github.com/prometheus/client_golang/prometheus/metric.go index c5cb90adf8..e4bbf3b8ec 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/metric.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/metric.go @@ -22,7 +22,6 @@ import ( dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/model" - "google.golang.org/protobuf/proto" ) var separatorByteSlice = []byte{model.SeparatorByte} // For convenient use with xxhash. @@ -153,7 +152,7 @@ type timestampedMetric struct { func (m timestampedMetric) Write(pb *dto.Metric) error { e := m.Metric.Write(pb) - pb.TimestampMs = proto.Int64(m.t.Unix()*1000 + int64(m.t.Nanosecond()/1000000)) + pb.TimestampMs = new(m.t.Unix()*1000 + int64(m.t.Nanosecond()/1000000)) return e } @@ -209,8 +208,8 @@ func (m *withExemplarsMetric) Write(pb *dto.Metric) error { } else { // The +Inf bucket should be explicitly added if there is an exemplar for it, similar to non-const histogram logic in https://github.com/prometheus/client_golang/blob/main/prometheus/histogram.go#L357-L365. b := &dto.Bucket{ - CumulativeCount: proto.Uint64(h.GetSampleCount()), - UpperBound: proto.Float64(math.Inf(1)), + CumulativeCount: new(h.GetSampleCount()), + UpperBound: new(math.Inf(1)), Exemplar: e, } h.Bucket = append(h.Bucket, b) diff --git a/vendor/github.com/prometheus/client_golang/prometheus/num_threads.go b/vendor/github.com/prometheus/client_golang/prometheus/num_threads.go index 7c12b21087..7ffac21340 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/num_threads.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/num_threads.go @@ -12,7 +12,6 @@ // limitations under the License. //go:build !js || wasm -// +build !js wasm package prometheus diff --git a/vendor/github.com/prometheus/client_golang/prometheus/num_threads_gopherjs.go b/vendor/github.com/prometheus/client_golang/prometheus/num_threads_gopherjs.go index 7348df01df..65f4263f9f 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/num_threads_gopherjs.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/num_threads_gopherjs.go @@ -12,7 +12,6 @@ // limitations under the License. //go:build js && !wasm -// +build js,!wasm package prometheus diff --git a/vendor/github.com/prometheus/client_golang/prometheus/process_collector_darwin.go b/vendor/github.com/prometheus/client_golang/prometheus/process_collector_darwin.go index 2b16298f40..46e7e45aab 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/process_collector_darwin.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/process_collector_darwin.go @@ -54,21 +54,22 @@ func getOpenFileCount() (float64, error) { // benefits, however, include fewer system calls and not failing when at the // open file soft limit. - if dir, err := os.Open("/dev/fd"); err != nil { + dir, err := os.Open("/dev/fd") + if err != nil { return 0.0, err - } else { - defer dir.Close() - - // Avoid ReadDir(), as it calls stat(2) on each descriptor. Not only is - // that info not used, but KQUEUE descriptors fail stat(2), which causes - // the whole method to fail. - if names, err := dir.Readdirnames(0); err != nil { - return 0.0, err - } else { - // Subtract 1 to ignore the open /dev/fd descriptor above. - return float64(len(names) - 1), nil - } } + defer dir.Close() + + // Avoid ReadDir(), as it calls stat(2) on each descriptor. Not only is + // that info not used, but KQUEUE descriptors fail stat(2), which causes + // the whole method to fail. + names, err := dir.Readdirnames(0) + if err != nil { + return 0.0, err + } + + // Subtract 1 to ignore the open /dev/fd descriptor above. + return float64(len(names) - 1), nil } func (c *processCollector) processCollect(ch chan<- Metric) { diff --git a/vendor/github.com/prometheus/client_golang/prometheus/process_collector_not_supported.go b/vendor/github.com/prometheus/client_golang/prometheus/process_collector_not_supported.go index 7732b7f376..8650b6f4a7 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/process_collector_not_supported.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/process_collector_not_supported.go @@ -12,7 +12,6 @@ // limitations under the License. //go:build wasip1 || js || ios -// +build wasip1 js ios package prometheus diff --git a/vendor/github.com/prometheus/client_golang/prometheus/process_collector_procfsenabled.go b/vendor/github.com/prometheus/client_golang/prometheus/process_collector_procfsenabled.go index 8074f70f5d..56c97d62ef 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/process_collector_procfsenabled.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/process_collector_procfsenabled.go @@ -12,7 +12,6 @@ // limitations under the License. //go:build !windows && !js && !wasip1 && !darwin -// +build !windows,!js,!wasip1,!darwin package prometheus diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http.go index c28af5ce2b..0af3deebfe 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http.go @@ -51,10 +51,12 @@ import ( ) const ( + acceptHeader = "Accept" contentTypeHeader = "Content-Type" contentEncodingHeader = "Content-Encoding" acceptEncodingHeader = "Accept-Encoding" processStartTimeHeader = "Process-Start-Time-Unix" + varyHeader = "Vary" ) // Compression represents the content encodings handlers support for the HTTP @@ -70,9 +72,8 @@ const ( func defaultCompressionFormats() []Compression { if internal.NewZstdWriter != nil { return []Compression{Identity, Gzip, Zstd} - } else { - return []Compression{Identity, Gzip} } + return []Compression{Identity, Gzip} } var gzipPool = sync.Pool{ @@ -315,11 +316,20 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO } } - var contentType expfmt.Format - if opts.EnableOpenMetrics { - contentType = expfmt.NegotiateIncludingOpenMetrics(req.Header) - } else { - contentType = expfmt.Negotiate(req.Header) + var ( + contentType expfmt.Format + acceptedFormats = opts.AcceptedFormats + ) + if len(acceptedFormats) == 0 { + acceptedFormats = expfmt.DefaultAcceptedFormats() + if opts.EnableOpenMetrics { + acceptedFormats = expfmt.DefaultOpenMetricsAcceptedFormats() + } + } + contentType = expfmt.NegotiateAccept(req.Header, acceptedFormats...) + rsp.Header().Add(varyHeader, acceptHeader) + if len(compressions) > 0 { + rsp.Header().Add(varyHeader, acceptEncodingHeader) } rsp.Header().Set(contentTypeHeader, string(contentType)) @@ -598,6 +608,8 @@ type HandlerOpts struct { // a trailing ".0" if they would otherwise look like integer numbers // (which changes the identity of the resulting series on the Prometheus // server). + // + // Note that setting AcceptedFormats overrides this option. EnableOpenMetrics bool // EnableOpenMetricsTextCreatedSamples specifies if this handler should add, extra, synthetic // Created Timestamps for counters, histograms and summaries, which for the current @@ -622,6 +634,18 @@ type HandlerOpts struct { // NOTE: This feature is experimental and not covered by OpenMetrics or Prometheus // exposition format. ProcessStartTime time.Time + // AcceptedFormats allows customizing the formats the handler will negotiate + // with the client based on the request's Accept header. + // The client's Accept header and q-values take precedence, but the order of + // formats in AcceptedFormats specifies the server's preference order used as a + // tie-breaker (e.g. for wildcard "*/*" requests or equal q-values) and fallback. + // If empty, the default set is used (expfmt.DefaultAcceptedFormats()). + // If empty and EnableOpenMetrics is true, stable OpenMetrics formats are negotiated + // (expfmt.DefaultOpenMetricsAcceptedFormats()). + // This is useful for testing experimental formats (e.g. OpenMetrics 2.0) + // or restricting the formats you want to allow. + // Using EnableOpenMetrics with an empty AcceptedFormats is still the recommended option for the majority of cases. + AcceptedFormats []expfmt.Format } // httpError removes any content-encoding header and then calls http.Error with diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/option.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/option.go index d4c0954f36..258a2a5b55 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/option.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/option.go @@ -41,7 +41,7 @@ type options struct { func defaultOptions() *options { return &options{ - getExemplarFn: func(req *http.Request) prometheus.Labels { return nil }, + getExemplarFn: func(_ *http.Request) prometheus.Labels { return nil }, extraLabelsFromRequest: map[string]LabelValueFromRequest{}, } } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/registry.go b/vendor/github.com/prometheus/client_golang/prometheus/registry.go index ed0681c8b4..4151b59c48 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/registry.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/registry.go @@ -17,6 +17,7 @@ import ( "bytes" "errors" "fmt" + "maps" "os" "path/filepath" "runtime" @@ -31,7 +32,6 @@ import ( "github.com/cespare/xxhash/v2" dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/expfmt" - "google.golang.org/protobuf/proto" ) const ( @@ -296,6 +296,7 @@ func (r *Registry) Register(c Collector) error { defer func() { // Drain channel in case of premature return to not leak a goroutine. for range descChan { + continue } r.mtx.Unlock() }() @@ -369,9 +370,7 @@ func (r *Registry) Register(c Collector) error { for hash := range newDescIDs { r.descIDs[hash] = struct{}{} } - for name, dimHash := range newDimHashesByName { - r.dimHashesByName[name] = dimHash - } + maps.Copy(r.dimHashesByName, newDimHashesByName) return nil } @@ -476,7 +475,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { for { select { case collector := <-checkedCollectors: - safeErrs.Append((safeCollect(collector, checkedMetricChan))) + safeErrs.Append(safeCollect(collector, checkedMetricChan)) case collector := <-uncheckedCollectors: safeErrs.Append(safeCollect(collector, uncheckedMetricChan)) default: @@ -502,10 +501,12 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { defer func() { if checkedMetricChan != nil { for range checkedMetricChan { + continue } } if uncheckedMetricChan != nil { for range uncheckedMetricChan { + continue } } }() @@ -727,10 +728,10 @@ func processMetric( } } else { // New name. metricFamily = &dto.MetricFamily{} - metricFamily.Name = proto.String(desc.fqName) - metricFamily.Help = proto.String(desc.help) + metricFamily.Name = new(desc.fqName) + metricFamily.Help = new(desc.help) if desc.unit != "" { - metricFamily.Unit = proto.String(desc.unit) + metricFamily.Unit = new(desc.unit) } // TODO(beorn7): Simplify switch once Desc has type. switch { @@ -956,7 +957,8 @@ func checkMetricConsistency( if !utf8.ValidString(labelPair.GetValue()) { return fmt.Errorf( "collected metric %q { %s} has a label named %q whose value is not utf8: %#v", - name, dtoMetric, labelName, labelPair.GetValue()) + name, dtoMetric, labelName, labelPair.GetValue(), + ) } previousLabelName = labelName } @@ -981,7 +983,7 @@ func checkMetricConsistency( h.Write(separatorByteSlice) } if dtoMetric.TimestampMs != nil { - h.WriteString(strconv.FormatInt(*(dtoMetric.TimestampMs), 10)) + h.WriteString(strconv.FormatInt(*dtoMetric.TimestampMs, 10)) h.Write(separatorByteSlice) } hSum := h.Sum64() @@ -1013,7 +1015,7 @@ func checkDescConsistency( copy(lpsFromDesc, desc.constLabelPairs) for _, l := range desc.variableLabels.names { lpsFromDesc = append(lpsFromDesc, &dto.LabelPair{ - Name: proto.String(l), + Name: new(l), }) } if len(lpsFromDesc) != len(dtoMetric.Label) { diff --git a/vendor/github.com/prometheus/client_golang/prometheus/summary.go b/vendor/github.com/prometheus/client_golang/prometheus/summary.go index c12b8d13d4..d92850ca01 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/summary.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/summary.go @@ -25,7 +25,6 @@ import ( dto "github.com/prometheus/client_model/go" "github.com/beorn7/perks/quantile" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -337,8 +336,8 @@ func (s *summary) Write(out *dto.Metric) error { s.bufMtx.Unlock() s.flushColdBuf() - sum.SampleCount = proto.Uint64(s.cnt) - sum.SampleSum = proto.Float64(s.sum) + sum.SampleCount = new(s.cnt) + sum.SampleSum = new(s.sum) for _, rank := range s.sortedObjectives { var q float64 @@ -348,8 +347,8 @@ func (s *summary) Write(out *dto.Metric) error { q = s.headStream.Query(rank) } qs = append(qs, &dto.Quantile{ - Quantile: proto.Float64(rank), - Value: proto.Float64(q), + Quantile: new(rank), + Value: new(q), }) } @@ -512,8 +511,8 @@ func (s *noObjectivesSummary) Write(out *dto.Metric) error { } sum := &dto.Summary{ - SampleCount: proto.Uint64(count), - SampleSum: proto.Float64(math.Float64frombits(atomic.LoadUint64(&coldCounts.sumBits))), + SampleCount: new(count), + SampleSum: new(math.Float64frombits(atomic.LoadUint64(&coldCounts.sumBits))), CreatedTimestamp: s.createdTs, } @@ -718,13 +717,13 @@ func (s *constSummary) Write(out *dto.Metric) error { } qs := make([]*dto.Quantile, 0, len(s.quantiles)) - sum.SampleCount = proto.Uint64(s.count) - sum.SampleSum = proto.Float64(s.sum) + sum.SampleCount = new(s.count) + sum.SampleSum = new(s.sum) for rank, q := range s.quantiles { qs = append(qs, &dto.Quantile{ - Quantile: proto.Float64(rank), - Value: proto.Float64(q), + Quantile: new(rank), + Value: new(q), }) } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/value.go b/vendor/github.com/prometheus/client_golang/prometheus/value.go index cc23011fad..c856e4f336 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/value.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/value.go @@ -23,7 +23,6 @@ import ( "github.com/prometheus/client_golang/prometheus/internal" dto "github.com/prometheus/client_model/go" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -196,11 +195,11 @@ func populateMetric( m.Label = labelPairs switch t { case CounterValue: - m.Counter = &dto.Counter{Value: proto.Float64(v), Exemplar: e, CreatedTimestamp: ct} + m.Counter = &dto.Counter{Value: new(v), Exemplar: e, CreatedTimestamp: ct} case GaugeValue: - m.Gauge = &dto.Gauge{Value: proto.Float64(v)} + m.Gauge = &dto.Gauge{Value: new(v)} case UntypedValue: - m.Untyped = &dto.Untyped{Value: proto.Float64(v)} + m.Untyped = &dto.Untyped{Value: new(v)} default: return fmt.Errorf("encountered unknown type %v", t) } @@ -227,8 +226,8 @@ func MakeLabelPairs(desc *Desc, labelValues []string) []*dto.LabelPair { labelPairs := make([]*dto.LabelPair, 0, totalLen) for i, l := range desc.variableLabels.names { labelPairs = append(labelPairs, &dto.LabelPair{ - Name: proto.String(l), - Value: proto.String(labelValues[i]), + Name: new(l), + Value: new(labelValues[i]), }) } labelPairs = append(labelPairs, desc.constLabelPairs...) @@ -244,7 +243,7 @@ const ExemplarMaxRunes = 128 // number of runes in the label names and values exceeds ExemplarMaxRunes. func newExemplar(value float64, ts time.Time, l Labels) (*dto.Exemplar, error) { e := &dto.Exemplar{} - e.Value = proto.Float64(value) + e.Value = new(value) tsProto := timestamppb.New(ts) if err := tsProto.CheckValid(); err != nil { return nil, err @@ -262,8 +261,8 @@ func newExemplar(value float64, ts time.Time, l Labels) (*dto.Exemplar, error) { } runes += utf8.RuneCountInString(value) labelPairs = append(labelPairs, &dto.LabelPair{ - Name: proto.String(name), - Value: proto.String(value), + Name: new(name), + Value: new(value), }) } if runes > ExemplarMaxRunes { diff --git a/vendor/github.com/prometheus/client_golang/prometheus/wrap.go b/vendor/github.com/prometheus/client_golang/prometheus/wrap.go index 697f55558b..cc9ae8f20b 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/wrap.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/wrap.go @@ -20,7 +20,6 @@ import ( "github.com/prometheus/client_golang/prometheus/internal" dto "github.com/prometheus/client_model/go" - "google.golang.org/protobuf/proto" ) // WrapRegistererWith returns a Registerer wrapping the provided @@ -212,8 +211,8 @@ func (m *wrappingMetric) Write(out *dto.Metric) error { } for ln, lv := range m.labels { out.Label = append(out.Label, &dto.LabelPair{ - Name: proto.String(ln), - Value: proto.String(lv), + Name: new(ln), + Value: new(lv), }) } sort.Sort(internal.LabelPairSorter(out.Label)) diff --git a/vendor/github.com/prometheus/common/expfmt/decode.go b/vendor/github.com/prometheus/common/expfmt/decode.go index 8f8dc65d38..0339d8c1d0 100644 --- a/vendor/github.com/prometheus/common/expfmt/decode.go +++ b/vendor/github.com/prometheus/common/expfmt/decode.go @@ -72,13 +72,15 @@ func ResponseFormat(h http.Header) Format { // NewDecoder returns a new decoder based on the given input format. Metric // names are validated based on the provided Format -- if the format requires -// escaping, raditional Prometheues validity checking is used. Otherwise, names +// escaping, traditional Prometheus validity checking is used. Otherwise, names // are checked for UTF-8 validity. Supported formats include delimited protobuf -// and Prometheus text format. For historical reasons, this decoder fallbacks -// to classic text decoding for any other format. This decoder does not fully -// support OpenMetrics although it may often succeed due to the similarities -// between the formats. This decoder may not support the latest features of -// Prometheus text format and is not intended for high-performance applications. +// and Prometheus text format. For historical reasons, this decoder falls back +// to classic text decoding for other legacy formats, but returns an error for +// unsupported formats such as OpenMetrics 2.0. This decoder does not fully +// support OpenMetrics although it may often succeed for OpenMetrics 1.0 due to +// the similarities between the formats. This decoder may not support the latest +// features of Prometheus text format and is not intended for high-performance +// applications. // See: https://github.com/prometheus/common/issues/812 func NewDecoder(r io.Reader, format Format) Decoder { scheme := model.LegacyValidation @@ -90,6 +92,11 @@ func NewDecoder(r io.Reader, format Format) Decoder { return &protoDecoder{r: bufio.NewReader(r), s: scheme} case TypeProtoText, TypeProtoCompact: return &errDecoder{err: fmt.Errorf("format %s not supported for decoding", format)} + case TypeOpenMetrics: + _, params, err := mime.ParseMediaType(string(format)) + if err == nil && params["version"] == OpenMetricsVersion_2_0_0 { + return &errDecoder{err: fmt.Errorf("format %s not supported for decoding", format)} + } } return &textDecoder{r: r, s: scheme} } diff --git a/vendor/github.com/prometheus/common/expfmt/encode.go b/vendor/github.com/prometheus/common/expfmt/encode.go index 73c24dfbc9..651e69a2c8 100644 --- a/vendor/github.com/prometheus/common/expfmt/encode.go +++ b/vendor/github.com/prometheus/common/expfmt/encode.go @@ -16,7 +16,9 @@ package expfmt import ( "fmt" "io" + "mime" "net/http" + "slices" "github.com/munnerz/goautoneg" dto "github.com/prometheus/client_model/go" @@ -26,6 +28,55 @@ import ( "github.com/prometheus/common/model" ) +var formatToAccept = map[Format]goautoneg.Accept{} + +func init() { + for _, f := range []Format{ + FmtText, + FmtProtoDelim, + FmtProtoText, + FmtProtoCompact, + FmtOpenMetrics_1_0_0, + FmtOpenMetrics_2_0_0, + FmtOpenMetrics_0_0_1, + } { + if parsed := goautoneg.ParseAccept(string(f)); len(parsed) > 0 { + formatToAccept[f] = parsed[0] + } + } +} + +var ( + defaultAcceptedFormats = []Format{ + FmtProtoDelim, + FmtProtoText, + FmtProtoCompact, + FmtText, + } + + defaultOpenMetricsAcceptedFormats = []Format{ + FmtOpenMetrics_1_0_0, + FmtOpenMetrics_0_0_1, + FmtProtoDelim, + FmtProtoText, + FmtProtoCompact, + FmtText, + } +) + +// DefaultAcceptedFormats returns a copy of standard accepted formats for Negotiate, +// ordered by preference (delimited protobuf, protobuf text, compact protobuf text, +// and Prometheus text format). +func DefaultAcceptedFormats() []Format { + return slices.Clone(defaultAcceptedFormats) +} + +// DefaultOpenMetricsAcceptedFormats returns a copy of standard accepted formats including +// stable OpenMetrics formats, ordered by preference. +func DefaultOpenMetricsAcceptedFormats() []Format { + return slices.Clone(defaultOpenMetricsAcceptedFormats) +} + // Encoder types encode metric families into an underlying wire protocol. type Encoder interface { Encode(*dto.MetricFamily) error @@ -57,11 +108,30 @@ func (ec encoderCloser) Close() error { // Negotiate returns the Content-Type based on the given Accept header. If no // appropriate accepted type is found, FmtText is returned (which is the -// Prometheus text format). This function will never negotiate FmtOpenMetrics, -// as the support is still experimental. To include the option to negotiate -// FmtOpenMetrics, use NegotiateIncludingOpenMetrics. +// Prometheus text format). +// +// Deprecated: Use NegotiateAccept(h, DefaultAcceptedFormats()...) +// or specify only the formats supported by your server. func Negotiate(h http.Header) Format { - escapingScheme := Format(fmt.Sprintf("; escaping=%s", Format(model.NameEscapingScheme.String()))) + return NegotiateAccept(h, defaultAcceptedFormats...) +} + +// NegotiateIncludingOpenMetrics works like Negotiate but includes all stable +// FmtOpenMetrics formats as an option for the result. +// +// Deprecated: Use NegotiateAccept(h, DefaultOpenMetricsAcceptedFormats()...) +// or specify only the formats supported by your server. +func NegotiateIncludingOpenMetrics(h http.Header) Format { + return NegotiateAccept(h, defaultOpenMetricsAcceptedFormats...) +} + +// NegotiateAccept returns the Content-Type based on the given Accept header +// and the list of accepted Formats provided by the caller, in order of preference. +// If no accepted format matches the Accept header, it falls back to the text +// format if present in the accepted list, or the first accepted format (or FmtText +// if accepted is empty). +func NegotiateAccept(h http.Header, accepted ...Format) Format { + escapingScheme := Format("; escaping=" + model.NameEscapingScheme.String()) for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) { if escapeParam := ac.Params[model.EscapingKey]; escapeParam != "" { switch Format(escapeParam) { @@ -71,63 +141,66 @@ func Negotiate(h http.Header) Format { // If the escaping parameter is unknown, ignore it. } } - ver := ac.Params["version"] - if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol { - switch ac.Params["encoding"] { - case "delimited": - return FmtProtoDelim + escapingScheme - case "text": - return FmtProtoText + escapingScheme - case "compact-text": - return FmtProtoCompact + escapingScheme + + for _, f := range accepted { + if matchFormat(ac, f) { + return f + escapingScheme } } - if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") { - return FmtText + escapingScheme + } + for _, f := range accepted { + if f.FormatType() == TypeTextPlain { + return f + escapingScheme } } + if len(accepted) > 0 { + return accepted[0] + escapingScheme + } return FmtText + escapingScheme } -// NegotiateIncludingOpenMetrics works like Negotiate but includes -// FmtOpenMetrics as an option for the result. Note that this function is -// temporary and will disappear once FmtOpenMetrics is fully supported and as -// such may be negotiated by the normal Negotiate function. -func NegotiateIncludingOpenMetrics(h http.Header) Format { - escapingScheme := Format(fmt.Sprintf("; escaping=%s", Format(model.NameEscapingScheme.String()))) - for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) { - if escapeParam := ac.Params[model.EscapingKey]; escapeParam != "" { - switch Format(escapeParam) { - case model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues: - escapingScheme = Format("; escaping=" + escapeParam) - default: - // If the escaping parameter is unknown, ignore it. - } +// matchFormat checks if a parsed accept clause matches a given Format. +func matchFormat(ac goautoneg.Accept, f Format) bool { + target, ok := formatToAccept[f] + if !ok { + parsed := goautoneg.ParseAccept(string(f)) + if len(parsed) == 0 { + return false } - ver := ac.Params["version"] - if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol { - switch ac.Params["encoding"] { - case "delimited": - return FmtProtoDelim + escapingScheme - case "text": - return FmtProtoText + escapingScheme - case "compact-text": - return FmtProtoCompact + escapingScheme - } + target = parsed[0] + } + + if ac.Type != "*" && ac.Type != target.Type { + return false + } + if ac.SubType != "*" && ac.SubType != target.SubType { + return false + } + + // Default OpenMetrics version to OpenMetricsVersion_0_0_1. + acVersion := ac.Params["version"] + if acVersion == "" && ac.Type+"/"+ac.SubType == OpenMetricsType { + acVersion = OpenMetricsVersion_0_0_1 + } + if acVersion == "" && ac.Type == "text" && ac.SubType == "plain" { + acVersion = TextVersion + } + + // General param matching. + for k, v := range target.Params { + if k == "charset" { + continue } - if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") { - return FmtText + escapingScheme + acVal := ac.Params[k] + if k == "version" { + acVal = acVersion } - if ac.Type+"/"+ac.SubType == OpenMetricsType && (ver == OpenMetricsVersion_0_0_1 || ver == OpenMetricsVersion_1_0_0 || ver == "") { - switch ver { - case OpenMetricsVersion_1_0_0: - return FmtOpenMetrics_1_0_0 + escapingScheme - default: - return FmtOpenMetrics_0_0_1 + escapingScheme - } + if acVal != v { + return false } } - return FmtText + escapingScheme + + return true } // NewEncoder returns a new encoder based on content type negotiation. All @@ -181,6 +254,19 @@ func NewEncoder(w io.Writer, format Format, options ...EncoderOption) Encoder { close: func() error { return nil }, } case TypeOpenMetrics: + _, params, err := mime.ParseMediaType(string(format)) + if err == nil && params["version"] == OpenMetricsVersion_2_0_0 { + return encoderCloser{ + encode: func(v *dto.MetricFamily) error { + _, err := MetricFamilyToOpenMetrics20(w, model.EscapeMetricFamily(v, escapingScheme), options...) + return err + }, + close: func() error { + _, err := FinalizeOpenMetrics(w) + return err + }, + } + } return encoderCloser{ encode: func(v *dto.MetricFamily) error { _, err := MetricFamilyToOpenMetrics(w, model.EscapeMetricFamily(v, escapingScheme), options...) diff --git a/vendor/github.com/prometheus/common/expfmt/expfmt.go b/vendor/github.com/prometheus/common/expfmt/expfmt.go index 10bf35708c..ea9a0a2a04 100644 --- a/vendor/github.com/prometheus/common/expfmt/expfmt.go +++ b/vendor/github.com/prometheus/common/expfmt/expfmt.go @@ -16,50 +16,48 @@ package expfmt import ( "errors" + "mime" "strings" "github.com/prometheus/common/model" ) -// Format specifies the HTTP content type of the different wire protocols. -type Format string - // Constants to assemble the Content-Type values for the different wire -// protocols. The Content-Type strings here are all for the legacy exposition -// formats, where valid characters for metric names and label names are limited. -// Support for arbitrary UTF-8 characters in those names is already partially -// implemented in this module (see model.ValidationScheme), but to actually use -// it on the wire, new content-type strings will have to be agreed upon and -// added here. +// protocols. const ( TextVersion = "0.0.4" ProtoType = `application/vnd.google.protobuf` ProtoProtocol = `io.prometheus.client.MetricFamily` - // Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoCompact) instead. - ProtoFmt = ProtoType + "; proto=" + ProtoProtocol + ";" + ProtoFmt = ProtoType + "; proto=" + ProtoProtocol + ";" + OpenMetricsType = `application/openmetrics-text` //nolint:revive // Allow for underscores. OpenMetricsVersion_0_0_1 = "0.0.1" //nolint:revive // Allow for underscores. OpenMetricsVersion_1_0_0 = "1.0.0" + //nolint:revive // Allow for underscores. + OpenMetricsVersion_2_0_0 = "2.0.0" +) - // The Content-Type values for the different wire protocols. Do not do direct - // comparisons to these constants, instead use the comparison functions. - // - // Deprecated: Use expfmt.NewFormat(expfmt.TypeUnknown) instead. - FmtUnknown Format = `` - // Deprecated: Use expfmt.NewFormat(expfmt.TypeTextPlain) instead. - FmtText Format = `text/plain; version=` + TextVersion + `; charset=utf-8` - // Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoDelim) instead. - FmtProtoDelim Format = ProtoFmt + ` encoding=delimited` - // Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoText) instead. - FmtProtoText Format = ProtoFmt + ` encoding=text` - // Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoCompact) instead. +// Format specifies the HTTP content type of the different wire protocols. +// The Content-Type values for the different wire protocols represent +// baseline Content-Types used for HTTP headers and content negotiation. +// +// Because Content-Types on the wire may include dynamic parameters +// (such as "; escaping=...") or whitespace variations, avoid comparing Format +// values using direct equality (==). +type Format string + +const ( + FmtUnknown Format = `` + FmtText Format = `text/plain; version=` + TextVersion + `; charset=utf-8` + FmtProtoDelim Format = ProtoFmt + ` encoding=delimited` + FmtProtoText Format = ProtoFmt + ` encoding=text` FmtProtoCompact Format = ProtoFmt + ` encoding=compact-text` - // Deprecated: Use expfmt.NewFormat(expfmt.TypeOpenMetrics) instead. + //nolint:revive // Allow for underscores. + FmtOpenMetrics_2_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_2_0_0 + `; charset=utf-8` //nolint:revive // Allow for underscores. FmtOpenMetrics_1_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_1_0_0 + `; charset=utf-8` - // Deprecated: Use expfmt.NewFormat(expfmt.TypeOpenMetrics) instead. //nolint:revive // Allow for underscores. FmtOpenMetrics_0_0_1 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_0_0_1 + `; charset=utf-8` ) @@ -86,7 +84,7 @@ const ( // NewFormat generates a new Format from the type provided. Mostly used for // tests, most Formats should be generated as part of content negotiation in -// encode.go. If a type has more than one version, the latest version will be +// encode.go. If a type has more than one version, the latest stable version will be // returned. func NewFormat(t FormatType) Format { switch t { @@ -107,6 +105,9 @@ func NewFormat(t FormatType) Format { // NewOpenMetricsFormat generates a new OpenMetrics format matching the // specified version number. +// +// Note: OpenMetrics version 2.0.0 is experimental and encode-only (supporting +// counter, gauge, summary, untyped, histogram, and gaugehistogram metric types). func NewOpenMetricsFormat(version string) (Format, error) { if version == OpenMetricsVersion_0_0_1 { return FmtOpenMetrics_0_0_1, nil @@ -114,30 +115,22 @@ func NewOpenMetricsFormat(version string) (Format, error) { if version == OpenMetricsVersion_1_0_0 { return FmtOpenMetrics_1_0_0, nil } + if version == OpenMetricsVersion_2_0_0 { + // OpenMetrics 2.0.0 is experimental and encode-only. + return FmtOpenMetrics_2_0_0, nil + } return FmtUnknown, errors.New("unknown open metrics version string") } -// WithEscapingScheme returns a copy of Format with the specified escaping -// scheme appended to the end. If an escaping scheme already exists it is -// removed. -func (f Format) WithEscapingScheme(s model.EscapingScheme) Format { - var terms []string - for p := range strings.SplitSeq(string(f), ";") { - toks := strings.Split(p, "=") - if len(toks) != 2 { - trimmed := strings.TrimSpace(p) - if len(trimmed) > 0 { - terms = append(terms, trimmed) - } - continue - } - key := strings.TrimSpace(toks[0]) - if key != model.EscapingKey { - terms = append(terms, strings.TrimSpace(p)) - } +// Version returns the version parameter without validating whether it is supported. +// It returns an empty string if the parameter is absent or the Content-Type +// cannot be parsed. +func (f Format) Version() string { + _, params, err := mime.ParseMediaType(string(f)) + if err != nil { + return "" } - terms = append(terms, model.EscapingKey+"="+s.String()) - return Format(strings.Join(terms, "; ")) + return params["version"] } // FormatType deduces an overall FormatType for the given format. @@ -189,8 +182,31 @@ func (f Format) FormatType() FormatType { } } -// ToEscapingScheme returns an EscapingScheme depending on the Format. Iff the -// Format contains a escaping=allow-utf-8 term, it will select NoEscaping. If a valid +// WithEscapingScheme returns a copy of Format with the specified escaping +// scheme appended to the end. If an escaping scheme already exists it is +// removed. +func (f Format) WithEscapingScheme(s model.EscapingScheme) Format { + var terms []string + for p := range strings.SplitSeq(string(f), ";") { + toks := strings.Split(p, "=") + if len(toks) != 2 { + trimmed := strings.TrimSpace(p) + if len(trimmed) > 0 { + terms = append(terms, trimmed) + } + continue + } + key := strings.TrimSpace(toks[0]) + if key != model.EscapingKey { + terms = append(terms, strings.TrimSpace(p)) + } + } + terms = append(terms, model.EscapingKey+"="+s.String()) + return Format(strings.Join(terms, "; ")) +} + +// ToEscapingScheme returns an EscapingScheme depending on the Format. IFF the +// Format contains an escaping=allow-utf-8 term, it will select NoEscaping. If a valid // "escaping" term exists, that will be used. Otherwise, the global default will // be returned. func (f Format) ToEscapingScheme() model.EscapingScheme { diff --git a/vendor/github.com/prometheus/common/expfmt/openmetrics_2_0_create.go b/vendor/github.com/prometheus/common/expfmt/openmetrics_2_0_create.go new file mode 100644 index 0000000000..600ac32f5c --- /dev/null +++ b/vendor/github.com/prometheus/common/expfmt/openmetrics_2_0_create.go @@ -0,0 +1,1221 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package expfmt + +import ( + "bufio" + "errors" + "fmt" + "io" + "math" + "strconv" + "strings" + "unicode/utf8" + + dto "github.com/prometheus/client_model/go" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// MetricFamilyToOpenMetrics20 converts a MetricFamily proto message into the +// OpenMetrics text format version 2.0.0 and writes the resulting lines to 'out'. +// It returns the number of bytes written and any error encountered. +// +// NOTE: This method targets OpenMetrics 2.0.0 (currently aligned with 2.0-rc.0) which is experimental and +// encode-only. All metric types defined in client_model (counter, gauge, summary, untyped, histogram, +// and gaugehistogram) are supported; info and stateset are not supported as they are not present in +// client_model. Breaking changes might happen in the future. +// EncoderOptions are accepted for signature compatibility with +// MetricFamilyToOpenMetrics and are currently ignored. +// +// OpenMetrics 2.0 enforces stricter validation rules defined in the specification +// than Prometheus text or OpenMetrics 1.0 formats (such as requiring non-negative +// count and sum, and non-negative quantile values for summaries). Consequently, +// MetricFamilyToOpenMetrics20 may reject metric families that the other encoders accept. +func MetricFamilyToOpenMetrics20(out io.Writer, in *dto.MetricFamily, options ...EncoderOption) (written int, err error) { + // Options are accepted for signature compatibility and ignored. + _ = options + name := in.GetName() + if name == "" { + return 0, fmt.Errorf("MetricFamily has no name: %s", in) + } + if containsRawNewline(name) { + return 0, fmt.Errorf("MetricFamily name %q contains raw newlines", name) + } + if !utf8.ValidString(name) { + return 0, fmt.Errorf("MetricFamily name %q is not valid UTF-8", name) + } + if in.Help != nil { + if !utf8.ValidString(*in.Help) { + return 0, fmt.Errorf("MetricFamily help %q is not valid UTF-8", *in.Help) + } + // A carriage return may appear in HELP, but not at the end, where it + // would make the line end in "\r\n". + if strings.HasSuffix(*in.Help, "\r") { + return 0, fmt.Errorf("MetricFamily help %q ends with a carriage return", *in.Help) + } + } + if in.Unit != nil { + if containsRawNewline(*in.Unit) { + return 0, fmt.Errorf("MetricFamily unit %q contains raw newlines", *in.Unit) + } + if !utf8.ValidString(*in.Unit) { + return 0, fmt.Errorf("MetricFamily unit %q is not valid UTF-8", *in.Unit) + } + } + + // Try the interface upgrade. If it doesn't work, we'll use a + // bufio.Writer from the sync.Pool. + w, ok := out.(enhancedWriter) + if !ok { + b := bufPool.Get().(*bufio.Writer) + b.Reset(out) + w = b + defer func() { + bErr := b.Flush() + if err == nil { + err = bErr + } + bufPool.Put(b) + }() + } + + var ( + n int + metricType = in.GetType() + ) + + // Comments, first HELP, then TYPE. + if in.Help != nil { + n, err = w.WriteString("# HELP ") + written += n + if err != nil { + return written, err + } + n, err = writeName(w, name) + written += n + if err != nil { + return written, err + } + err = w.WriteByte(' ') + written++ + if err != nil { + return written, err + } + n, err = writeEscapedString(w, *in.Help, true) + written += n + if err != nil { + return written, err + } + err = w.WriteByte('\n') + written++ + if err != nil { + return written, err + } + } + n, err = w.WriteString("# TYPE ") + written += n + if err != nil { + return written, err + } + n, err = writeName(w, name) + written += n + if err != nil { + return written, err + } + switch metricType { + case dto.MetricType_COUNTER: + n, err = w.WriteString(" counter\n") + case dto.MetricType_GAUGE: + n, err = w.WriteString(" gauge\n") + case dto.MetricType_SUMMARY: + n, err = w.WriteString(" summary\n") + case dto.MetricType_UNTYPED: + n, err = w.WriteString(" unknown\n") + case dto.MetricType_HISTOGRAM: + n, err = w.WriteString(" histogram\n") + case dto.MetricType_GAUGE_HISTOGRAM: + n, err = w.WriteString(" gaugehistogram\n") + default: + // TODO: Support Info and StateSet once they are supported in the + // Prometheus protobuf format. + return written, fmt.Errorf("unknown metric type %s", metricType.String()) + } + written += n + if err != nil { + return written, err + } + if in.Unit != nil { + n, err = w.WriteString("# UNIT ") + written += n + if err != nil { + return written, err + } + n, err = writeName(w, name) + written += n + if err != nil { + return written, err + } + + err = w.WriteByte(' ') + written++ + if err != nil { + return written, err + } + n, err = writeEscapedString(w, *in.Unit, true) + written += n + if err != nil { + return written, err + } + err = w.WriteByte('\n') + written++ + if err != nil { + return written, err + } + } + + // Finally the samples, one line for each. + for _, metric := range in.Metric { + if metric == nil { + return written, fmt.Errorf("expected non-nil metric in MetricFamily %s", name) + } + switch metricType { + case dto.MetricType_COUNTER: + if metric.Counter == nil { + return written, fmt.Errorf("expected counter in metric %s %s", name, metric) + } + val := metric.Counter.GetValue() + if math.IsNaN(val) { + return written, fmt.Errorf("counter value cannot be NaN in metric %s", name) + } + if val < 0 { + return written, fmt.Errorf("counter value cannot be negative (%g) in metric %s", val, name) + } + n, err = writeOpenMetrics20Sample(w, name, metric, val, 0, false, metric.Counter.CreatedTimestamp, metric.Counter.Exemplar) + case dto.MetricType_GAUGE: + if metric.Gauge == nil { + return written, fmt.Errorf("expected gauge in metric %s %s", name, metric) + } + n, err = writeOpenMetrics20Sample(w, name, metric, metric.Gauge.GetValue(), 0, false, nil, nil) + case dto.MetricType_UNTYPED: + if metric.Untyped == nil { + return written, fmt.Errorf("expected untyped in metric %s %s", name, metric) + } + n, err = writeOpenMetrics20Sample(w, name, metric, metric.Untyped.GetValue(), 0, false, nil, nil) + case dto.MetricType_SUMMARY: + if metric.Summary == nil { + return written, fmt.Errorf("expected summary in metric %s %s", name, metric) + } + n, err = writeCompositeSummary(w, name, metric) + case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM: + if metric.Histogram == nil { + return written, fmt.Errorf("expected histogram in metric %s %s", name, metric) + } + n, err = writeCompositeHistogram(w, name, metric, metricType == dto.MetricType_GAUGE_HISTOGRAM) + default: + return written, fmt.Errorf("unexpected type in metric %s %s", name, metric) + } + written += n + if err != nil { + return written, err + } + } + return written, nil +} + +// writeOpenMetrics20Sample writes a single sample for simple types (Counter, Gauge, Untyped). +func writeOpenMetrics20Sample(w enhancedWriter, name string, metric *dto.Metric, floatValue float64, intValue uint64, useIntValue bool, startTimestamp *timestamppb.Timestamp, exemplar *dto.Exemplar) (int, error) { + if err := validateLabels20(metric.Label); err != nil { + return 0, err + } + written := 0 + n, err := writeOpenMetricsNameAndLabelPairs(w, name, metric.Label, "", 0) + written += n + if err != nil { + return written, err + } + err = w.WriteByte(' ') + written++ + if err != nil { + return written, err + } + + if useIntValue { + n, err = writeUint(w, intValue) + } else { + n, err = writeOpenMetricsFloat(w, floatValue) + } + written += n + if err != nil { + return written, err + } + + if metric.TimestampMs != nil { + err = w.WriteByte(' ') + written++ + if err != nil { + return written, err + } + n, err = writeOpenMetrics20Timestamp(w, float64(*metric.TimestampMs)/1000) + written += n + if err != nil { + return written, err + } + } + + // Start Timestamp + if startTimestamp != nil { + if err := startTimestamp.CheckValid(); err != nil { + return written, fmt.Errorf("invalid created timestamp in metric %s: %w", name, err) + } + n, err = w.WriteString(" st@") + written += n + if err != nil { + return written, err + } + n, err = writeProtoTimestamp(w, startTimestamp) + written += n + if err != nil { + return written, err + } + } + + if exemplar != nil { + n, err = writeExemplar20(w, exemplar) + written += n + if err != nil { + return written, err + } + } + + err = w.WriteByte('\n') + written++ + if err != nil { + return written, err + } + return written, nil +} + +// writeExemplar20 writes the provided exemplar in OpenMetrics 2.0 format to w. +// In OpenMetrics 2.0, invalid exemplars or exemplars without a timestamp are dropped. +func writeExemplar20(w enhancedWriter, e *dto.Exemplar) (int, error) { + if e == nil { + return 0, nil + } + // In OpenMetrics 2.0, invalid exemplars are dropped rather than failing the entire exposition. + if err := validateExemplar20(e); err != nil { + return 0, nil + } + written := 0 + n, err := w.WriteString(" # ") + written += n + if err != nil { + return written, err + } + if len(e.Label) == 0 { + n, err = w.WriteString("{}") + } else { + n, err = writeOpenMetricsNameAndLabelPairs(w, "", e.Label, "", 0) + } + written += n + if err != nil { + return written, err + } + err = w.WriteByte(' ') + written++ + if err != nil { + return written, err + } + n, err = writeOpenMetricsFloat(w, e.GetValue()) + written += n + if err != nil { + return written, err + } + err = w.WriteByte(' ') + written++ + if err != nil { + return written, err + } + ts := e.Timestamp + n, err = writeProtoTimestamp(w, ts) + written += n + if err != nil { + return written, err + } + return written, nil +} + +// writeOpenMetrics20Timestamp writes a float64 as a timestamp without scientific notation. +func writeOpenMetrics20Timestamp(w enhancedWriter, f float64) (int, error) { + bp := numBufPool.Get().(*[]byte) + *bp = strconv.AppendFloat((*bp)[:0], f, 'f', -1, 64) + written, err := w.Write(*bp) + numBufPool.Put(bp) + return written, err +} + +func writeCompositeSummary(w enhancedWriter, name string, metric *dto.Metric) (int, error) { + s := metric.Summary + if s == nil { + return 0, fmt.Errorf("expected summary in metric %s", name) + } + + if err := validateLabels20(metric.Label); err != nil { + return 0, err + } + for _, lp := range metric.Label { + if lp.GetName() == "quantile" { + return 0, fmt.Errorf("metric %s is a summary but label set contains %q label", name, "quantile") + } + } + + sum := s.GetSampleSum() + if math.IsNaN(sum) { + return 0, fmt.Errorf("summary sum cannot be NaN in metric %s", name) + } + if sum < 0 { + return 0, fmt.Errorf("summary sum cannot be negative (%g) in metric %s", sum, name) + } + + var prevQuantile float64 + for i, q := range s.Quantile { + if q == nil { + return 0, fmt.Errorf("expected non-nil quantile in metric %s", name) + } + qv := q.GetQuantile() + if math.IsNaN(qv) { + return 0, fmt.Errorf("summary quantile cannot be NaN in metric %s", name) + } + if qv < 0 || qv > 1 { + return 0, fmt.Errorf("summary quantile %g must be between 0 and 1 in metric %s", qv, name) + } + if i > 0 && qv <= prevQuantile { + return 0, fmt.Errorf("summary quantiles must be strictly increasing: %g <= %g in metric %s", qv, prevQuantile, name) + } + prevQuantile = qv + + v := q.GetValue() + if !math.IsNaN(v) && v < 0 { + return 0, fmt.Errorf("summary quantile value cannot be negative (%g) in metric %s", v, name) + } + } + + if s.CreatedTimestamp != nil { + if err := s.CreatedTimestamp.CheckValid(); err != nil { + return 0, fmt.Errorf("invalid created timestamp in metric %s: %w", name, err) + } + } + + written := 0 + n, err := writeOpenMetricsNameAndLabelPairs(w, name, metric.Label, "", 0) + written += n + if err != nil { + return written, err + } + + n, err = w.WriteString(" {count:") + written += n + if err != nil { + return written, err + } + + n, err = writeUint(w, s.GetSampleCount()) + written += n + if err != nil { + return written, err + } + + n, err = w.WriteString(",sum:") + written += n + if err != nil { + return written, err + } + + n, err = writeFloat(w, sum) + written += n + if err != nil { + return written, err + } + + n, err = w.WriteString(",quantile:[") + written += n + if err != nil { + return written, err + } + + for i, q := range s.Quantile { + if i > 0 { + err = w.WriteByte(',') + written++ + if err != nil { + return written, err + } + } + n, err = writeFloat(w, q.GetQuantile()) + written += n + if err != nil { + return written, err + } + err = w.WriteByte(':') + written++ + if err != nil { + return written, err + } + n, err = writeFloat(w, q.GetValue()) + written += n + if err != nil { + return written, err + } + } + + n, err = w.WriteString("]}") + written += n + if err != nil { + return written, err + } + + if metric.TimestampMs != nil { + err = w.WriteByte(' ') + written++ + if err != nil { + return written, err + } + n, err = writeOpenMetrics20Timestamp(w, float64(*metric.TimestampMs)/1000) + written += n + if err != nil { + return written, err + } + } + + if s.CreatedTimestamp != nil { + n, err = w.WriteString(" st@") + written += n + if err != nil { + return written, err + } + n, err = writeProtoTimestamp(w, s.CreatedTimestamp) + written += n + if err != nil { + return written, err + } + } + + err = w.WriteByte('\n') + written++ + if err != nil { + return written, err + } + + return written, nil +} + +func writeCompositeHistogram(w enhancedWriter, name string, metric *dto.Metric, isGauge bool) (int, error) { + h := metric.Histogram + if h == nil { + return 0, fmt.Errorf("expected histogram in metric %s", name) + } + + isNative := h.Schema != nil + hasClassicBuckets := len(h.Bucket) > 0 || !isNative + + if err := validateLabels20(metric.Label); err != nil { + return 0, err + } + if hasClassicBuckets { + for _, lp := range metric.Label { + if lp.GetName() == "le" { + return 0, fmt.Errorf("metric %s has classic buckets but label set contains %q label", name, "le") + } + } + } + + if h.SampleCountFloat != nil { + c := *h.SampleCountFloat + if math.IsNaN(c) { + if isGauge { + return 0, fmt.Errorf("gaugehistogram count cannot be NaN in metric %s", name) + } + return 0, fmt.Errorf("histogram count cannot be NaN in metric %s", name) + } + if !isGauge && c < 0 { + return 0, fmt.Errorf("histogram count cannot be negative (%g) in metric %s", c, name) + } + } + + var isFloatCount bool + var sampleCountFloat float64 + var sampleCountUint uint64 + switch { + case h.SampleCountFloat != nil && (*h.SampleCountFloat > 0 || h.SampleCount == nil || isGauge): + isFloatCount = true + sampleCountFloat = *h.SampleCountFloat + case h.SampleCount != nil: + sampleCountUint = *h.SampleCount + sampleCountFloat = float64(sampleCountUint) + } + + if isNative { + if err := validateNativeHistogram(name, h, isGauge); err != nil { + return 0, err + } + } + + if hasClassicBuckets { + if err := validateClassicBuckets(name, h, sampleCountFloat, sampleCountUint, isFloatCount, isGauge); err != nil { + return 0, err + } + } + + if !isGauge && h.CreatedTimestamp != nil { + if err := h.CreatedTimestamp.CheckValid(); err != nil { + return 0, fmt.Errorf("invalid created timestamp in metric %s: %w", name, err) + } + } + + written := 0 + n, err := writeOpenMetricsNameAndLabelPairs(w, name, metric.Label, "", 0) + written += n + if err != nil { + return written, err + } + + n, err = w.WriteString(" {") + written += n + if err != nil { + return written, err + } + + if isGauge { + n, err = w.WriteString("gcount:") + } else { + n, err = w.WriteString("count:") + } + written += n + if err != nil { + return written, err + } + + if isFloatCount { + n, err = writeFloat(w, sampleCountFloat) + } else { + n, err = writeUint(w, sampleCountUint) + } + written += n + if err != nil { + return written, err + } + + if isGauge { + n, err = w.WriteString(",gsum:") + } else { + n, err = w.WriteString(",sum:") + } + written += n + if err != nil { + return written, err + } + n, err = writeFloat(w, h.GetSampleSum()) + written += n + if err != nil { + return written, err + } + + if isNative { + n, err = writeNativeBuckets(w, h, isGauge) + written += n + if err != nil { + return written, err + } + } + + var classicExemplars []*dto.Exemplar + if hasClassicBuckets { + n, err = writeClassicBuckets(w, h, sampleCountFloat, sampleCountUint, isFloatCount, isGauge, &classicExemplars) + written += n + if err != nil { + return written, err + } + } + + err = w.WriteByte('}') + written++ + if err != nil { + return written, err + } + + if metric.TimestampMs != nil { + err = w.WriteByte(' ') + written++ + if err != nil { + return written, err + } + n, err = writeOpenMetrics20Timestamp(w, float64(*metric.TimestampMs)/1000) + written += n + if err != nil { + return written, err + } + } + + if !isGauge && h.CreatedTimestamp != nil { + ts := h.CreatedTimestamp + n, err = w.WriteString(" st@") + written += n + if err != nil { + return written, err + } + n, err = writeProtoTimestamp(w, ts) + written += n + if err != nil { + return written, err + } + } + + var exemplarsToEmit []*dto.Exemplar + if len(classicExemplars) > 0 { + // We follow the spec suggestion and prefer classic histogram + // exemplars in dual mode. + exemplarsToEmit = classicExemplars + } else if len(h.Exemplars) > 0 { + exemplarsToEmit = h.Exemplars + } + + for _, e := range exemplarsToEmit { + if e == nil || e.Timestamp == nil { + continue + } + n, err = writeExemplar20(w, e) + written += n + if err != nil { + return written, err + } + } + + err = w.WriteByte('\n') + written++ + if err != nil { + return written, err + } + + return written, nil +} + +func validateNativeHistogram(name string, h *dto.Histogram, isGauge bool) error { + schema := *h.Schema + if schema < -4 || schema > 8 { + return fmt.Errorf("native histogram schema %d is out of range [-4, 8] in metric %s", schema, name) + } + + zeroThreshold := h.GetZeroThreshold() + if math.IsNaN(zeroThreshold) || math.IsInf(zeroThreshold, 0) || zeroThreshold < 0 { + return fmt.Errorf("native histogram zero_threshold %g must be a non-negative, finite number in metric %s", zeroThreshold, name) + } + + if h.ZeroCountFloat != nil { + c := *h.ZeroCountFloat + if math.IsNaN(c) { + return fmt.Errorf("native histogram zero_count cannot be NaN in metric %s", name) + } + if !isGauge && c < 0 { + return fmt.Errorf("native histogram zero_count cannot be negative (%g) in metric %s", c, name) + } + } + + if err := validateSpansAndBuckets(name, "negative", h.NegativeSpan, h.NegativeDelta, h.NegativeCount, isGauge); err != nil { + return err + } + return validateSpansAndBuckets(name, "positive", h.PositiveSpan, h.PositiveDelta, h.PositiveCount, isGauge) +} + +func validateSpansAndBuckets( + name string, + spanName string, + spans []*dto.BucketSpan, + deltas []int64, + floatCounts []float64, + isGauge bool, +) error { + isFloatBuckets := len(floatCounts) > 0 + var numBuckets int + if isFloatBuckets { + numBuckets = len(floatCounts) + } else { + numBuckets = len(deltas) + } + + var totalLength uint64 + for i, span := range spans { + if span == nil { + return errors.New("expected non-nil bucket span") + } + if i > 0 && span.GetOffset() < 0 { + return fmt.Errorf("subsequent %s span offset cannot be negative: %d in metric %s", spanName, span.GetOffset(), name) + } + totalLength += uint64(span.GetLength()) + } + + if numBuckets == 0 { + if totalLength == 0 { + return nil + } + return fmt.Errorf("sum of %s span lengths (%d) does not match bucket count (0) in metric %s", spanName, totalLength, name) + } + + if totalLength != uint64(numBuckets) { + return fmt.Errorf("sum of %s span lengths (%d) does not match bucket count (%d) in metric %s", spanName, totalLength, numBuckets, name) + } + + if isFloatBuckets { + for _, v := range floatCounts { + if math.IsNaN(v) { + return fmt.Errorf("%s bucket count cannot be NaN in metric %s", spanName, name) + } + if !isGauge && v < 0 { + return fmt.Errorf("%s bucket count cannot be negative (%g) in metric %s", spanName, v, name) + } + } + } else { + var current int64 + for _, d := range deltas { + current += d + if !isGauge && current < 0 { + return fmt.Errorf("%s bucket count cannot be negative (%d) in metric %s", spanName, current, name) + } + } + } + return nil +} + +func writeNativeBuckets(w enhancedWriter, h *dto.Histogram, isGauge bool) (int, error) { + schema := *h.Schema + zeroThreshold := h.GetZeroThreshold() + + var isFloatZeroCount bool + var zeroCountFloat float64 + var zeroCountUint uint64 + switch { + case h.ZeroCountFloat != nil && (*h.ZeroCountFloat > 0 || h.ZeroCount == nil || isGauge): + isFloatZeroCount = true + zeroCountFloat = *h.ZeroCountFloat + case h.ZeroCount != nil: + zeroCountUint = *h.ZeroCount + zeroCountFloat = float64(zeroCountUint) + } + + written := 0 + n, err := w.WriteString(",schema:") + written += n + if err != nil { + return written, err + } + n, err = writeInt(w, int64(schema)) + written += n + if err != nil { + return written, err + } + + n, err = w.WriteString(",zero_threshold:") + written += n + if err != nil { + return written, err + } + n, err = writeFloat(w, zeroThreshold) + written += n + if err != nil { + return written, err + } + + n, err = w.WriteString(",zero_count:") + written += n + if err != nil { + return written, err + } + if isFloatZeroCount { + n, err = writeFloat(w, zeroCountFloat) + } else { + n, err = writeUint(w, zeroCountUint) + } + written += n + if err != nil { + return written, err + } + + n, err = writeSpansAndBuckets(w, ",negative_spans:[", ",negative_buckets:[", h.NegativeSpan, h.NegativeDelta, h.NegativeCount) + written += n + if err != nil { + return written, err + } + + n, err = writeSpansAndBuckets(w, ",positive_spans:[", ",positive_buckets:[", h.PositiveSpan, h.PositiveDelta, h.PositiveCount) + written += n + if err != nil { + return written, err + } + + return written, nil +} + +func writeSpansAndBuckets( + w enhancedWriter, + spansPrefix string, + bucketsPrefix string, + spans []*dto.BucketSpan, + deltas []int64, + floatCounts []float64, +) (int, error) { + isFloatBuckets := len(floatCounts) > 0 + var numBuckets int + if isFloatBuckets { + numBuckets = len(floatCounts) + } else { + numBuckets = len(deltas) + } + + if numBuckets == 0 { + return 0, nil + } + + written := 0 + n, err := w.WriteString(spansPrefix) + written += n + if err != nil { + return written, err + } + for i, span := range spans { + if i > 0 { + err = w.WriteByte(',') + written++ + if err != nil { + return written, err + } + } + n, err = writeInt(w, int64(span.GetOffset())) + written += n + if err != nil { + return written, err + } + err = w.WriteByte(':') + written++ + if err != nil { + return written, err + } + n, err = writeUint(w, uint64(span.GetLength())) + written += n + if err != nil { + return written, err + } + } + err = w.WriteByte(']') + written++ + if err != nil { + return written, err + } + + n, err = w.WriteString(bucketsPrefix) + written += n + if err != nil { + return written, err + } + if isFloatBuckets { + for i, v := range floatCounts { + if i > 0 { + err = w.WriteByte(',') + written++ + if err != nil { + return written, err + } + } + n, err = writeFloat(w, v) + written += n + if err != nil { + return written, err + } + } + } else { + var current int64 + for i, d := range deltas { + if i > 0 { + err = w.WriteByte(',') + written++ + if err != nil { + return written, err + } + } + current += d + n, err = writeInt(w, current) + written += n + if err != nil { + return written, err + } + } + } + err = w.WriteByte(']') + written++ + if err != nil { + return written, err + } + + return written, nil +} + +func validateClassicBuckets( + name string, + h *dto.Histogram, + sampleCount float64, + sampleCountUint uint64, + isFloatCount bool, + isGauge bool, +) error { + var infSeen bool + var prevBound float64 + var prevCount float64 + for i, b := range h.Bucket { + if b == nil { + return errors.New("expected non-nil bucket") + } + ub := b.GetUpperBound() + if math.IsNaN(ub) { + return fmt.Errorf("classic bucket upper bound cannot be NaN in metric %s", name) + } + if i > 0 && ub <= prevBound { + return fmt.Errorf("classic bucket upper bounds must be strictly increasing: %g <= %g in metric %s", ub, prevBound, name) + } + prevBound = ub + + if b.CumulativeCountFloat != nil { + c := *b.CumulativeCountFloat + if math.IsNaN(c) { + return fmt.Errorf("classic bucket count cannot be NaN in metric %s", name) + } + if !isGauge && c < 0 { + return fmt.Errorf("classic bucket count cannot be negative (%g) in metric %s", c, name) + } + } + + var bCount float64 + switch { + case b.CumulativeCountFloat != nil && (*b.CumulativeCountFloat > 0 || b.CumulativeCount == nil || isGauge): + bCount = *b.CumulativeCountFloat + case b.CumulativeCount != nil: + bCount = float64(*b.CumulativeCount) + } + + if !isGauge && i > 0 && bCount < prevCount { + return fmt.Errorf("classic bucket counts must be monotonically increasing: %g < %g in metric %s", bCount, prevCount, name) + } + prevCount = bCount + + if math.IsInf(ub, +1) { + if i != len(h.Bucket)-1 { + return fmt.Errorf("+Inf bucket must be the last bucket in metric %s", name) + } + infSeen = true + if isFloatCount { + if bCount != sampleCount { + return fmt.Errorf("classic bucket +Inf count (%g) does not match sample count (%g) in metric %s", bCount, sampleCount, name) + } + } else { + if b.CumulativeCount != nil { + if *b.CumulativeCount != sampleCountUint { + return fmt.Errorf("classic bucket +Inf count (%d) does not match sample count (%d) in metric %s", *b.CumulativeCount, sampleCountUint, name) + } + } else { + if bCount < 0 || bCount > math.MaxUint64 || uint64(bCount) != sampleCountUint || float64(uint64(bCount)) != bCount { + return fmt.Errorf("classic bucket +Inf count (%g) does not match sample count (%d) in metric %s", bCount, sampleCountUint, name) + } + } + } + } + } + + if !infSeen && len(h.Bucket) > 0 && !isGauge { + if sampleCount < prevCount { + return fmt.Errorf("sample count (%g) is less than highest bucket count (%g) in metric %s", sampleCount, prevCount, name) + } + } + return nil +} + +func writeClassicBuckets( + w enhancedWriter, + h *dto.Histogram, + sampleCount float64, + sampleCountUint uint64, + isFloatCount bool, + isGauge bool, + collectedExemplars *[]*dto.Exemplar, +) (int, error) { + var infSeen bool + written := 0 + n, err := w.WriteString(",bucket:[") + written += n + if err != nil { + return written, err + } + + for i, b := range h.Bucket { + if i > 0 { + err = w.WriteByte(',') + written++ + if err != nil { + return written, err + } + } + n, err = writeFloat(w, b.GetUpperBound()) + written += n + if err != nil { + return written, err + } + err = w.WriteByte(':') + written++ + if err != nil { + return written, err + } + switch { + case b.CumulativeCountFloat != nil && (*b.CumulativeCountFloat > 0 || b.CumulativeCount == nil || isGauge): + n, err = writeFloat(w, *b.CumulativeCountFloat) + case b.CumulativeCount != nil: + n, err = writeUint(w, *b.CumulativeCount) + default: + n, err = writeUint(w, 0) + } + written += n + if err != nil { + return written, err + } + if math.IsInf(b.GetUpperBound(), +1) { + infSeen = true + } + if b.Exemplar != nil && b.Exemplar.Timestamp != nil { + *collectedExemplars = append(*collectedExemplars, b.Exemplar) + } + } + + if !infSeen { + if len(h.Bucket) > 0 { + err = w.WriteByte(',') + written++ + if err != nil { + return written, err + } + } + n, err = w.WriteString("+Inf:") + written += n + if err != nil { + return written, err + } + if isFloatCount { + n, err = writeFloat(w, sampleCount) + } else { + n, err = writeUint(w, sampleCountUint) + } + written += n + if err != nil { + return written, err + } + } + + err = w.WriteByte(']') + written++ + if err != nil { + return written, err + } + + return written, nil +} + +func validateLabels20(labels []*dto.LabelPair) error { + for _, lp := range labels { + if lp == nil { + return errors.New("expected non-nil label pair") + } + lname := lp.GetName() + if lname == "" { + return errors.New("label name cannot be empty") + } + if containsRawNewline(lname) { + return fmt.Errorf("label name %q contains raw newlines", lname) + } + } + return nil +} + +func containsRawNewline(s string) bool { + return strings.IndexByte(s, '\n') >= 0 || strings.IndexByte(s, '\r') >= 0 +} + +func validateExemplar20(e *dto.Exemplar) error { + if e.Timestamp == nil { + return errors.New("exemplar timestamp is required") + } + if err := e.Timestamp.CheckValid(); err != nil { + return err + } + return validateLabels20(e.Label) +} + +func writeProtoTimestamp(w enhancedWriter, ts *timestamppb.Timestamp) (int, error) { + if err := ts.CheckValid(); err != nil { + return 0, err + } + if ts.Nanos == 0 { + return writeInt(w, ts.Seconds) + } + sec := ts.Seconds + nanos := int64(ts.Nanos) + if sec < 0 { + sec++ + nanos = 1_000_000_000 - nanos + if sec == 0 { + n, err := w.WriteString("-0") + if err != nil { + return n, err + } + n2, err := writeNanos(w, nanos) + return n + n2, err + } + } + + n, err := writeInt(w, sec) + if err != nil { + return n, err + } + n2, err := writeNanos(w, nanos) + return n + n2, err +} + +func writeNanos(w enhancedWriter, nanos int64) (int, error) { + err := w.WriteByte('.') + if err != nil { + return 0, err + } + written := 1 + bp := numBufPool.Get().(*[]byte) + *bp = strconv.AppendInt((*bp)[:0], nanos, 10) + pad := 9 - len(*bp) + for range pad { + err = w.WriteByte('0') + written++ + if err != nil { + numBufPool.Put(bp) + return written, err + } + } + val := *bp + for len(val) > 0 && val[len(val)-1] == '0' { + val = val[:len(val)-1] + } + n2, err := w.Write(val) + written += n2 + numBufPool.Put(bp) + return written, err +} diff --git a/vendor/github.com/prometheus/common/expfmt/text_parse.go b/vendor/github.com/prometheus/common/expfmt/text_parse.go index 4ce1f40b81..10529300f4 100644 --- a/vendor/github.com/prometheus/common/expfmt/text_parse.go +++ b/vendor/github.com/prometheus/common/expfmt/text_parse.go @@ -194,6 +194,7 @@ func (p *TextParser) reset(in io.Reader) { // start of a line (or whitespace leading up to it). func (p *TextParser) startOfLine() stateFn { p.lineCount++ + p.currentMetric = nil p.currentMetricIsInsideBraces = false p.currentMetricInsideBracesIsPresent = false if p.skipBlankTab(); p.err != nil { @@ -339,12 +340,11 @@ func (p *TextParser) startLabelName() stateFn { return nil // Unexpected end of input. } if p.currentByte == '}' { - if p.currentMF == nil { + if p.currentMetric == nil { // The closing brace was reached before any metric name was read, // e.g. for the input "{}". There is no metric to attach labels to, // so this is a malformed exposition. This mirrors the guard in - // startLabelValue. currentMF (not currentMetric) is checked because - // reset only clears currentMF between parses. + // startLabelValue. currentMetric is cleared at the start of each line. p.parseError("invalid metric name") p.currentLabelPairs = nil return nil @@ -495,7 +495,7 @@ func (p *TextParser) startLabelValue() stateFn { return p.startLabelName case '}': - if p.currentMF == nil { + if p.currentMetric == nil { p.parseError("invalid metric name") return nil } diff --git a/vendor/github.com/prometheus/common/model/signature.go b/vendor/github.com/prometheus/common/model/signature.go index dc8a0026c4..b723e3bb2d 100644 --- a/vendor/github.com/prometheus/common/model/signature.go +++ b/vendor/github.com/prometheus/common/model/signature.go @@ -14,7 +14,7 @@ package model import ( - "sort" + "slices" ) // SeparatorByte is a byte that cannot occur in valid UTF-8 sequences and is @@ -37,7 +37,7 @@ func LabelsToSignature(labels map[string]string) uint64 { for labelName := range labels { labelNames = append(labelNames, labelName) } - sort.Strings(labelNames) + slices.Sort(labelNames) sum := hashNew() for _, labelName := range labelNames { @@ -60,7 +60,7 @@ func labelSetToFingerprint(ls LabelSet) Fingerprint { for labelName := range ls { labelNames = append(labelNames, labelName) } - sort.Sort(labelNames) + slices.Sort(labelNames) sum := hashNew() for _, labelName := range labelNames { @@ -100,7 +100,7 @@ func SignatureForLabels(m Metric, labels ...LabelName) uint64 { return emptyLabelSignature } - sort.Sort(LabelNames(labels)) + slices.Sort(labels) sum := hashNew() for _, label := range labels { @@ -129,7 +129,7 @@ func SignatureWithoutLabels(m Metric, labels map[LabelName]struct{}) uint64 { if len(labelNames) == 0 { return emptyLabelSignature } - sort.Sort(labelNames) + slices.Sort(labelNames) sum := hashNew() for _, labelName := range labelNames { diff --git a/vendor/github.com/prometheus/common/model/time.go b/vendor/github.com/prometheus/common/model/time.go index 0854753f4a..9860717422 100644 --- a/vendor/github.com/prometheus/common/model/time.go +++ b/vendor/github.com/prometheus/common/model/time.go @@ -164,6 +164,34 @@ func (t *Time) UnmarshalJSON(b []byte) error { // This type should not propagate beyond the scope of input/output processing. type Duration time.Duration +// Common durations. Day and Week are included because ParseDuration supports +// those units (unlike the standard time package). +// +// To count the number of units in a Duration, divide: +// +// second := model.Second +// fmt.Print(int64(second/model.Millisecond)) // prints 1000 +// +// To convert an integer number of units to a Duration, multiply: +// +// seconds := 10 +// fmt.Print(model.Duration(seconds)*model.Second) // prints 10s +const ( + Nanosecond Duration = 1 + Microsecond = 1000 * Nanosecond + Millisecond = 1000 * Microsecond + Second = 1000 * Millisecond + Minute = 60 * Second + Hour = 60 * Minute + Day = 24 * Hour + Week = 7 * Day +) + +// Milliseconds returns the duration as an integer millisecond count. +// Prometheus stores timestamps in milliseconds; time.Duration already +// covers the other units. +func (d Duration) Milliseconds() int64 { return time.Duration(d).Milliseconds() } + // Set implements pflag/flag.Value. func (d *Duration) Set(s string) error { var err error diff --git a/vendor/github.com/prometheus/common/model/value.go b/vendor/github.com/prometheus/common/model/value.go index 8dffd9c4a5..eed5c9ef3b 100644 --- a/vendor/github.com/prometheus/common/model/value.go +++ b/vendor/github.com/prometheus/common/model/value.go @@ -224,28 +224,6 @@ func (ss SampleStream) MarshalJSON() ([]byte, error) { } } -func (ss *SampleStream) UnmarshalJSON(b []byte) error { - v := struct { - Metric Metric `json:"metric"` - Values []SamplePair `json:"values"` - Histograms []SampleHistogramPair `json:"histograms"` - }{ - Metric: ss.Metric, - Values: ss.Values, - Histograms: ss.Histograms, - } - - if err := json.Unmarshal(b, &v); err != nil { - return err - } - - ss.Metric = v.Metric - ss.Values = v.Values - ss.Histograms = v.Histograms - - return nil -} - // Scalar is a scalar value evaluated at the set timestamp. type Scalar struct { Value SampleValue `json:"value"` diff --git a/vendor/github.com/prometheus/common/model/value_float127.go b/vendor/github.com/prometheus/common/model/value_float127.go new file mode 100644 index 0000000000..821cc1e725 --- /dev/null +++ b/vendor/github.com/prometheus/common/model/value_float127.go @@ -0,0 +1,70 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.27 + +package model + +import ( + "bytes" + "encoding/json/jsontext" + "errors" +) + +var nullBytes = []byte("null") + +func (s *SamplePair) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + if t, err := dec.ReadToken(); err != nil { + return err + } else if t.Kind() == jsontext.KindNull { + return nil + } else if t.Kind() != jsontext.KindBeginArray { + return errors.New("expected [") + } + + // Loop until we see the end of the array (tolerate arrays of any length). + for i := 0; dec.PeekKind() != jsontext.KindEndArray; i++ { + switch i { + case 0: + if v, err := dec.ReadValue(); err != nil { + return err + } else if bytes.Equal(v, nullBytes) { + // Skip null values to match SamplePair#UnmarshalJSON behavior. + } else if err := s.Timestamp.UnmarshalJSON(v); err != nil { + return err + } + case 1: + if v, err := dec.ReadValue(); err != nil { + return err + } else if bytes.Equal(v, nullBytes) { + // Skip null values to match SamplePair#UnmarshalJSON behavior. + } else if err := s.Value.UnmarshalJSON(v); err != nil { + return err + } + default: + // Skip any remaining array items to match UnmarshalJSON behavior. + if err := dec.SkipValue(); err != nil { + return err + } + } + } + + // Read the final end array token. + if t, err := dec.ReadToken(); err != nil { + return err + } else if t.Kind() != jsontext.KindEndArray { + return errors.New("expected ]") + } + + return nil +} diff --git a/vendor/github.com/prometheus/common/model/value_histogram127.go b/vendor/github.com/prometheus/common/model/value_histogram127.go new file mode 100644 index 0000000000..fbcfb3f94f --- /dev/null +++ b/vendor/github.com/prometheus/common/model/value_histogram127.go @@ -0,0 +1,95 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.27 + +package model + +import ( + "bytes" + "encoding/json/jsontext" + "encoding/json/v2" + "errors" +) + +func (s *SampleHistogramPair) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + if t, err := dec.ReadToken(); err != nil { + return err + } else if t.Kind() != jsontext.KindBeginArray { + return errors.New("expected [") + } + + if v, err := dec.ReadValue(); err != nil { + return err + } else if bytes.Equal(v, nullBytes) { + // Skip null values to match SamplePair#UnmarshalJSON behavior. + } else if err := s.Timestamp.UnmarshalJSON(v); err != nil { + return err + } + + if err := json.UnmarshalDecode(dec, &s.Histogram); err != nil { + return err + } else if s.Histogram == nil { + return errors.New("histogram is nil") + } + + if t, err := dec.ReadToken(); err != nil { + return err + } else if t.Kind() != jsontext.KindEndArray { + return errors.New("expected ]") + } + return nil +} + +func (s *HistogramBucket) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + if t, err := dec.ReadToken(); err != nil { + return err + } else if t.Kind() != jsontext.KindBeginArray { + return errors.New("expected [") + } + + if err := json.UnmarshalDecode(dec, &s.Boundaries); err != nil { + return err + } + + if v, err := dec.ReadValue(); err != nil { + return err + } else if bytes.Equal(v, nullBytes) { + // Skip null values to match SamplePair#UnmarshalJSON behavior. + } else if err := s.Lower.UnmarshalJSON(v); err != nil { + return err + } + + if v, err := dec.ReadValue(); err != nil { + return err + } else if bytes.Equal(v, nullBytes) { + // Skip null values to match SamplePair#UnmarshalJSON behavior. + } else if err := s.Upper.UnmarshalJSON(v); err != nil { + return err + } + + if v, err := dec.ReadValue(); err != nil { + return err + } else if bytes.Equal(v, nullBytes) { + // Skip null values to match SamplePair#UnmarshalJSON behavior. + } else if err := s.Count.UnmarshalJSON(v); err != nil { + return err + } + + if t, err := dec.ReadToken(); err != nil { + return err + } else if t.Kind() != jsontext.KindEndArray { + return errors.New("expected ]") + } + return nil +} diff --git a/vendor/github.com/prometheus/procfs/.golangci.yml b/vendor/github.com/prometheus/procfs/.golangci.yml index eac920ba80..23ecd4505b 100644 --- a/vendor/github.com/prometheus/procfs/.golangci.yml +++ b/vendor/github.com/prometheus/procfs/.golangci.yml @@ -34,14 +34,6 @@ linters: capital: true misspell: locale: US - revive: - rules: - - name: var-naming - # TODO(SuperQ): See: https://github.com/prometheus/prometheus/issues/17766 - arguments: - - [] - - [] - - - skip-package-name-checks: true exclusions: presets: - comments diff --git a/vendor/github.com/prometheus/procfs/CONTRIBUTING.md b/vendor/github.com/prometheus/procfs/CONTRIBUTING.md index 853eb9d49b..812a71bb97 100644 --- a/vendor/github.com/prometheus/procfs/CONTRIBUTING.md +++ b/vendor/github.com/prometheus/procfs/CONTRIBUTING.md @@ -96,7 +96,7 @@ The `proc` and `sys` filesystems are pseudo file systems and work a bit differen Many of the files are changing continuously and the data being read can in some cases change between subsequent reads in the same file. Also, most of the files are relatively small (less than a few KBs), and system calls to the `stat` function will often return the wrong size. Therefore, for most files it's recommended to read the -full file in a single operation using an internal utility function called `util.ReadFileNoStat`. +full file in a single operation using an internal utility function called `parsers.ReadFileNoStat`. This function is similar to `os.ReadFile`, but it avoids the system call to `stat` to get the current size of the file. @@ -104,7 +104,7 @@ Note that parsing the file's contents can still be performed one line at a time. the full file, and then using a scanner on the `[]byte` or `string` containing the data. ``` - data, err := util.ReadFileNoStat("/proc/cpuinfo") + data, err := parsers.ReadFileNoStat("/proc/cpuinfo") if err != nil { return err } @@ -113,9 +113,9 @@ the full file, and then using a scanner on the `[]byte` or `string` containing t ``` The `/sys` filesystem contains many very small files which contain only a single numeric or text value. These files -can be read using an internal function called `util.SysReadFile` which is similar to `os.ReadFile` but does +can be read using an internal function called `parsers.SysReadFile` which is similar to `os.ReadFile` but does not bother to check the size of the file before reading. ``` - data, err := util.SysReadFile("/sys/class/power_supply/BAT0/capacity") + data, err := parsers.SysReadFile("/sys/class/power_supply/BAT0/capacity") ``` diff --git a/vendor/github.com/prometheus/procfs/Makefile.common b/vendor/github.com/prometheus/procfs/Makefile.common index a7c5f553e1..cd54cb419c 100644 --- a/vendor/github.com/prometheus/procfs/Makefile.common +++ b/vendor/github.com/prometheus/procfs/Makefile.common @@ -61,7 +61,7 @@ PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_ SKIP_GOLANGCI_LINT := GOLANGCI_LINT := GOLANGCI_LINT_OPTS ?= -GOLANGCI_LINT_VERSION ?= v2.11.4 +GOLANGCI_LINT_VERSION ?= v2.12.2 GOLANGCI_FMT_OPTS ?= # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. # windows isn't included here because of the path separator being different. @@ -165,8 +165,9 @@ common-deps: .PHONY: update-go-deps update-go-deps: @echo ">> updating Go dependencies" + # GOTOOLCHAIN prevents go get from pulling deps that require a newer Go version. @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ - $(GO) get $$m; \ + GOTOOLCHAIN=go$$($(GO) mod edit -json | jq -r .Go) $(GO) get $$m; \ done $(GO) mod tidy @@ -246,6 +247,7 @@ common-docker-repo-name: .PHONY: common-docker $(BUILD_DOCKER_ARCHS) common-docker: $(BUILD_DOCKER_ARCHS) +# DOCKER_ARCHS holds promu architecture names, which are not OCI platform strings. $(BUILD_DOCKER_ARCHS): common-docker-%: @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ dockerfile=$${variant#*:}; \ @@ -254,10 +256,16 @@ $(BUILD_DOCKER_ARCHS): common-docker-%: if [ "$*" = "armv7" ]; then \ distroless_arch="arm"; \ fi; \ + arch="$*"; \ + case "$$arch" in \ + armv*) docker_platform="linux/arm/v$${arch#armv}";; \ + *) docker_platform="linux/$$arch";; \ + esac; \ if [ "$$dockerfile" = "Dockerfile" ]; then \ echo "Building default variant ($$variant_name) for linux-$* using $$dockerfile"; \ docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ -f $$dockerfile \ + --platform "$$docker_platform" \ --build-arg ARCH="$*" \ --build-arg OS="linux" \ --build-arg DISTROLESS_ARCH="$$distroless_arch" \ @@ -271,6 +279,7 @@ $(BUILD_DOCKER_ARCHS): common-docker-%: echo "Building $$variant_name variant for linux-$* using $$dockerfile"; \ docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" \ -f $$dockerfile \ + --platform "$$docker_platform" \ --build-arg ARCH="$*" \ --build-arg OS="linux" \ --build-arg DISTROLESS_ARCH="$$distroless_arch" \ diff --git a/vendor/github.com/prometheus/procfs/cmdline.go b/vendor/github.com/prometheus/procfs/cmdline.go index 4f1cac1f0a..6a2e7bb0ec 100644 --- a/vendor/github.com/prometheus/procfs/cmdline.go +++ b/vendor/github.com/prometheus/procfs/cmdline.go @@ -16,12 +16,12 @@ package procfs import ( "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // CmdLine returns the command line of the kernel. func (fs FS) CmdLine() ([]string, error) { - data, err := util.ReadFileNoStat(fs.proc.Path("cmdline")) + data, err := parsers.ReadFileNoStat(fs.proc.Path("cmdline")) if err != nil { return nil, err } diff --git a/vendor/github.com/prometheus/procfs/cpuinfo.go b/vendor/github.com/prometheus/procfs/cpuinfo.go index 4b23d8d6b5..7a31a4555b 100644 --- a/vendor/github.com/prometheus/procfs/cpuinfo.go +++ b/vendor/github.com/prometheus/procfs/cpuinfo.go @@ -24,7 +24,7 @@ import ( "strconv" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // CPUInfo contains general information about a system CPU found in /proc/cpuinfo. @@ -65,7 +65,7 @@ var ( // CPUInfo returns information about current system CPUs. // See https://www.kernel.org/doc/Documentation/filesystems/proc.txt func (fs FS) CPUInfo() ([]CPUInfo, error) { - data, err := util.ReadFileNoStat(fs.proc.Path("cpuinfo")) + data, err := parsers.ReadFileNoStat(fs.proc.Path("cpuinfo")) if err != nil { return nil, err } diff --git a/vendor/github.com/prometheus/procfs/crypto.go b/vendor/github.com/prometheus/procfs/crypto.go index d93b712e05..c4ba05b15a 100644 --- a/vendor/github.com/prometheus/procfs/crypto.go +++ b/vendor/github.com/prometheus/procfs/crypto.go @@ -20,7 +20,7 @@ import ( "io" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // Crypto holds info parsed from /proc/crypto. @@ -55,7 +55,7 @@ var cryptoFile = "crypto" // https://kernel.readthedocs.io/en/sphinx-samples/crypto-API.html func (fs FS) Crypto() ([]Crypto, error) { path := fs.proc.Path(cryptoFile) - b, err := util.ReadFileNoStat(path) + b, err := parsers.ReadFileNoStat(path) if err != nil { return nil, fmt.Errorf("%w: Cannot read file %v: %w", ErrFileRead, b, err) @@ -112,7 +112,7 @@ func parseCrypto(r io.Reader) ([]Crypto, error) { // parseKV parses a key/value pair into the appropriate field of c. func (c *Crypto) parseKV(k, v string) error { - vp := util.NewValueParser(v) + vp := parsers.NewValueParser(v) switch k { case "async": diff --git a/vendor/github.com/prometheus/procfs/fs_statfs_notype.go b/vendor/github.com/prometheus/procfs/fs_statfs_notype.go index 0bef25bdd9..fc16802dcb 100644 --- a/vendor/github.com/prometheus/procfs/fs_statfs_notype.go +++ b/vendor/github.com/prometheus/procfs/fs_statfs_notype.go @@ -15,8 +15,8 @@ package procfs -// isRealProc returns true on architectures that don't have a Type argument +// isRealProc returns false on architectures that don't have a Type argument // in their Statfs_t struct. func isRealProc(_ string) (bool, error) { - return true, nil + return false, nil } diff --git a/vendor/github.com/prometheus/procfs/fscache.go b/vendor/github.com/prometheus/procfs/fscache.go index 9dde857073..388295cb46 100644 --- a/vendor/github.com/prometheus/procfs/fscache.go +++ b/vendor/github.com/prometheus/procfs/fscache.go @@ -21,7 +21,7 @@ import ( "strconv" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // Fscacheinfo represents fscache statistics. @@ -229,7 +229,7 @@ type Fscacheinfo struct { // Fscacheinfo returns information about current fscache statistics. // See https://www.kernel.org/doc/Documentation/filesystems/caching/fscache.txt func (fs FS) Fscacheinfo() (Fscacheinfo, error) { - b, err := util.ReadFileNoStat(fs.proc.Path("fs/fscache/stats")) + b, err := parsers.ReadFileNoStat(fs.proc.Path("fs/fscache/stats")) if err != nil { return Fscacheinfo{}, err } diff --git a/vendor/github.com/prometheus/procfs/internal/fs/fs.go b/vendor/github.com/prometheus/procfs/internal/fs/fs.go index e7ccad66b2..bdd8010a48 100644 --- a/vendor/github.com/prometheus/procfs/internal/fs/fs.go +++ b/vendor/github.com/prometheus/procfs/internal/fs/fs.go @@ -31,6 +31,9 @@ const ( // DefaultSelinuxMountPoint is the common mount point of the selinuxfs. DefaultSelinuxMountPoint = "/sys/fs/selinux" + + // DefaultResctrlMountPoint is the common mount point of the resctrl filesystem. + DefaultResctrlMountPoint = "/sys/fs/resctrl" ) // FS represents a pseudo-filesystem, normally /proc or /sys, which provides an diff --git a/vendor/github.com/prometheus/procfs/internal/util/parse.go b/vendor/github.com/prometheus/procfs/internal/parsers/parse.go similarity index 99% rename from vendor/github.com/prometheus/procfs/internal/util/parse.go rename to vendor/github.com/prometheus/procfs/internal/parsers/parse.go index 30c5872019..5789ae5db0 100644 --- a/vendor/github.com/prometheus/procfs/internal/util/parse.go +++ b/vendor/github.com/prometheus/procfs/internal/parsers/parse.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package util +package parsers import ( "errors" diff --git a/vendor/github.com/prometheus/procfs/internal/util/readfile.go b/vendor/github.com/prometheus/procfs/internal/parsers/readfile.go similarity index 98% rename from vendor/github.com/prometheus/procfs/internal/util/readfile.go rename to vendor/github.com/prometheus/procfs/internal/parsers/readfile.go index 0e41f71af1..c68d292153 100644 --- a/vendor/github.com/prometheus/procfs/internal/util/readfile.go +++ b/vendor/github.com/prometheus/procfs/internal/parsers/readfile.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package util +package parsers import ( "io" diff --git a/vendor/github.com/prometheus/procfs/internal/util/sysreadfile.go b/vendor/github.com/prometheus/procfs/internal/parsers/sysreadfile.go similarity index 99% rename from vendor/github.com/prometheus/procfs/internal/util/sysreadfile.go rename to vendor/github.com/prometheus/procfs/internal/parsers/sysreadfile.go index f6a4a4de62..0f72f6021c 100644 --- a/vendor/github.com/prometheus/procfs/internal/util/sysreadfile.go +++ b/vendor/github.com/prometheus/procfs/internal/parsers/sysreadfile.go @@ -13,7 +13,7 @@ //go:build (linux || darwin) && !appengine -package util +package parsers import ( "bytes" diff --git a/vendor/github.com/prometheus/procfs/internal/util/sysreadfile_compat.go b/vendor/github.com/prometheus/procfs/internal/parsers/sysreadfile_compat.go similarity index 98% rename from vendor/github.com/prometheus/procfs/internal/util/sysreadfile_compat.go rename to vendor/github.com/prometheus/procfs/internal/parsers/sysreadfile_compat.go index c80e082cb9..6e19c45559 100644 --- a/vendor/github.com/prometheus/procfs/internal/util/sysreadfile_compat.go +++ b/vendor/github.com/prometheus/procfs/internal/parsers/sysreadfile_compat.go @@ -13,7 +13,7 @@ //go:build (linux && appengine) || (!linux && !darwin) -package util +package parsers import ( "fmt" diff --git a/vendor/github.com/prometheus/procfs/internal/util/valueparser.go b/vendor/github.com/prometheus/procfs/internal/parsers/valueparser.go similarity index 99% rename from vendor/github.com/prometheus/procfs/internal/util/valueparser.go rename to vendor/github.com/prometheus/procfs/internal/parsers/valueparser.go index e0ed671ea0..6289fd6704 100644 --- a/vendor/github.com/prometheus/procfs/internal/util/valueparser.go +++ b/vendor/github.com/prometheus/procfs/internal/parsers/valueparser.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package util +package parsers import ( "strconv" diff --git a/vendor/github.com/prometheus/procfs/ipvs.go b/vendor/github.com/prometheus/procfs/ipvs.go index 5374da9fa8..8a6f737478 100644 --- a/vendor/github.com/prometheus/procfs/ipvs.go +++ b/vendor/github.com/prometheus/procfs/ipvs.go @@ -25,7 +25,7 @@ import ( "strconv" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // IPVSStats holds IPVS statistics, as exposed by the kernel in `/proc/net/ip_vs_stats`. @@ -66,7 +66,7 @@ type IPVSBackendStatus struct { // IPVSStats reads the IPVS statistics from the specified `proc` filesystem. func (fs FS) IPVSStats() (IPVSStats, error) { - data, err := util.ReadFileNoStat(fs.proc.Path("net/ip_vs_stats")) + data, err := parsers.ReadFileNoStat(fs.proc.Path("net/ip_vs_stats")) if err != nil { return IPVSStats{}, err } diff --git a/vendor/github.com/prometheus/procfs/kernel_random.go b/vendor/github.com/prometheus/procfs/kernel_random.go index e7c5b8cf2b..cbcd12b436 100644 --- a/vendor/github.com/prometheus/procfs/kernel_random.go +++ b/vendor/github.com/prometheus/procfs/kernel_random.go @@ -18,7 +18,7 @@ package procfs import ( "os" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // KernelRandom contains information about to the kernel's random number generator. @@ -48,7 +48,7 @@ func (fs FS) KernelRandom() (KernelRandom, error) { "write_wakeup_threshold": &random.WriteWakeupThreshold, "read_wakeup_threshold": &random.ReadWakeupThreshold, } { - val, err := util.ReadUintFromFile(fs.proc.Path("sys", "kernel", "random", file)) + val, err := parsers.ReadUintFromFile(fs.proc.Path("sys", "kernel", "random", file)) if os.IsNotExist(err) { continue } diff --git a/vendor/github.com/prometheus/procfs/kernel_tainted.go b/vendor/github.com/prometheus/procfs/kernel_tainted.go new file mode 100644 index 0000000000..9707f02808 --- /dev/null +++ b/vendor/github.com/prometheus/procfs/kernel_tainted.go @@ -0,0 +1,95 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build linux + +package procfs + +import ( + "github.com/prometheus/procfs/internal/parsers" +) + +// KernelTaintBit represents a single kernel taint flag. +type KernelTaintBit struct { + // Index is the zero-based bit position in the tainted bitmask. + Index int + // Flag is the letter code identifying this taint (e.g. "L" for soft lockup). + // See https://www.kernel.org/doc/html/latest/admin-guide/tainted-kernels.html + Flag string + // Description is a human-readable explanation of what this taint means. + Description string + // Set indicates whether this taint flag is currently active. + Set bool +} + +// KernelTainted holds the kernel taint state read from /proc/sys/kernel/tainted. +type KernelTainted struct { + // Value is the raw integer from /proc/sys/kernel/tainted. + Value uint64 + // Bits contains each known taint flag and whether it is currently set. + // Flags are listed in bit-position order (bit 0 first). + Bits []KernelTaintBit +} + +// kernelTaintBitDefs lists all known kernel taint flags in bit-position order. +// Reference: https://www.kernel.org/doc/html/latest/admin-guide/tainted-kernels.html +var kernelTaintBitDefs = []struct { + flag string + description string +}{ + {"P", "Proprietary module was loaded."}, + {"F", "Module was force loaded."}, + {"S", "SMP kernel oops on an officially SMP incapable processor."}, + {"R", "Module was force unloaded."}, + {"M", "Processor reported a Machine Check Exception (MCE)."}, + {"B", "Bad page referenced or some unexpected page flags."}, + {"U", "Taint requested by userspace application."}, + {"D", "Kernel died recently, i.e. there was an OOPS or BUG."}, + {"A", "An ACPI table was overridden by user."}, + {"W", "Kernel issued warning."}, + {"C", "Staging driver was loaded."}, + {"I", "Workaround for bug in platform firmware applied."}, + {"O", "Externally-built (out-of-tree) module was loaded."}, + {"E", "Unsigned module was loaded."}, + {"L", "Soft lockup occurred."}, + {"K", "Kernel has been live patched."}, + {"X", "Auxiliary taint, defined and used by distros."}, + {"T", "The kernel was built with the struct randomization plugin."}, + {"N", "An in-kernel test such as a KUnit test has been run."}, + {"J", "Userspace used a mutating debug operation in fwctl."}, +} + +// KernelTainted reads /proc/sys/kernel/tainted and returns the raw taint value +// along with each known flag parsed into a KernelTainted struct. +// See https://www.kernel.org/doc/html/latest/admin-guide/tainted-kernels.html +func (fs FS) KernelTainted() (KernelTainted, error) { + value, err := parsers.SysReadUintFromFile(fs.proc.Path("sys", "kernel", "tainted")) + if err != nil { + return KernelTainted{}, err + } + return parseTainted(value), nil +} + +// parseTainted builds a KernelTainted from the raw taint bitmask value. +func parseTainted(value uint64) KernelTainted { + bits := make([]KernelTaintBit, len(kernelTaintBitDefs)) + for i, def := range kernelTaintBitDefs { + bits[i] = KernelTaintBit{ + Index: i, + Flag: def.flag, + Description: def.description, + Set: value&(1</environ`. func (p Proc) Environ() ([]string, error) { environments := make([]string, 0) - data, err := util.ReadFileNoStat(p.path("environ")) + data, err := parsers.ReadFileNoStat(p.path("environ")) if err != nil { return environments, err } diff --git a/vendor/github.com/prometheus/procfs/proc_fdinfo.go b/vendor/github.com/prometheus/procfs/proc_fdinfo.go index fa57761dbe..9e3e313e14 100644 --- a/vendor/github.com/prometheus/procfs/proc_fdinfo.go +++ b/vendor/github.com/prometheus/procfs/proc_fdinfo.go @@ -19,7 +19,7 @@ import ( "fmt" "regexp" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) var ( @@ -49,7 +49,7 @@ type ProcFDInfo struct { // FDInfo constructor. On kernels older than 3.8, InotifyInfos will always be empty. func (p Proc) FDInfo(fd string) (*ProcFDInfo, error) { - data, err := util.ReadFileNoStat(p.path("fdinfo", fd)) + data, err := parsers.ReadFileNoStat(p.path("fdinfo", fd)) if err != nil { return nil, err } diff --git a/vendor/github.com/prometheus/procfs/proc_interrupts.go b/vendor/github.com/prometheus/procfs/proc_interrupts.go index 643b500d5d..2a2412c4f8 100644 --- a/vendor/github.com/prometheus/procfs/proc_interrupts.go +++ b/vendor/github.com/prometheus/procfs/proc_interrupts.go @@ -22,7 +22,7 @@ import ( "strconv" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // Interrupt represents a single interrupt line. @@ -42,7 +42,7 @@ type Interrupts map[string]Interrupt // Interrupts creates a new instance from a given Proc instance. func (p Proc) Interrupts() (Interrupts, error) { - data, err := util.ReadFileNoStat(p.fs.proc.Path("interrupts")) + data, err := parsers.ReadFileNoStat(p.fs.proc.Path("interrupts")) if err != nil { return nil, err } diff --git a/vendor/github.com/prometheus/procfs/proc_io.go b/vendor/github.com/prometheus/procfs/proc_io.go index dd8086ba2e..ca221bb656 100644 --- a/vendor/github.com/prometheus/procfs/proc_io.go +++ b/vendor/github.com/prometheus/procfs/proc_io.go @@ -16,7 +16,7 @@ package procfs import ( "fmt" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // ProcIO models the content of /proc//io. @@ -43,7 +43,7 @@ type ProcIO struct { func (p Proc) IO() (ProcIO, error) { pio := ProcIO{} - data, err := util.ReadFileNoStat(p.path("io")) + data, err := parsers.ReadFileNoStat(p.path("io")) if err != nil { return pio, err } diff --git a/vendor/github.com/prometheus/procfs/proc_netstat.go b/vendor/github.com/prometheus/procfs/proc_netstat.go index 7f94cc8914..6a118f58a7 100644 --- a/vendor/github.com/prometheus/procfs/proc_netstat.go +++ b/vendor/github.com/prometheus/procfs/proc_netstat.go @@ -21,7 +21,7 @@ import ( "strconv" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // ProcNetstat models the content of /proc//net/netstat. @@ -146,6 +146,20 @@ type TcpExt struct { // nolint:revive TCPMTUPFail *float64 TCPMTUPSuccess *float64 TCPWqueueTooBig *float64 + + // Legacy TcpExt counters dropped from recent kernels' TcpExt table; + // modeled so consumers retain metric parity with older kernels that + // still print them in /proc/net/netstat. + TCPLoss *float64 + PAWSPassive *float64 + TCPForwardRetrans *float64 + TCPSchedulerFailed *float64 + TCPPrequeued *float64 + TCPDirectCopyFromBacklog *float64 + TCPDirectCopyFromPrequeue *float64 + TCPPrequeueDropped *float64 + TCPFACKReorder *float64 + TCPHPHitsToUser *float64 } type IpExt struct { // nolint:revive @@ -171,7 +185,7 @@ type IpExt struct { // nolint:revive func (p Proc) Netstat() (ProcNetstat, error) { filename := p.path("net/netstat") - data, err := util.ReadFileNoStat(filename) + data, err := parsers.ReadFileNoStat(filename) if err != nil { return ProcNetstat{PID: p.PID}, err } @@ -396,6 +410,26 @@ func parseProcNetstat(r io.Reader, fileName string) (ProcNetstat, error) { procNetstat.TCPMTUPSuccess = &value case "TCPWqueueTooBig": procNetstat.TCPWqueueTooBig = &value + case "TCPLoss": + procNetstat.TCPLoss = &value + case "PAWSPassive": + procNetstat.PAWSPassive = &value + case "TCPForwardRetrans": + procNetstat.TCPForwardRetrans = &value + case "TCPSchedulerFailed": + procNetstat.TCPSchedulerFailed = &value + case "TCPPrequeued": + procNetstat.TCPPrequeued = &value + case "TCPDirectCopyFromBacklog": + procNetstat.TCPDirectCopyFromBacklog = &value + case "TCPDirectCopyFromPrequeue": + procNetstat.TCPDirectCopyFromPrequeue = &value + case "TCPPrequeueDropped": + procNetstat.TCPPrequeueDropped = &value + case "TCPFACKReorder": + procNetstat.TCPFACKReorder = &value + case "TCPHPHitsToUser": + procNetstat.TCPHPHitsToUser = &value } case "IpExt": switch key { diff --git a/vendor/github.com/prometheus/procfs/proc_psi.go b/vendor/github.com/prometheus/procfs/proc_psi.go index cc2c5de873..5f0fbdffb5 100644 --- a/vendor/github.com/prometheus/procfs/proc_psi.go +++ b/vendor/github.com/prometheus/procfs/proc_psi.go @@ -30,7 +30,7 @@ import ( "io" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) const lineFormat = "avg10=%f avg60=%f avg300=%f total=%d" @@ -59,7 +59,7 @@ type PSIStats struct { // resource from /proc/pressure/. At time of writing this can be // either "cpu", "memory" or "io". func (fs FS) PSIStatsForResource(resource string) (PSIStats, error) { - data, err := util.ReadFileNoStat(fs.proc.Path(fmt.Sprintf("%s/%s", "pressure", resource))) + data, err := parsers.ReadFileNoStat(fs.proc.Path(fmt.Sprintf("%s/%s", "pressure", resource))) if err != nil { return PSIStats{}, fmt.Errorf("%w: psi_stats: unavailable for %q: %w", ErrFileRead, resource, err) } diff --git a/vendor/github.com/prometheus/procfs/proc_smaps.go b/vendor/github.com/prometheus/procfs/proc_smaps.go index f637309b3d..dff5cfb885 100644 --- a/vendor/github.com/prometheus/procfs/proc_smaps.go +++ b/vendor/github.com/prometheus/procfs/proc_smaps.go @@ -23,7 +23,7 @@ import ( "strconv" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) var ( @@ -60,7 +60,7 @@ type ProcSMapsRollup struct { // If smaps_rollup does not exists (require kernel >= 4.15), the content of /proc/pid/smaps will // we read and summed. func (p Proc) ProcSMapsRollup() (ProcSMapsRollup, error) { - data, err := util.ReadFileNoStat(p.path("smaps_rollup")) + data, err := parsers.ReadFileNoStat(p.path("smaps_rollup")) if err != nil && os.IsNotExist(err) { return p.procSMapsRollupManual() } diff --git a/vendor/github.com/prometheus/procfs/proc_snmp.go b/vendor/github.com/prometheus/procfs/proc_snmp.go index 8d9a9bcd67..531bc8371c 100644 --- a/vendor/github.com/prometheus/procfs/proc_snmp.go +++ b/vendor/github.com/prometheus/procfs/proc_snmp.go @@ -21,7 +21,7 @@ import ( "strconv" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // ProcSnmp models the content of /proc//net/snmp. @@ -135,7 +135,7 @@ type UdpLite struct { // nolint:revive func (p Proc) Snmp() (ProcSnmp, error) { filename := p.path("net/snmp") - data, err := util.ReadFileNoStat(filename) + data, err := parsers.ReadFileNoStat(filename) if err != nil { return ProcSnmp{PID: p.PID}, err } diff --git a/vendor/github.com/prometheus/procfs/proc_snmp6.go b/vendor/github.com/prometheus/procfs/proc_snmp6.go index 841fef4649..8296a4aed1 100644 --- a/vendor/github.com/prometheus/procfs/proc_snmp6.go +++ b/vendor/github.com/prometheus/procfs/proc_snmp6.go @@ -22,7 +22,7 @@ import ( "strconv" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // ProcSnmp6 models the content of /proc//net/snmp6. @@ -140,7 +140,7 @@ type UdpLite6 struct { // nolint:revive func (p Proc) Snmp6() (ProcSnmp6, error) { filename := p.path("net/snmp6") - data, err := util.ReadFileNoStat(filename) + data, err := parsers.ReadFileNoStat(filename) if err != nil { // On systems with IPv6 disabled, this file won't exist. // Do nothing. diff --git a/vendor/github.com/prometheus/procfs/proc_stat.go b/vendor/github.com/prometheus/procfs/proc_stat.go index 02e3f9e316..d9e544bcc8 100644 --- a/vendor/github.com/prometheus/procfs/proc_stat.go +++ b/vendor/github.com/prometheus/procfs/proc_stat.go @@ -18,7 +18,7 @@ import ( "fmt" "os" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // Originally, this USER_HZ value was dynamically retrieved via a sysconf call @@ -134,7 +134,7 @@ func (p Proc) NewStat() (ProcStat, error) { // Stat returns the current status information of the process. func (p Proc) Stat() (ProcStat, error) { - data, err := util.ReadFileNoStat(p.path("stat")) + data, err := parsers.ReadFileNoStat(p.path("stat")) if err != nil { return ProcStat{}, err } diff --git a/vendor/github.com/prometheus/procfs/proc_statm.go b/vendor/github.com/prometheus/procfs/proc_statm.go index 6bcc97ec9c..5fbe27965f 100644 --- a/vendor/github.com/prometheus/procfs/proc_statm.go +++ b/vendor/github.com/prometheus/procfs/proc_statm.go @@ -18,7 +18,7 @@ import ( "strconv" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // - https://man7.org/linux/man-pages/man5/proc_pid_statm.5.html @@ -53,7 +53,7 @@ func (p Proc) NewStatm() (ProcStatm, error) { // Statm returns the current memory usage information of the process. func (p Proc) Statm() (ProcStatm, error) { - data, err := util.ReadFileNoStat(p.path("statm")) + data, err := parsers.ReadFileNoStat(p.path("statm")) if err != nil { return ProcStatm{}, err } diff --git a/vendor/github.com/prometheus/procfs/proc_status.go b/vendor/github.com/prometheus/procfs/proc_status.go index 12d65581c8..8c1652e172 100644 --- a/vendor/github.com/prometheus/procfs/proc_status.go +++ b/vendor/github.com/prometheus/procfs/proc_status.go @@ -20,7 +20,7 @@ import ( "strconv" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // ProcStatus provides status information about the process, @@ -100,7 +100,7 @@ type ProcStatus struct { // NewStatus returns the current status information of the process. func (p Proc) NewStatus() (ProcStatus, error) { - data, err := util.ReadFileNoStat(p.path("status")) + data, err := parsers.ReadFileNoStat(p.path("status")) if err != nil { return ProcStatus{}, err } diff --git a/vendor/github.com/prometheus/procfs/proc_sys.go b/vendor/github.com/prometheus/procfs/proc_sys.go index 52658a4d52..c465b6be02 100644 --- a/vendor/github.com/prometheus/procfs/proc_sys.go +++ b/vendor/github.com/prometheus/procfs/proc_sys.go @@ -17,7 +17,7 @@ import ( "fmt" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) func sysctlToPath(sysctl string) string { @@ -25,7 +25,7 @@ func sysctlToPath(sysctl string) string { } func (fs FS) SysctlStrings(sysctl string) ([]string, error) { - value, err := util.SysReadFile(fs.proc.Path("sys", sysctlToPath(sysctl))) + value, err := parsers.SysReadFile(fs.proc.Path("sys", sysctlToPath(sysctl))) if err != nil { return nil, err } @@ -41,7 +41,7 @@ func (fs FS) SysctlInts(sysctl string) ([]int, error) { values := make([]int, len(fields)) for i, f := range fields { - vp := util.NewValueParser(f) + vp := parsers.NewValueParser(f) values[i] = vp.Int() if err := vp.Err(); err != nil { return nil, fmt.Errorf("%w: field %d in sysctl %s is not a valid int: %w", ErrFileParse, i, sysctl, err) diff --git a/vendor/github.com/prometheus/procfs/slab.go b/vendor/github.com/prometheus/procfs/slab.go index 32a04678ad..b78bac6344 100644 --- a/vendor/github.com/prometheus/procfs/slab.go +++ b/vendor/github.com/prometheus/procfs/slab.go @@ -21,7 +21,7 @@ import ( "strconv" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) var ( @@ -142,7 +142,7 @@ func (fs FS) SlabInfo() (SlabInfo, error) { // TODO: Consider passing options to allow for parsing different // slabinfo versions. However, slabinfo 2.1 has been stable since // kernel 2.6.10 and later. - data, err := util.ReadFileNoStat(fs.proc.Path("slabinfo")) + data, err := parsers.ReadFileNoStat(fs.proc.Path("slabinfo")) if err != nil { return SlabInfo{}, err } diff --git a/vendor/github.com/prometheus/procfs/softirqs.go b/vendor/github.com/prometheus/procfs/softirqs.go index 47b73a7297..dcd5859335 100644 --- a/vendor/github.com/prometheus/procfs/softirqs.go +++ b/vendor/github.com/prometheus/procfs/softirqs.go @@ -21,7 +21,7 @@ import ( "strconv" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // Softirqs represents the softirq statistics. @@ -40,7 +40,7 @@ type Softirqs struct { func (fs FS) Softirqs() (Softirqs, error) { fileName := fs.proc.Path("softirqs") - data, err := util.ReadFileNoStat(fileName) + data, err := parsers.ReadFileNoStat(fileName) if err != nil { return Softirqs{}, err } diff --git a/vendor/github.com/prometheus/procfs/stat.go b/vendor/github.com/prometheus/procfs/stat.go index 593ad0f62f..781bd28ada 100644 --- a/vendor/github.com/prometheus/procfs/stat.go +++ b/vendor/github.com/prometheus/procfs/stat.go @@ -23,7 +23,7 @@ import ( "strings" "github.com/prometheus/procfs/internal/fs" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // CPUStat shows how much time the cpu spend in various stages. @@ -167,7 +167,7 @@ func (fs FS) NewStat() (Stat, error) { // See: https://www.kernel.org/doc/Documentation/filesystems/proc.txt func (fs FS) Stat() (Stat, error) { fileName := fs.proc.Path("stat") - data, err := util.ReadFileNoStat(fileName) + data, err := parsers.ReadFileNoStat(fileName) if err != nil { return Stat{}, err } diff --git a/vendor/github.com/prometheus/procfs/swaps.go b/vendor/github.com/prometheus/procfs/swaps.go index ee17bf4888..8f9bbfb5c4 100644 --- a/vendor/github.com/prometheus/procfs/swaps.go +++ b/vendor/github.com/prometheus/procfs/swaps.go @@ -20,7 +20,7 @@ import ( "strconv" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // Swap represents an entry in /proc/swaps. @@ -34,7 +34,7 @@ type Swap struct { // Swaps returns a slice of all configured swap devices on the system. func (fs FS) Swaps() ([]*Swap, error) { - data, err := util.ReadFileNoStat(fs.proc.Path("swaps")) + data, err := parsers.ReadFileNoStat(fs.proc.Path("swaps")) if err != nil { return nil, err } diff --git a/vendor/github.com/prometheus/procfs/vm.go b/vendor/github.com/prometheus/procfs/vm.go index 52180c03e2..62725b4207 100644 --- a/vendor/github.com/prometheus/procfs/vm.go +++ b/vendor/github.com/prometheus/procfs/vm.go @@ -21,7 +21,7 @@ import ( "path/filepath" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // The VM interface is described at @@ -102,11 +102,11 @@ func (fs FS) VM() (*VM, error) { name := filepath.Join(path, f.Name()) // ignore errors on read, as there are some write only // in /proc/sys/vm - value, err := util.SysReadFile(name) + value, err := parsers.SysReadFile(name) if err != nil { continue } - vp := util.NewValueParser(value) + vp := parsers.NewValueParser(value) switch f.Name() { case "admin_reserve_kbytes": @@ -143,7 +143,7 @@ func (fs FS) VM() (*VM, error) { stringSlice := strings.Fields(value) pint64Slice := make([]*int64, 0, len(stringSlice)) for _, value := range stringSlice { - vp := util.NewValueParser(value) + vp := parsers.NewValueParser(value) pint64Slice = append(pint64Slice, vp.PInt64()) } vm.LowmemReserveRatio = pint64Slice diff --git a/vendor/github.com/prometheus/procfs/zoneinfo.go b/vendor/github.com/prometheus/procfs/zoneinfo.go index 63d1898bc8..71b3c2b653 100644 --- a/vendor/github.com/prometheus/procfs/zoneinfo.go +++ b/vendor/github.com/prometheus/procfs/zoneinfo.go @@ -22,7 +22,7 @@ import ( "regexp" "strings" - "github.com/prometheus/procfs/internal/util" + "github.com/prometheus/procfs/internal/parsers" ) // Zoneinfo holds info parsed from /proc/zoneinfo. @@ -103,7 +103,7 @@ func parseZoneinfo(zoneinfoData []byte) ([]Zoneinfo, error) { if len(parts) < 2 { continue } - vp := util.NewValueParser(parts[1]) + vp := parsers.NewValueParser(parts[1]) switch parts[0] { case "nr_free_pages": zoneinfoElement.NrFreePages = vp.PInt64() @@ -179,7 +179,7 @@ func parseZoneinfo(zoneinfoData []byte) ([]Zoneinfo, error) { protectionValues = strings.Replace(protectionValues, ")", "", 1) protectionValues = strings.TrimSpace(protectionValues) protectionStringMap := strings.Split(protectionValues, ", ") - val, err := util.ParsePInt64s(protectionStringMap) + val, err := parsers.ParsePInt64s(protectionStringMap) if err == nil { zoneinfoElement.Protection = val } diff --git a/vendor/modules.txt b/vendor/modules.txt index c29ccd4fed..ad6f2d9ad0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -867,8 +867,8 @@ github.com/kovidgoyal/go-parallel # github.com/kovidgoyal/go-shm v1.0.0 ## explicit; go 1.24.0 github.com/kovidgoyal/go-shm -# github.com/kovidgoyal/imaging v1.8.23 -## explicit; go 1.25.0 +# github.com/kovidgoyal/imaging v1.9.0 +## explicit; go 1.26.0 github.com/kovidgoyal/imaging github.com/kovidgoyal/imaging/apng github.com/kovidgoyal/imaging/colorconv @@ -1159,7 +1159,7 @@ github.com/nats-io/nats-server/v2/server/stree github.com/nats-io/nats-server/v2/server/sysmem github.com/nats-io/nats-server/v2/server/thw github.com/nats-io/nats-server/v2/server/tpm -# github.com/nats-io/nats.go v1.54.0 +# github.com/nats-io/nats.go v1.55.0 ## explicit; go 1.26.0 github.com/nats-io/nats.go github.com/nats-io/nats.go/encoders/builtin @@ -1795,8 +1795,8 @@ github.com/power-devops/perfstat ## explicit; go 1.16 github.com/pquerna/cachecontrol github.com/pquerna/cachecontrol/cacheobject -# github.com/prometheus/client_golang v1.24.1 -## explicit; go 1.25.0 +# github.com/prometheus/client_golang v1.25.0 +## explicit; go 1.26.0 github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header github.com/prometheus/client_golang/prometheus @@ -1807,15 +1807,15 @@ github.com/prometheus/client_golang/prometheus/promhttp/internal # github.com/prometheus/client_model v0.6.3 ## explicit; go 1.23 github.com/prometheus/client_model/go -# github.com/prometheus/common v0.70.1 -## explicit; go 1.25.0 +# github.com/prometheus/common v0.72.0 +## explicit; go 1.26.0 github.com/prometheus/common/expfmt github.com/prometheus/common/model -# github.com/prometheus/procfs v0.21.1 +# github.com/prometheus/procfs v0.22.0 ## explicit; go 1.25.0 github.com/prometheus/procfs github.com/prometheus/procfs/internal/fs -github.com/prometheus/procfs/internal/util +github.com/prometheus/procfs/internal/parsers # github.com/prometheus/statsd_exporter v0.22.8 ## explicit; go 1.17 github.com/prometheus/statsd_exporter/pkg/level