mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-11-08 21:43:00 -05:00
Merge branch 'origin/main' into 'next-release/main'
This commit is contained in:
commit
17e9e68efc
129 files changed
+3393
-1080
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
Whitespace-only changes.
+9
@@ -0,0 +1,9 @@
|
||||
//go:build js
|
||||
|
||||
package magick
|
||||
|
||||
import "os"
|
||||
|
||||
func get_temp_dir() string {
|
||||
return os.TempDir()
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
//go:build !windows
|
||||
//go:build !windows && !js
|
||||
|
||||
package magick
|
||||
|
||||
|
||||
+34
-8
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -5,7 +5,7 @@ import os
|
||||
import subprocess
|
||||
|
||||
|
||||
VERSION = "1.8.23"
|
||||
VERSION = "1.9.0"
|
||||
|
||||
|
||||
def run(*args: str):
|
||||
|
||||
+3
-5
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
vendor/github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header/header.go
Generated
Vendored
+1
-1
@@ -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
|
||||
|
||||
+2
-1
@@ -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
@@ -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
@@ -12,7 +12,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !js || wasm
|
||||
// +build !js wasm
|
||||
|
||||
package prometheus
|
||||
|
||||
|
||||
-1
@@ -12,7 +12,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build js && !wasm
|
||||
// +build js,!wasm
|
||||
|
||||
package prometheus
|
||||
|
||||
|
||||
+10
-5
@@ -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()},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-8
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Generated
Vendored
-3
@@ -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
@@ -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
@@ -12,7 +12,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !js || wasm
|
||||
// +build !js wasm
|
||||
|
||||
package prometheus
|
||||
|
||||
|
||||
-1
@@ -12,7 +12,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build js && !wasm
|
||||
// +build js,!wasm
|
||||
|
||||
package prometheus
|
||||
|
||||
|
||||
Generated
Vendored
+14
-13
@@ -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) {
|
||||
|
||||
Generated
Vendored
-1
@@ -12,7 +12,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build wasip1 || js || ios
|
||||
// +build wasip1 js ios
|
||||
|
||||
package prometheus
|
||||
|
||||
|
||||
Generated
Vendored
-1
@@ -12,7 +12,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !windows && !js && !wasip1 && !darwin
|
||||
// +build !windows,!js,!wasip1,!darwin
|
||||
|
||||
package prometheus
|
||||
|
||||
|
||||
+31
-7
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
+1221
File diff suppressed because it is too large.
Load diff
+4
-4
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
Generated
Vendored
+1
-1
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"io"
|
||||
Generated
Vendored
+1
-1
@@ -13,7 +13,7 @@
|
||||
|
||||
//go:build (linux || darwin) && !appengine
|
||||
|
||||
package util
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
Generated
Vendored
+1
-1
@@ -13,7 +13,7 @@
|
||||
|
||||
//go:build (linux && appengine) || (!linux && !darwin)
|
||||
|
||||
package util
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
Generated
Vendored
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in new issue
Block a user