Merge branch 'origin/main' into 'next-release/main'

This commit is contained in:
oauth committed 2026-11-05 16:24:15 +00:00
commit 17e9e68efc
129 files changed
+3393 -1080

No files matched your search

+5 -5
View File
@@ -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
+10 -10
View File
@@ -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=
+8 -4
View File
@@ -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++ {
+209 -4
View File
@@ -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
}
+7
View File
@@ -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
+56 -10
View File
@@ -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) {
View File
Whitespace-only changes.
+9
View File
@@ -0,0 +1,9 @@
//go:build js
package magick
import "os"
func get_temp_dir() string {
return os.TempDir()
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !windows
//go:build !windows && !js
package magick
+34 -8
View File
@@ -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 {
+90
View File
@@ -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
}
+3
View File
@@ -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
+7
View File
@@ -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}}
+73 -48
View File
@@ -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
}
+1 -1
View File
@@ -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)
}
}
+8 -1
View File
@@ -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 {
+11 -3
View File
@@ -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
+13 -11
View File
@@ -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
+3 -2
View File
@@ -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
-17
View File
@@ -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),
+1 -1
View File
@@ -5,7 +5,7 @@ import os
import subprocess
VERSION = "1.8.23"
VERSION = "1.9.0"
def run(*args: str):
+3 -5
View File
@@ -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:<port>
# 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)"
+1 -1
View File
@@ -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
+4 -1
View File
@@ -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()
+9 -11
View File
@@ -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 |
+6 -5
View File
@@ -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 -}}
+1 -1
View File
@@ -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
)
+2 -2
View File
@@ -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=
+19 -26
View File
@@ -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
+19 -12
View File
@@ -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
}
+7 -2
View File
@@ -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}}
+16 -9
View File
@@ -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
+21
View File
@@ -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.
+1
View File
@@ -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):]
+12 -6
View File
@@ -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
+4 -4
View File
@@ -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
}
+119 -161
View File
@@ -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 {
+168 -156
View File
@@ -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()
+110 -57
View File
@@ -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 {
+21 -10
View File
@@ -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
+7 -4
View File
@@ -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)
+44
View File
@@ -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
+16 -7
View File
@@ -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
+5 -1
View File
@@ -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}
+209 -102
View File
@@ -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) {
+4 -4
View File
@@ -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
}
@@ -39,7 +39,7 @@ func init() {
// token = 1*<any CHAR except CTLs or separators>
// qdtext = <any TEXT except <">>
for c := 0; c < 256; c++ {
for c := range 256 {
var t octetType
isCtl := c <= 31 || c == 127
isChar := 0 <= c && c <= 127
@@ -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
}
+2 -3
View File
@@ -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))
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !js || wasm
// +build !js wasm
package prometheus
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build js && !wasm
// +build js,!wasm
package prometheus
+10 -5
View File
@@ -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()},
),
}
}
@@ -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
}
+21 -21
View File
@@ -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)
}
}
@@ -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 (
+3 -4
View File
@@ -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)
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !js || wasm
// +build !js wasm
package prometheus
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build js && !wasm
// +build js,!wasm
package prometheus
@@ -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) {
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build wasip1 || js || ios
// +build wasip1 js ios
package prometheus
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !windows && !js && !wasip1 && !darwin
// +build !windows,!js,!wasip1,!darwin
package prometheus
+31 -7
View File
@@ -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
+1 -1
View File
@@ -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{},
}
}
+13 -11
View File
@@ -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) {
+10 -11
View File
@@ -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),
})
}
+8 -9
View File
@@ -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 {
+2 -3
View File
@@ -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))
+13 -6
View File
@@ -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}
}
+135 -49
View File
@@ -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...)
+64 -48
View File
@@ -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 = `<unknown>`
// 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 = `<unknown>`
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 {
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -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
}
+5 -5
View File
@@ -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 {
+28
View File
@@ -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
-22
View File
@@ -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"`
+70
View File
@@ -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
}
+95
View File
@@ -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
}
-8
View File
@@ -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
+4 -4
View File
@@ -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")
```
+11 -2
View File
@@ -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" \
+2 -2
View File
@@ -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
}
+2 -2
View File
@@ -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
}
+3 -3
View File
@@ -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":
+2 -2
View File
@@ -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
}
+2 -2
View File
@@ -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
}
+3
View File
@@ -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
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package util
package parsers
import (
"errors"
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package util
package parsers
import (
"io"
@@ -13,7 +13,7 @@
//go:build (linux || darwin) && !appengine
package util
package parsers
import (
"bytes"
@@ -13,7 +13,7 @@
//go:build (linux && appengine) || (!linux && !darwin)
package util
package parsers
import (
"fmt"
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package util
package parsers
import (
"strconv"
+2 -2
View File
@@ -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
}
+2 -2
View File
@@ -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
}
+95
View File
@@ -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<<uint(i)) != 0,
}
}
return KernelTainted{Value: value, Bits: bits}
}
+2 -2
View File
@@ -18,7 +18,7 @@ import (
"strconv"
"strings"
"github.com/prometheus/procfs/internal/util"
"github.com/prometheus/procfs/internal/parsers"
)
// LoadAvg represents an entry in /proc/loadavg.
@@ -32,7 +32,7 @@ type LoadAvg struct {
func (fs FS) LoadAvg() (*LoadAvg, error) {
path := fs.proc.Path("loadavg")
data, err := util.ReadFileNoStat(path)
data, err := parsers.ReadFileNoStat(path)
if err != nil {
return nil, err
}
+1 -1
View File
@@ -27,7 +27,7 @@ var (
recoveryLinePctRE = regexp.MustCompile(`= (.+)%`)
recoveryLineFinishRE = regexp.MustCompile(`finish=(.+)min`)
recoveryLineSpeedRE = regexp.MustCompile(`speed=(.+)[A-Z]`)
componentDeviceRE = regexp.MustCompile(`(.*)\[(\d+)\](\([SF]+\))?`)
componentDeviceRE = regexp.MustCompile(`(.*)\[(\d+)\]((?:\([FJRSW]\))+)?`)
personalitiesPrefix = "Personalities : "
)
+2 -2
View File
@@ -21,7 +21,7 @@ import (
"strconv"
"strings"
"github.com/prometheus/procfs/internal/util"
"github.com/prometheus/procfs/internal/parsers"
)
// Meminfo represents memory statistics.
@@ -210,7 +210,7 @@ type Meminfo struct {
// Meminfo returns an information about current kernel/system memory statistics.
// See https://www.kernel.org/doc/Documentation/filesystems/proc.txt
func (fs FS) Meminfo() (Meminfo, error) {
b, err := util.ReadFileNoStat(fs.proc.Path("meminfo"))
b, err := parsers.ReadFileNoStat(fs.proc.Path("meminfo"))
if err != nil {
return Meminfo{}, err
}
+3 -3
View File
@@ -98,11 +98,11 @@ func parseMountInfoString(mountString string) (*MountInfo, error) {
mount.MountID, err = strconv.Atoi(mountInfo[0])
if err != nil {
return nil, fmt.Errorf("%w: mount ID: %q", ErrFileParse, mount.MountID)
return nil, fmt.Errorf("%w: mount ID: %q", ErrFileParse, mountInfo[0])
}
mount.ParentID, err = strconv.Atoi(mountInfo[1])
if err != nil {
return nil, fmt.Errorf("%w: parent ID: %q", ErrFileParse, mount.ParentID)
return nil, fmt.Errorf("%w: parent ID: %q", ErrFileParse, mountInfo[1])
}
// Has optional fields, which is a space separated list of values.
// Example: shared:2 master:7
@@ -160,7 +160,7 @@ func mountOptionsParser(mountOptions string) map[string]string {
return opts
}
// readMountInfo reads a full mountinfo file (no 1 MiB cap, unlike util.ReadFileNoStat).
// readMountInfo reads a full mountinfo file (no 1 MiB cap, unlike parsers.ReadFileNoStat).
func readMountInfo(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
+3 -3
View File
@@ -20,7 +20,7 @@ import (
"io"
"strings"
"github.com/prometheus/procfs/internal/util"
"github.com/prometheus/procfs/internal/parsers"
)
// A ConntrackStatEntry represents one line from net/stat/nf_conntrack
@@ -49,7 +49,7 @@ func (fs FS) ConntrackStat() ([]ConntrackStatEntry, error) {
// Parses a slice of ConntrackStatEntries from the given filepath.
func readConntrackStat(path string) ([]ConntrackStatEntry, error) {
// This file is small and can be read with one syscall.
b, err := util.ReadFileNoStat(path)
b, err := parsers.ReadFileNoStat(path)
if err != nil {
// Do not wrap this error so the caller can detect os.IsNotExist and
// similar conditions.
@@ -84,7 +84,7 @@ func parseConntrackStat(r io.Reader) ([]ConntrackStatEntry, error) {
// Parses a ConntrackStatEntry from given array of fields.
func parseConntrackStatEntry(fields []string) (*ConntrackStatEntry, error) {
entries, err := util.ParseHexUint64s(fields)
entries, err := parsers.ParseHexUint64s(fields)
if err != nil {
return nil, fmt.Errorf("%w: Cannot parse entry: %d: %w", ErrFileParse, entries, err)
}
+9 -9
View File
@@ -178,7 +178,7 @@ func parseNetIPSocketLine(fields []string, isUDP bool) (*netIPSocketLine, error)
}
if line.Sl, err = strconv.ParseUint(s[0], 0, 64); err != nil {
return nil, fmt.Errorf("%w: Unable to parse sl field in %q: %w", ErrFileParse, line.Sl, err)
return nil, fmt.Errorf("%w: Unable to parse sl field in %q: %w", ErrFileParse, s[0], err)
}
// local_address
l := strings.Split(fields[1], ":")
@@ -189,7 +189,7 @@ func parseNetIPSocketLine(fields []string, isUDP bool) (*netIPSocketLine, error)
return nil, err
}
if line.LocalPort, err = strconv.ParseUint(l[1], 16, 64); err != nil {
return nil, fmt.Errorf("%w: Unable to parse local_address port value line %q: %w", ErrFileParse, line.LocalPort, err)
return nil, fmt.Errorf("%w: Unable to parse local_address port value line %q: %w", ErrFileParse, l[1], err)
}
// remote_address
@@ -201,12 +201,12 @@ func parseNetIPSocketLine(fields []string, isUDP bool) (*netIPSocketLine, error)
return nil, err
}
if line.RemPort, err = strconv.ParseUint(r[1], 16, 64); err != nil {
return nil, fmt.Errorf("%w: Cannot parse rem_address port value in %q: %w", ErrFileParse, line.RemPort, err)
return nil, fmt.Errorf("%w: Cannot parse rem_address port value in %q: %w", ErrFileParse, r[1], err)
}
// st
if line.St, err = strconv.ParseUint(fields[3], 16, 64); err != nil {
return nil, fmt.Errorf("%w: Cannot parse st value in %q: %w", ErrFileParse, line.St, err)
return nil, fmt.Errorf("%w: Cannot parse st value in %q: %w", ErrFileParse, fields[3], err)
}
// tx_queue and rx_queue
@@ -219,27 +219,27 @@ func parseNetIPSocketLine(fields []string, isUDP bool) (*netIPSocketLine, error)
)
}
if line.TxQueue, err = strconv.ParseUint(q[0], 16, 64); err != nil {
return nil, fmt.Errorf("%w: Cannot parse tx_queue value in %q: %w", ErrFileParse, line.TxQueue, err)
return nil, fmt.Errorf("%w: Cannot parse tx_queue value in %q: %w", ErrFileParse, q[0], err)
}
if line.RxQueue, err = strconv.ParseUint(q[1], 16, 64); err != nil {
return nil, fmt.Errorf("%w: Cannot parse trx_queue value in %q: %w", ErrFileParse, line.RxQueue, err)
return nil, fmt.Errorf("%w: Cannot parse trx_queue value in %q: %w", ErrFileParse, q[1], err)
}
// uid
if line.UID, err = strconv.ParseUint(fields[7], 0, 64); err != nil {
return nil, fmt.Errorf("%w: Cannot parse UID value in %q: %w", ErrFileParse, line.UID, err)
return nil, fmt.Errorf("%w: Cannot parse UID value in %q: %w", ErrFileParse, fields[7], err)
}
// inode
if line.Inode, err = strconv.ParseUint(fields[9], 0, 64); err != nil {
return nil, fmt.Errorf("%w: Cannot parse inode value in %q: %w", ErrFileParse, line.Inode, err)
return nil, fmt.Errorf("%w: Cannot parse inode value in %q: %w", ErrFileParse, fields[9], err)
}
// drops
if isUDP {
drops, err := strconv.ParseUint(fields[12], 0, 64)
if err != nil {
return nil, fmt.Errorf("%w: Cannot parse drops value in %q: %w", ErrFileParse, drops, err)
return nil, fmt.Errorf("%w: Cannot parse drops value in %q: %w", ErrFileParse, fields[12], err)
}
line.Drops = &drops
}
Loaded 100 of 129 files, more files were not shown because too many files have changed in this diff. Show more