From a75726e4eb2d1dbefd7f753709c33bb71818fadf Mon Sep 17 00:00:00 2001 From: "Nicola Asuni (Vonage)" <113166816+nicolaasuni-vonage@users.noreply.github.com> Date: Fri, 1 Mar 2024 14:30:37 +0000 Subject: [PATCH] DNS Cache (#221) * Add httpclient option to set custom DialContext * Add tsmap Delete and GetOK functions * New dnscache package * Bump Version --- VERSION | 2 +- examples/service/go.mod | 14 +- examples/service/go.sum | 30 ++- go.mod | 14 +- go.sum | 30 ++- pkg/dnscache/dnscache.go | 173 +++++++++++++ pkg/dnscache/dnscache_bechmark_test.go | 56 ++++ pkg/dnscache/dnscache_test.go | 286 +++++++++++++++++++++ pkg/httpclient/client_test.go | 4 +- pkg/httpclient/options.go | 17 ++ pkg/httpclient/options_test.go | 15 ++ pkg/threadsafe/tsmap/example_tsmap_test.go | 29 +++ pkg/threadsafe/tsmap/tsmap.go | 20 ++ pkg/threadsafe/tsmap/tsmap_test.go | 35 +++ 14 files changed, 679 insertions(+), 46 deletions(-) create mode 100644 pkg/dnscache/dnscache.go create mode 100644 pkg/dnscache/dnscache_bechmark_test.go create mode 100644 pkg/dnscache/dnscache_test.go diff --git a/VERSION b/VERSION index 32b8efa4..71fae54f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.81.6 +1.82.0 diff --git a/examples/service/go.mod b/examples/service/go.mod index 78bca1bf..1702150d 100644 --- a/examples/service/go.mod +++ b/examples/service/go.mod @@ -5,14 +5,14 @@ go 1.22 replace github.com/Vonage/gosrvlib => ../.. require ( - github.com/Vonage/gosrvlib v1.81.6 + github.com/Vonage/gosrvlib v1.82.0 github.com/golang/mock v1.6.0 github.com/jstemmer/go-junit-report v1.0.0 github.com/prometheus/client_golang v1.19.0 github.com/rakyll/gotest v0.0.6 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 ) @@ -44,7 +44,7 @@ require ( github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.2 // indirect - github.com/hashicorp/consul/api v1.27.0 // indirect + github.com/hashicorp/consul/api v1.28.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.2 // indirect @@ -73,7 +73,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.0 // indirect - github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/common v0.49.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/sagikazarmark/crypt v0.18.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect @@ -106,9 +106,9 @@ require ( golang.org/x/tools v0.18.0 // indirect google.golang.org/api v0.167.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto v0.0.0-20240228224816-df926f6c8641 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 // indirect google.golang.org/grpc v1.62.0 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/examples/service/go.sum b/examples/service/go.sum index e531ecf7..9f7b3a35 100644 --- a/examples/service/go.sum +++ b/examples/service/go.sum @@ -445,10 +445,10 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92Bcuy github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.27.0 h1:gmJ6DPKQog1426xsdmgk5iqDyoRiNc+ipBdJOqKQFjc= -github.com/hashicorp/consul/api v1.27.0/go.mod h1:JkekNRSou9lANFdt+4IKx3Za7XY0JzzpQjEb4Ivo1c8= -github.com/hashicorp/consul/sdk v0.15.1 h1:kKIGxc7CZtflcF5DLfHeq7rOQmRq3vk7kwISN9bif8Q= -github.com/hashicorp/consul/sdk v0.15.1/go.mod h1:7pxqqhqoaPqnBnzXD1StKed62LqJeClzVsUEy85Zr0A= +github.com/hashicorp/consul/api v1.28.2 h1:mXfkRHrpHN4YY3RqL09nXU1eHKLNiuAN4kHvDQ16k/8= +github.com/hashicorp/consul/api v1.28.2/go.mod h1:KyzqzgMEya+IZPcD65YFoOVAgPpbfERu4I/tzG6/ueE= +github.com/hashicorp/consul/sdk v0.16.0 h1:SE9m0W6DEfgIVCJX7xU+iv/hUl4m/nxqMTnCdMxDpJ8= +github.com/hashicorp/consul/sdk v0.16.0/go.mod h1:7pxqqhqoaPqnBnzXD1StKed62LqJeClzVsUEy85Zr0A= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -615,8 +615,8 @@ github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOA github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/common v0.49.0 h1:ToNTdK4zSnPVJmh698mGFkDor9wBI/iGaJy5dbH1EgI= +github.com/prometheus/common v0.49.0/go.mod h1:Kxm+EULxRbUkjGU6WFsQqo3ORzB4tyKvlWFOE9mB2sE= 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= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= @@ -658,8 +658,9 @@ github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMV github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -667,8 +668,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tecnickcom/farmhash64 v1.4.0 h1:i2r24VPPmKAT9V8bU5al3Hz0qF5EEClAyTNLOeGK3Oc= @@ -843,14 +845,14 @@ google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto v0.0.0-20240228224816-df926f6c8641 h1:GihpvzHjeZHw+/mzsWpdxwr1LaG6E3ff/gyeZlVHbyc= +google.golang.org/genproto v0.0.0-20240228224816-df926f6c8641/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641 h1:SO1wX9btGFrwj9EzH3ocqfwiPVOxfv4ggAJajzlHA5s= +google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641/go.mod h1:wLupoVsUfYPgOMwjzhYFbaVklw/INms+dqTp0tc1fv8= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240213162025-012b6fc9bca9 h1:SlwxrP5Neh11ftq/nw7ooqXcJNSqf0VM7wFiGRk8J9Y= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:om8Bj876Z0v9ei+RD1LnEWig7vpHQ371PUqsgjmLQEA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 h1:DKU1r6Tj5s1vlU/moGhuGz7E3xRfwjdAfDzbsaQJtEY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= diff --git a/go.mod b/go.mod index 7e7bd3d0..95bce4d4 100644 --- a/go.mod +++ b/go.mod @@ -22,13 +22,14 @@ require ( github.com/segmentio/kafka-go v0.4.47 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.18.2 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/tecnickcom/farmhash64 v1.4.0 github.com/tecnickcom/statsd v1.0.0 github.com/undefinedlabs/go-mpatch v1.0.7 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.20.0 + golang.org/x/net v0.21.0 golang.org/x/text v0.14.0 ) @@ -73,7 +74,7 @@ require ( github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.2 // indirect - github.com/hashicorp/consul/api v1.27.0 // indirect + github.com/hashicorp/consul/api v1.28.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.2 // indirect @@ -101,7 +102,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.0 // indirect - github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/common v0.49.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/sagikazarmark/crypt v0.18.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect @@ -122,7 +123,6 @@ require ( go.opentelemetry.io/otel/trace v1.24.0 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect golang.org/x/mod v0.15.0 // indirect - golang.org/x/net v0.21.0 // indirect golang.org/x/oauth2 v0.17.0 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.17.0 // indirect @@ -130,9 +130,9 @@ require ( golang.org/x/tools v0.18.0 // indirect google.golang.org/api v0.167.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto v0.0.0-20240228224816-df926f6c8641 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 // indirect google.golang.org/grpc v1.62.0 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 81a7f314..dfb29a7d 100644 --- a/go.sum +++ b/go.sum @@ -486,10 +486,10 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hamba/avro v1.5.6 h1:/UBljlJ9hLjkcY7PhpI/bFYb4RMEXHEwHr17gAm/+l8= github.com/hamba/avro v1.5.6/go.mod h1:3vNT0RLXXpFm2Tb/5KC71ZRJlOroggq1Rcitb6k4Fr8= -github.com/hashicorp/consul/api v1.27.0 h1:gmJ6DPKQog1426xsdmgk5iqDyoRiNc+ipBdJOqKQFjc= -github.com/hashicorp/consul/api v1.27.0/go.mod h1:JkekNRSou9lANFdt+4IKx3Za7XY0JzzpQjEb4Ivo1c8= -github.com/hashicorp/consul/sdk v0.15.1 h1:kKIGxc7CZtflcF5DLfHeq7rOQmRq3vk7kwISN9bif8Q= -github.com/hashicorp/consul/sdk v0.15.1/go.mod h1:7pxqqhqoaPqnBnzXD1StKed62LqJeClzVsUEy85Zr0A= +github.com/hashicorp/consul/api v1.28.2 h1:mXfkRHrpHN4YY3RqL09nXU1eHKLNiuAN4kHvDQ16k/8= +github.com/hashicorp/consul/api v1.28.2/go.mod h1:KyzqzgMEya+IZPcD65YFoOVAgPpbfERu4I/tzG6/ueE= +github.com/hashicorp/consul/sdk v0.16.0 h1:SE9m0W6DEfgIVCJX7xU+iv/hUl4m/nxqMTnCdMxDpJ8= +github.com/hashicorp/consul/sdk v0.16.0/go.mod h1:7pxqqhqoaPqnBnzXD1StKed62LqJeClzVsUEy85Zr0A= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -693,8 +693,8 @@ github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOA github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/common v0.49.0 h1:ToNTdK4zSnPVJmh698mGFkDor9wBI/iGaJy5dbH1EgI= +github.com/prometheus/common v0.49.0/go.mod h1:Kxm+EULxRbUkjGU6WFsQqo3ORzB4tyKvlWFOE9mB2sE= 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= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= @@ -740,8 +740,9 @@ github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMV github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -752,8 +753,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tecnickcom/farmhash64 v1.4.0 h1:i2r24VPPmKAT9V8bU5al3Hz0qF5EEClAyTNLOeGK3Oc= @@ -964,14 +966,14 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20220503193339-ba3ae3f07e29/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto v0.0.0-20240228224816-df926f6c8641 h1:GihpvzHjeZHw+/mzsWpdxwr1LaG6E3ff/gyeZlVHbyc= +google.golang.org/genproto v0.0.0-20240228224816-df926f6c8641/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641 h1:SO1wX9btGFrwj9EzH3ocqfwiPVOxfv4ggAJajzlHA5s= +google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641/go.mod h1:wLupoVsUfYPgOMwjzhYFbaVklw/INms+dqTp0tc1fv8= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240213162025-012b6fc9bca9 h1:SlwxrP5Neh11ftq/nw7ooqXcJNSqf0VM7wFiGRk8J9Y= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:om8Bj876Z0v9ei+RD1LnEWig7vpHQ371PUqsgjmLQEA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 h1:DKU1r6Tj5s1vlU/moGhuGz7E3xRfwjdAfDzbsaQJtEY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= diff --git a/pkg/dnscache/dnscache.go b/pkg/dnscache/dnscache.go new file mode 100644 index 00000000..cbc357c2 --- /dev/null +++ b/pkg/dnscache/dnscache.go @@ -0,0 +1,173 @@ +// Package dnscache provides a local DNS cache for LookupHost. +// The cache has a maximum size and a time-to-live (TTL) for each DNS entry. +package dnscache + +import ( + "context" + "fmt" + "net" + "sync" + "time" + + "github.com/Vonage/gosrvlib/pkg/threadsafe/tsmap" +) + +// dnsItem represents a DNS cache entry for a host. +type dnsItem struct { + // expireAt is the expiration time in seconds elapsed since January 1, 1970 UTC. + expireAt int64 + + // addrs is the list of IP addresses associated with the host by the DNS. + addrs []string +} + +// Resolver is a net.Resolver interface for DNS lookups. +type Resolver interface { + LookupHost(ctx context.Context, host string) (addrs []string, err error) +} + +// CacheResolver represents a cache for DNS items. +type CacheResolver struct { + // resolver is the net.resolver used to resolve DNS queries. + resolver Resolver + + // mux is the mutex for the cache. + mux *sync.RWMutex + + // ttl is the time-to-live for DNS items. + ttl time.Duration + + // size is the maximum size of the cache (min = 1). + size int + + // cache maps a host name to a DNS item. + cache map[string]*dnsItem +} + +// New creates a new DNS resolver with a cache of the specified size and TTL. +// If the resolver parameter is nil, a default resolver will be used. +// The size parameter determines the maximum number of DNS entries that can be cached (min = 1). +// If the size is less than or equal to zero, the cache will have a default size of 1. +// The ttl parameter specifies the time-to-live for each cached DNS entry. +func New(resolver Resolver, size int, ttl time.Duration) *CacheResolver { + if resolver == nil { + resolver = &net.Resolver{} + } + + if size <= 0 { + size = 1 + } + + return &CacheResolver{ + resolver: resolver, + mux: &sync.RWMutex{}, + ttl: ttl, + size: size, + cache: make(map[string]*dnsItem, size), + } +} + +// Reset clears the whole cache and initializes it with a new map of the specified size. +func (r *CacheResolver) Reset() { + r.mux.Lock() + defer r.mux.Unlock() + + r.cache = make(map[string]*dnsItem, r.size) +} + +// RemoveEntry removes the cache entry for the specified host. +func (r *CacheResolver) RemoveEntry(host string) { + tsmap.Delete(r.mux, r.cache, host) +} + +// LookupHost performs a DNS lookup for the given host using the DNSCacheResolver. +// It first checks if the host is already cached and not expired. If so, it returns +// the cached addresses. Otherwise, it performs a DNS lookup using the underlying +// Resolver and caches the obtained addresses for future use. +func (r *CacheResolver) LookupHost(ctx context.Context, host string) ([]string, error) { + item, exist := tsmap.GetOK(r.mux, r.cache, host) + if exist && (item.expireAt > time.Now().UTC().Unix()) { + return item.addrs, nil + } + + addrs, err := r.resolver.LookupHost(ctx, host) + if err != nil { + return nil, fmt.Errorf("failed DNS lookup for the host %s : %w", host, err) + } + + r.set(host, addrs, exist) + + return addrs, nil +} + +// DialContext dials the network and address specified by the parameters. +// It resolves the host from the address using the LookupHost method of the Resolver. +// It then attempts to establish a connection to each resolved IP address until a successful connection is made. +// If all connection attempts fail, it returns an error. +// The function returns the established net.Conn and any error encountered during the process. +// This function can replace the DialContext in http.Transport. +func (r *CacheResolver) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return nil, fmt.Errorf("failed to extract host and port from %s: %w", address, err) + } + + ips, err := r.LookupHost(ctx, host) + if err != nil { + return nil, err + } + + var ( + conn net.Conn + dialer net.Dialer + ) + + for _, ip := range ips { + conn, err = dialer.DialContext(ctx, network, net.JoinHostPort(ip, port)) + if err == nil { + return conn, nil + } + } + + return nil, fmt.Errorf("failed to dial %s: %w", address, err) +} + +// set adds or updates the cache entry for the given host with the provided addresses. +// If the cache is full, it will free up space by removing expired or old entries. +// If the host already exists in the cache, it will update the entry with the new addresses. +func (r *CacheResolver) set(host string, addrs []string, exist bool) { + if (!exist) && (len(r.cache) >= r.size) { + r.evict() + } + + tsmap.Set( + r.mux, + r.cache, + host, + &dnsItem{ + expireAt: time.Now().UTC().Add(r.ttl).Unix(), + addrs: addrs, + }, + ) +} + +// evict removes either the oldest entry or the first expired one from the DNS cache. +func (r *CacheResolver) evict() { + cuttime := time.Now().UTC().Unix() + oldest := int64(1<<63 - 1) + oldestHost := "" + + for h, d := range r.cache { + if d.expireAt < cuttime { + r.RemoveEntry(h) + return + } + + if d.expireAt < oldest { + oldest = d.expireAt + oldestHost = h + } + } + + r.RemoveEntry(oldestHost) +} diff --git a/pkg/dnscache/dnscache_bechmark_test.go b/pkg/dnscache/dnscache_bechmark_test.go new file mode 100644 index 00000000..c299e0ee --- /dev/null +++ b/pkg/dnscache/dnscache_bechmark_test.go @@ -0,0 +1,56 @@ +package dnscache + +import ( + "context" + "strconv" + "testing" + "time" +) + +const testDomain = "example.com" + +func BenchmarkLookupHost_cache_miss(b *testing.B) { + resolver := &mockResolver{ + lookupHost: func(_ context.Context, _ string) ([]string, error) { + return []string{"192.0.2.1"}, nil + }, + } + + r := New(resolver, int(1<<63-1), 1*time.Second) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, _ = r.LookupHost(context.TODO(), strconv.Itoa(i)+testDomain) + } +} + +func BenchmarkLookupHost_cache_hit(b *testing.B) { + resolver := &mockResolver{ + lookupHost: func(_ context.Context, _ string) ([]string, error) { + return []string{"192.0.2.1"}, nil + }, + } + + size := 255 + + r := New(resolver, size, 1*time.Minute) + + // fill the cache + for i := 1; i <= size; i++ { + _, _ = r.LookupHost(context.TODO(), strconv.Itoa(i)+testDomain) + } + + var j int + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + j++ + if j > size { + j = 0 + } + + _, _ = r.LookupHost(context.TODO(), strconv.Itoa(j)+testDomain) + } +} diff --git a/pkg/dnscache/dnscache_test.go b/pkg/dnscache/dnscache_test.go new file mode 100644 index 00000000..7b06feaf --- /dev/null +++ b/pkg/dnscache/dnscache_test.go @@ -0,0 +1,286 @@ +package dnscache + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "golang.org/x/net/nettest" +) + +func TestNew(t *testing.T) { + t.Parallel() + + got := New(nil, 3, 5*time.Second) + require.NotNil(t, got) + + require.NotNil(t, got.resolver) + require.NotNil(t, got.mux) + + require.Equal(t, 3, got.size) + require.Equal(t, 5*time.Second, got.ttl) + + require.NotNil(t, got.cache) + require.Empty(t, got.cache) + + got = New(nil, 0, 1*time.Second) + require.Equal(t, 1, got.size) +} + +func Test_evict_expired(t *testing.T) { + t.Parallel() + + r := New(nil, 3, 1*time.Minute) + + r.cache = map[string]*dnsItem{ + "example.com": { + expireAt: time.Now().UTC().Add(-2 * time.Second).Unix(), + }, + "example.org": { + expireAt: time.Now().UTC().Add(11 * time.Second).Unix(), + }, + "example.net": { + expireAt: time.Now().UTC().Add(13 * time.Second).Unix(), + }, + } + + require.Len(t, r.cache, 3) + + r.evict() + + require.Len(t, r.cache, 2) + require.Contains(t, r.cache, "example.org") + require.Contains(t, r.cache, "example.net") +} + +func Test_evict_oldest(t *testing.T) { + t.Parallel() + + r := New(nil, 3, 1*time.Second) + + r.cache = map[string]*dnsItem{ + "example.com": { + expireAt: time.Now().UTC().Add(11 * time.Second).Unix(), + }, + "example.org": { + expireAt: time.Now().UTC().Add(7 * time.Second).Unix(), + }, + "example.net": { + expireAt: time.Now().UTC().Add(13 * time.Second).Unix(), + }, + } + + r.evict() + + require.Len(t, r.cache, 2) + require.Contains(t, r.cache, "example.com") + require.Contains(t, r.cache, "example.net") +} + +/* +NOTE: +The IP blocks 192.0.2.0/24 (TEST-NET-1), 198.51.100.0/24 (TEST-NET-2), +and 203.0.113.0/24 (TEST-NET-3) are provided for use in documentation. +*/ + +func Test_set(t *testing.T) { + t.Parallel() + + r := New(nil, 2, 10*time.Second) + + r.set("example.com", []string{"192.0.2.1"}, false) + time.Sleep(1 * time.Second) + r.set("example.org", []string{"192.0.2.2", "198.51.100.2"}, false) + + require.Len(t, r.cache, 2) + require.Contains(t, r.cache, "example.com") + require.Contains(t, r.cache, "example.org") + + r.set("example.net", []string{"192.0.2.3", "198.51.100.3", "203.0.113.3"}, false) + + require.Len(t, r.cache, 2) + require.Contains(t, r.cache, "example.org") + require.Contains(t, r.cache, "example.net") + + r.set("example.net", []string{"198.51.100.4"}, true) + + require.Len(t, r.cache, 2) + require.Contains(t, r.cache, "example.org") + require.Contains(t, r.cache, "example.net") + require.Equal(t, []string{"198.51.100.4"}, r.cache["example.net"].addrs) +} + +type mockResolver struct { + lookupHost func(ctx context.Context, host string) ([]string, error) +} + +func (m *mockResolver) LookupHost(ctx context.Context, host string) ([]string, error) { + return m.lookupHost(ctx, host) +} + +func Test_LookupHost_error(t *testing.T) { + t.Parallel() + + resolver := &mockResolver{ + lookupHost: func(_ context.Context, _ string) ([]string, error) { + return nil, errors.New("mock error") + }, + } + + r := New(resolver, 1, 1*time.Second) + + addrs, err := r.LookupHost(context.TODO(), "example.com") + require.Error(t, err) + require.Nil(t, addrs) +} + +func Test_LookupHost(t *testing.T) { + t.Parallel() + + var i int + + resolver := &mockResolver{ + lookupHost: func(_ context.Context, _ string) ([]string, error) { + i++ + ip := fmt.Sprintf("192.0.2.%d", i) + return []string{ip}, nil + }, + } + + r := New(resolver, 1, 1*time.Second) + + // cache miss + addrs, err := r.LookupHost(context.TODO(), "example.com") + require.NoError(t, err) + require.Equal(t, []string{"192.0.2.1"}, addrs) + + // cache hit + addrs, err = r.LookupHost(context.TODO(), "example.com") + require.NoError(t, err) + require.Equal(t, []string{"192.0.2.1"}, addrs) + + time.Sleep(1 * time.Second) + + // cache expired + addrs, err = r.LookupHost(context.TODO(), "example.com") + require.NoError(t, err) + require.Equal(t, []string{"192.0.2.2"}, addrs) + + // cache miss with eviction + addrs, err = r.LookupHost(context.TODO(), "example.net") + require.NoError(t, err) + require.Equal(t, []string{"192.0.2.3"}, addrs) +} + +func Test_DialContext_lookup_errors(t *testing.T) { + t.Parallel() + + resolver := &mockResolver{ + lookupHost: func(_ context.Context, _ string) ([]string, error) { + return nil, errors.New("mock error") + }, + } + + r := New(resolver, 1, 1*time.Second) + + // SplitHostPort error + conn, err := r.DialContext(context.TODO(), "tcp", "~~~") + require.Error(t, err) + require.Nil(t, conn) + + // LookupHost error + conn, err = r.DialContext(context.TODO(), "tcp", "example.com:80") + require.Error(t, err) + require.Nil(t, conn) +} + +func Test_DialContext_ip_error(t *testing.T) { + t.Parallel() + + resolver := &mockResolver{ + lookupHost: func(_ context.Context, _ string) ([]string, error) { + return []string{"1"}, nil + }, + } + + r := New(resolver, 1, 1*time.Second) + + conn, err := r.DialContext(context.TODO(), "tcp", "example.com:80") + require.Error(t, err) + require.Nil(t, conn) +} + +func Test_DialContext(t *testing.T) { + t.Parallel() + + network := "tcp" + + listener, err := nettest.NewLocalListener(network) + require.NoError(t, err) + require.NotNil(t, listener) + + defer func() { + err := listener.Close() + require.NoError(t, err) + }() + + address := listener.Addr().String() + addrparts := strings.Split(address, ":") + ip := addrparts[0] + + resolver := &mockResolver{ + lookupHost: func(_ context.Context, _ string) ([]string, error) { + return []string{ip}, nil + }, + } + + r := New(resolver, 1, 1*time.Second) + + conn, err := r.DialContext(context.TODO(), network, address) + require.NoError(t, err) + require.NotNil(t, conn) +} + +func Test_Reset(t *testing.T) { + t.Parallel() + + r := New(nil, 1, 1*time.Second) + + r.cache = map[string]*dnsItem{ + "example.com": { + expireAt: time.Now().UTC().Unix(), + }, + } + + r.Reset() + + require.Empty(t, r.cache) +} + +func Test_RemoveEntry(t *testing.T) { + t.Parallel() + + r := New(nil, 3, 1*time.Second) + + r.cache = map[string]*dnsItem{ + "example.com": { + expireAt: time.Now().UTC().Unix(), + }, + "example.net": { + expireAt: time.Now().UTC().Unix(), + }, + "example.org": { + expireAt: time.Now().UTC().Unix(), + }, + } + + r.RemoveEntry("example.net") + + require.Len(t, r.cache, 2) + require.Contains(t, r.cache, "example.com") + require.Contains(t, r.cache, "example.org") +} diff --git a/pkg/httpclient/client_test.go b/pkg/httpclient/client_test.go index b788265c..1c1a41f0 100644 --- a/pkg/httpclient/client_test.go +++ b/pkg/httpclient/client_test.go @@ -38,10 +38,8 @@ func TestNew(t *testing.T) { require.Equal(t, fn(http.DefaultTransport), got.client.Transport) } -//nolint:gocognit +//nolint:gocognit,tparallel,paralleltest func TestClient_Do(t *testing.T) { - t.Parallel() - bodyStr := `TEST BODY OK` body := make([]byte, 0) diff --git a/pkg/httpclient/options.go b/pkg/httpclient/options.go index 7bd52d37..42067469 100644 --- a/pkg/httpclient/options.go +++ b/pkg/httpclient/options.go @@ -1,6 +1,8 @@ package httpclient import ( + "context" + "net" "net/http" "time" ) @@ -8,6 +10,9 @@ import ( // InstrumentRoundTripper is an alias for a RoundTripper function. type InstrumentRoundTripper func(next http.RoundTripper) http.RoundTripper +// DialContextFunc is an alias for a net.Dialer.DialContext function. +type DialContextFunc func(ctx context.Context, network, address string) (net.Conn, error) + // RedactFn is an alias for a redact function. type RedactFn func(s string) string @@ -55,3 +60,15 @@ func WithLogPrefix(prefix string) Option { c.logPrefix = prefix } } + +// WithDialContext sets the DialContext function for the HTTP client. +// The DialContext function is used to establish network connections. +// It allows customizing the behavior of the client's underlying transport. +func WithDialContext(fn DialContextFunc) Option { + return func(c *Client) { + t, ok := c.client.Transport.(*http.Transport) + if ok { + t.DialContext = fn + } + } +} diff --git a/pkg/httpclient/options_test.go b/pkg/httpclient/options_test.go index 4e22892a..a5702748 100644 --- a/pkg/httpclient/options_test.go +++ b/pkg/httpclient/options_test.go @@ -1,6 +1,9 @@ package httpclient import ( + "context" + "errors" + "net" "net/http" "testing" "time" @@ -61,3 +64,15 @@ func TestWithLogPrefix(t *testing.T) { WithLogPrefix(v)(c) require.Equal(t, v, c.logPrefix) } + +func TestWithDialContext(t *testing.T) { + t.Parallel() + + c := defaultClient() + v := func(_ context.Context, _, _ string) (net.Conn, error) { return nil, errors.New("TEST") } + WithDialContext(v)(c) + + out, err := c.client.Transport.(*http.Transport).DialContext(context.TODO(), "", "") + require.Error(t, err) + require.Nil(t, out) +} diff --git a/pkg/threadsafe/tsmap/example_tsmap_test.go b/pkg/threadsafe/tsmap/example_tsmap_test.go index afd5d5d2..6e18f100 100644 --- a/pkg/threadsafe/tsmap/example_tsmap_test.go +++ b/pkg/threadsafe/tsmap/example_tsmap_test.go @@ -20,6 +20,19 @@ func ExampleSet() { // map[0:Hello 1:World] } +func ExampleDelete() { + mux := &sync.Mutex{} + + m := map[int]string{0: "Hello", 1: "World"} + + tsmap.Delete(mux, m, 0) + + fmt.Println(m) + + // Output: + // map[1:World] +} + func ExampleGet() { mux := &sync.RWMutex{} @@ -32,6 +45,22 @@ func ExampleGet() { // World } +func ExampleGetOK() { + mux := &sync.RWMutex{} + + m := map[int]string{0: "Hello", 1: "World"} + + v, ok := tsmap.GetOK(mux, m, 0) + fmt.Println(v, ok) + + v, ok = tsmap.GetOK(mux, m, 3) + fmt.Println(v, ok) + + // Output: + // Hello true + // false +} + func ExampleLen() { mux := &sync.RWMutex{} diff --git a/pkg/threadsafe/tsmap/tsmap.go b/pkg/threadsafe/tsmap/tsmap.go index 1e730851..9d8c7ff0 100644 --- a/pkg/threadsafe/tsmap/tsmap.go +++ b/pkg/threadsafe/tsmap/tsmap.go @@ -18,7 +18,16 @@ func Set[M ~map[K]V, K comparable, V any](mux threadsafe.Locker, m M, k K, v V) m[k] = v } +// Delete is a thread-safe function to delete the key-value pair with the specified key from the given map. +func Delete[M ~map[K]V, K comparable, V any](mux threadsafe.Locker, m M, k K) { + mux.Lock() + defer mux.Unlock() + + delete(m, k) +} + // Get is a thread-safe function to get a value by key k in a map m. +// See also GetOK. func Get[M ~map[K]V, K comparable, V any](mux threadsafe.RLocker, m M, k K) V { mux.RLock() defer mux.RUnlock() @@ -26,6 +35,17 @@ func Get[M ~map[K]V, K comparable, V any](mux threadsafe.RLocker, m M, k K) V { return m[k] } +// GetOK is a thread-safe function to get a value by key k in a map m. +// The second return value is a boolean that indicates whether the key was present in the map. +func GetOK[M ~map[K]V, K comparable, V any](mux threadsafe.RLocker, m M, k K) (V, bool) { + mux.RLock() + defer mux.RUnlock() + + v, ok := m[k] + + return v, ok +} + // Len is a thread-safe function to get the length of a map m. func Len[M ~map[K]V, K comparable, V any](mux threadsafe.RLocker, m M) int { mux.RLock() diff --git a/pkg/threadsafe/tsmap/tsmap_test.go b/pkg/threadsafe/tsmap/tsmap_test.go index 1236c0d9..c6e9f4fe 100644 --- a/pkg/threadsafe/tsmap/tsmap_test.go +++ b/pkg/threadsafe/tsmap/tsmap_test.go @@ -20,6 +20,24 @@ func TestSet(t *testing.T) { require.Equal(t, "World", m[1]) } +func TestDelete(t *testing.T) { + t.Parallel() + + mux := &sync.Mutex{} + + m := map[int]string{3: "Hello", 5: "World"} + + v, ok := m[5] + require.True(t, ok) + require.Equal(t, "World", v) + + Delete(mux, m, 5) + + v, ok = m[5] + require.False(t, ok) + require.Equal(t, "", v) +} + func TestGet(t *testing.T) { t.Parallel() @@ -29,6 +47,23 @@ func TestGet(t *testing.T) { require.Equal(t, "Hello", Get(mux, m, 0)) require.Equal(t, "World", Get(mux, m, 1)) + require.Equal(t, "", Get(mux, m, 3)) +} + +func TestGetOK(t *testing.T) { + t.Parallel() + + mux := &sync.RWMutex{} + + m := map[int]string{5: "Hello", 7: "World"} + + v, ok := GetOK(mux, m, 7) + require.True(t, ok) + require.Equal(t, "World", v) + + v, ok = GetOK(mux, m, 6) + require.False(t, ok) + require.Equal(t, "", v) } func TestLen(t *testing.T) {