Skip to content

Commit c7dd381

Browse files
authored
Support UTC DateTime Structures
Contrary to 5.0, invalid timezones received from the server will result in the transaction being retried and cause the connection to eventually put back to pool, after all attempts are exhausted. While this is not ideal, this has been done so to minimize breaking changes in 4.x. This also fixes race condition in integration tests where RESET was racing against NEXT and would sometimes see a FAILURE, marking the connection as dead.
1 parent 576dcb4 commit c7dd381

File tree

16 files changed

+955
-1042
lines changed

16 files changed

+955
-1042
lines changed

neo4j/internal/bolt/bolt3.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ func NewBolt3(serverName string, conn net.Conn, logger log.Logger, boltLog log.B
113113
b.state = bolt3_dead
114114
},
115115
boltLogger: boltLog,
116+
useUtc: false,
116117
}
117118
return b
118119
}

neo4j/internal/bolt/bolt3_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ func TestBolt3(ot *testing.T) {
101101

102102
bolt := c.(*bolt3)
103103
assertBoltState(t, bolt3_ready, bolt)
104+
if bolt.out.useUtc {
105+
t.Fatalf("Bolt 3 connections must never send and receive UTC datetimes")
106+
}
104107
return bolt, cleanup
105108
}
106109

neo4j/internal/bolt/bolt4.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,10 @@ func (b *bolt4) connect(minor int, auth map[string]interface{}, userAgent string
226226
hello["routing"] = routingContext
227227
}
228228
}
229+
checkUtcPatch := minor >= 3
230+
if checkUtcPatch {
231+
hello["patch_bolt"] = []string{"utc"}
232+
}
229233
// Merge authentication keys into hello, avoid overwriting existing keys
230234
for k, v := range auth {
231235
_, exists := hello[k]
@@ -244,6 +248,17 @@ func (b *bolt4) connect(minor int, auth map[string]interface{}, userAgent string
244248

245249
b.connId = succ.connectionId
246250
b.serverVersion = succ.server
251+
if checkUtcPatch {
252+
useUtc := false
253+
for _, patch := range succ.patches {
254+
if patch == "utc" {
255+
useUtc = true
256+
break
257+
}
258+
}
259+
b.in.hyd.useUtc = useUtc
260+
b.out.useUtc = useUtc
261+
}
247262

248263
// Construct log identity
249264
connectionLogId := fmt.Sprintf("%s@%s", b.connId, b.serverName)

neo4j/internal/bolt/bolt4_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ func TestBolt4(ot *testing.T) {
138138
AssertStringEqual(t, bolt.ServerName(), "serverName")
139139
AssertTrue(t, bolt.IsAlive())
140140
AssertTrue(t, reflect.DeepEqual(bolt.in.connReadTimeout, time.Duration(-1)))
141+
AssertFalse(t, bolt.out.useUtc)
141142
})
142143

143144
ot.Run("Connect success with timeout hint", func(t *testing.T) {
@@ -153,6 +154,36 @@ func TestBolt4(ot *testing.T) {
153154
AssertTrue(t, reflect.DeepEqual(bolt.in.connReadTimeout, 42*time.Second))
154155
})
155156

157+
for _, version := range [][]byte{{4, 3}, {4, 4}} {
158+
major := version[0]
159+
minor := version[1]
160+
ot.Run(fmt.Sprintf("[%d.%d] Connect success with UTC patch", major, minor), func(t *testing.T) {
161+
bolt, cleanup := connectToServer(t, func(srv *bolt4server) {
162+
srv.waitForHandshake()
163+
srv.acceptVersion(major, minor)
164+
srv.waitForHelloWithPatches([]interface{}{"utc"})
165+
srv.acceptHelloWithPatches([]interface{}{"utc"})
166+
})
167+
defer cleanup()
168+
defer bolt.Close()
169+
170+
AssertTrue(t, bolt.out.useUtc)
171+
})
172+
173+
ot.Run(fmt.Sprintf("[%d.%d] Connect success with unknown patch", major, minor), func(t *testing.T) {
174+
bolt, cleanup := connectToServer(t, func(srv *bolt4server) {
175+
srv.waitForHandshake()
176+
srv.acceptVersion(major, minor)
177+
srv.waitForHelloWithPatches([]interface{}{"utc"})
178+
srv.acceptHelloWithPatches([]interface{}{"some-unknown-patch"})
179+
})
180+
defer cleanup()
181+
defer bolt.Close()
182+
183+
AssertFalse(t, bolt.out.useUtc)
184+
})
185+
}
186+
156187
invalidValues := []interface{}{4.2, "42", -42}
157188
for _, value := range invalidValues {
158189
ot.Run(fmt.Sprintf("Connect success with ignored invalid timeout hint %v", value), func(t *testing.T) {

neo4j/internal/bolt/bolt4server_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"fmt"
2424
"io"
2525
"net"
26+
"reflect"
2627
"testing"
2728

2829
"github.com/neo4j/neo4j-go-driver/v4/neo4j/internal/packstream"
@@ -76,6 +77,15 @@ func (s *bolt4server) sendIgnoredMsg() {
7677
s.send(msgIgnored)
7778
}
7879

80+
func (s *bolt4server) waitForHelloWithPatches(patches []interface{}) map[string]interface{} {
81+
m := s.waitForHello()
82+
actualPatches := m["patch_bolt"]
83+
if !reflect.DeepEqual(actualPatches, patches) {
84+
s.sendFailureMsg("?", fmt.Sprintf("Expected %v patches, got %v", patches, actualPatches))
85+
}
86+
return m
87+
}
88+
7989
// Returns the first hello field
8090
func (s *bolt4server) waitForHello() map[string]interface{} {
8191
msg := s.receiveMsg()
@@ -240,6 +250,14 @@ func (s *bolt4server) acceptHelloWithHints(hints map[string]interface{}) {
240250
})
241251
}
242252

253+
func (s *bolt4server) acceptHelloWithPatches(patches []interface{}) {
254+
s.send(msgSuccess, map[string]interface{}{
255+
"connection_id": "cid",
256+
"server": "fake/4.5",
257+
"patch_bolt": patches,
258+
})
259+
}
260+
243261
func (s *bolt4server) rejectHelloUnauthorized() {
244262
s.send(msgFailure, map[string]interface{}{
245263
"code": "Neo.ClientError.Security.Unauthorized",

neo4j/internal/bolt/hydrator.go

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type success struct {
5151
routingTable *db.RoutingTable
5252
num uint32
5353
configurationHints map[string]interface{}
54+
patches []string
5455
}
5556

5657
func (s *success) String() string {
@@ -92,6 +93,7 @@ type hydrator struct {
9293
cachedSuccess success
9394
boltLogger log.BoltLogger
9495
logId string
96+
useUtc bool
9597
}
9698

9799
func (h *hydrator) setErr(err error) {
@@ -245,6 +247,9 @@ func (h *hydrator) success(n uint32) *success {
245247
case "hints":
246248
hints := h.amap()
247249
succ.configurationHints = hints
250+
case "patch_bolt":
251+
patches := h.strings()
252+
succ.patches = patches
248253
default:
249254
// Unknown key, waste it
250255
h.trash()
@@ -404,9 +409,25 @@ func (h *hydrator) value() interface{} {
404409
case 'Y':
405410
return h.point3d(n)
406411
case 'F':
412+
if h.useUtc {
413+
return h.unknownStructError(t)
414+
}
407415
return h.dateTimeOffset(n)
416+
case 'I':
417+
if !h.useUtc {
418+
return h.unknownStructError(t)
419+
}
420+
return h.utcDateTimeOffset(n)
408421
case 'f':
422+
if h.useUtc {
423+
return h.unknownStructError(t)
424+
}
409425
return h.dateTimeNamedZone(n)
426+
case 'i':
427+
if !h.useUtc {
428+
return h.unknownStructError(t)
429+
}
430+
return h.utcDateTimeNamedZone(n)
410431
case 'd':
411432
return h.localDateTime(n)
412433
case 'D':
@@ -418,8 +439,7 @@ func (h *hydrator) value() interface{} {
418439
case 'E':
419440
return h.duration(n)
420441
default:
421-
h.err = errors.New(fmt.Sprintf("Unknown tag: %02x", t))
422-
return nil
442+
return h.unknownStructError(t)
423443
}
424444
case packstream.PackedByteArray:
425445
return h.unp.ByteArray()
@@ -568,30 +588,82 @@ func (h *hydrator) point3d(n uint32) interface{} {
568588

569589
func (h *hydrator) dateTimeOffset(n uint32) interface{} {
570590
h.unp.Next()
571-
secs := h.unp.Int()
591+
seconds := h.unp.Int()
572592
h.unp.Next()
573-
nans := h.unp.Int()
593+
nanos := h.unp.Int()
574594
h.unp.Next()
575-
offs := h.unp.Int()
576-
t := time.Unix(secs, nans).UTC()
577-
l := time.FixedZone("Offset", int(offs))
578-
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), l)
595+
offset := h.unp.Int()
596+
// time.Time in local timezone, e.g. 15th of June 2020, 15:30 in Paris (UTC+2h)
597+
unixTime := time.Unix(seconds, nanos)
598+
// time.Time computed in UTC timezone, e.g. 15th of June 2020, 13:30 in UTC
599+
utcTime := unixTime.UTC()
600+
// time.Time **copied** as-is in the target timezone, e.g. 15th of June 2020, 13:30 in target tz
601+
timeZone := time.FixedZone("Offset", int(offset))
602+
return time.Date(
603+
utcTime.Year(),
604+
utcTime.Month(),
605+
utcTime.Day(),
606+
utcTime.Hour(),
607+
utcTime.Minute(),
608+
utcTime.Second(),
609+
utcTime.Nanosecond(),
610+
timeZone,
611+
)
612+
}
613+
614+
func (h *hydrator) utcDateTimeOffset(n uint32) interface{} {
615+
h.unp.Next()
616+
seconds := h.unp.Int()
617+
h.unp.Next()
618+
nanos := h.unp.Int()
619+
h.unp.Next()
620+
offset := h.unp.Int()
621+
timeZone := time.FixedZone("Offset", int(offset))
622+
return time.Unix(seconds, nanos).In(timeZone)
579623
}
580624

581625
func (h *hydrator) dateTimeNamedZone(n uint32) interface{} {
626+
h.unp.Next()
627+
seconds := h.unp.Int()
628+
h.unp.Next()
629+
nanos := h.unp.Int()
630+
h.unp.Next()
631+
zone := h.unp.String()
632+
// time.Time in local timezone, e.g. 15th of June 2020, 15:30 in Paris (UTC+2h)
633+
unixTime := time.Unix(seconds, nanos)
634+
// time.Time computed in UTC timezone, e.g. 15th of June 2020, 13:30 in UTC
635+
utcTime := unixTime.UTC()
636+
// time.Time **copied** as-is in the target timezone, e.g. 15th of June 2020, 13:30 in target tz
637+
l, err := time.LoadLocation(zone)
638+
if err != nil {
639+
h.setErr(err)
640+
return nil
641+
}
642+
return time.Date(
643+
utcTime.Year(),
644+
utcTime.Month(),
645+
utcTime.Day(),
646+
utcTime.Hour(),
647+
utcTime.Minute(),
648+
utcTime.Second(),
649+
utcTime.Nanosecond(),
650+
l,
651+
)
652+
}
653+
654+
func (h *hydrator) utcDateTimeNamedZone(n uint32) interface{} {
582655
h.unp.Next()
583656
secs := h.unp.Int()
584657
h.unp.Next()
585658
nans := h.unp.Int()
586659
h.unp.Next()
587660
zone := h.unp.String()
588-
t := time.Unix(secs, nans).UTC()
589-
l, err := time.LoadLocation(zone)
661+
timeZone, err := time.LoadLocation(zone)
590662
if err != nil {
591663
h.setErr(err)
592664
return nil
593665
}
594-
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), l)
666+
return time.Unix(secs, nans).In(timeZone)
595667
}
596668

597669
func (h *hydrator) localDateTime(n uint32) interface{} {
@@ -740,3 +812,8 @@ func parseNotification(m map[string]interface{}) db.Notification {
740812

741813
return n
742814
}
815+
816+
func (h *hydrator) unknownStructError(t byte) interface{} {
817+
h.setErr(fmt.Errorf("Unknown tag: %02x", t))
818+
return nil
819+
}

0 commit comments

Comments
 (0)