Commit 95fbc76f authored by Kirill Smelkov's avatar Kirill Smelkov

Merge branch 't' into t2

* t: (129 commits)
  X go.mod: v↑ *
  go/zodb/btree: Change V<op> family to also provide visited node key coverage on visit callback
  go/zodb/btree: Add KeyRange type
  go/zodb/btree: Introduce constants for min/max key value
  go/zodb/btree: tests: Don't forget to close storage
  go/zodb/btree: Cosmetics
  .
  .
  .
  .
  .
  .
  .
  X Start reworking BTree to provide keycov on visit callback
  X go/neo: Fix credentials parsing with go1.17
  fixup! Y client: Fix URI scheme to move credentials out of query
  go/internal/xtesting: Add missing X
  go/zodb/{fs1,zeo}: ~staticcheck
  go/zodb/btree: Fix missing return on data-consistency error
  go/zodb, go/zodb/btree: Fix go generate after rename on zodbtools side
  ...
parents ad5940f3 ea538368
...@@ -3,24 +3,36 @@ module lab.nexedi.com/kirr/neo/go ...@@ -3,24 +3,36 @@ module lab.nexedi.com/kirr/neo/go
go 1.14 go 1.14
require ( require (
github.com/DataDog/czlib v0.0.0-20160811164712-4bc9a24e37f2 github.com/DataDog/czlib v0.0.0-20210322182103-8087f4e14ae7
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 github.com/cznic/strutil v0.0.0-20181122101858-275e90344537
github.com/fsnotify/fsnotify v1.4.10-0.20200417215612-7f4cf4dd2b52 github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/fsnotify/fsnotify v1.5.1
github.com/gwenn/gosqlite v0.0.0-20200521090053-24878be1a237 github.com/golang/glog v1.0.0
github.com/kisielk/og-rek v1.0.1-0.20180928202415-8b25c4cefd6c github.com/golang/protobuf v1.4.3 // indirect
github.com/google/go-cmp v0.5.4 // indirect
github.com/gwenn/gosqlite v0.0.0-20211101095637-b18efb2e44c8
github.com/gwenn/yacr v0.0.0-20211101095056-492fb0c571bc // indirect
github.com/kisielk/og-rek v1.2.0
github.com/kr/text v0.2.0 // indirect
github.com/kylelemons/godebug v1.1.0 github.com/kylelemons/godebug v1.1.0
github.com/philhofer/fwd v1.0.0 // indirect github.com/philhofer/fwd v1.1.1
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/shamaton/msgpack v1.1.1 github.com/shamaton/msgpack v1.2.1
github.com/soheilhy/cmux v0.1.4 github.com/soheilhy/cmux v0.1.5
github.com/someonegg/gocontainer v1.0.0 github.com/someonegg/gocontainer v1.0.0
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.7.0
github.com/tinylib/msgp v1.1.3-0.20200327023543-e88e92c0ccca github.com/tinylib/msgp v1.1.6
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 // indirect
google.golang.org/grpc v1.32.0 // indirect golang.org/x/net v0.0.0-20211111160137-58aab5ef257a // indirect
google.golang.org/grpc/examples v0.0.0-20200915000551-32e7099cccac // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
lab.nexedi.com/kirr/go123 v0.0.0-20200915142026-a281a51cf49b golang.org/x/sys v0.0.0-20211111213525-f221eed1c01e // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705 // indirect
google.golang.org/grpc v1.36.0 // indirect
google.golang.org/grpc/examples v0.0.0-20210301210255-fc8f38cccf75 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
lab.nexedi.com/kirr/go123 v0.0.0-20210906140734-c9eb28d9e408
) )
This diff is collapsed.
// Copyright (C) 2017-2020 Nexedi SA and Contributors. // Copyright (C) 2017-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
...@@ -29,10 +29,13 @@ import ( ...@@ -29,10 +29,13 @@ import (
"os" "os"
"os/exec" "os/exec"
"reflect" "reflect"
"strings"
"sync" "sync"
"testing" "testing"
"time"
"lab.nexedi.com/kirr/go123/xerr" "lab.nexedi.com/kirr/go123/xerr"
"lab.nexedi.com/kirr/go123/xstrings"
"lab.nexedi.com/kirr/neo/go/zodb" "lab.nexedi.com/kirr/neo/go/zodb"
) )
...@@ -92,13 +95,14 @@ func NeedPy(t testing.TB, modules ...string) { ...@@ -92,13 +95,14 @@ func NeedPy(t testing.TB, modules ...string) {
} }
// ZRawObject represents raw ZODB object state. // ZRawObject represents raw ZODB object state.
type ZRawObject struct { type ZRawObject struct { // keep in sync with zodb(test).ZRawObject
Oid zodb.Oid Oid zodb.Oid
Data []byte // raw serialized zodb data Data []byte // raw serialized zodb data
} }
// ZPyCommitRaw commits new transaction into database @ zurl with raw data specified by objv. // ZPyCommitRaw commits new transaction into database @ zurl with raw data specified by objv.
// //
// Nil data means "delete object".
// The commit is performed via zodbtools/py. // The commit is performed via zodbtools/py.
func ZPyCommitRaw(zurl string, at zodb.Tid, objv ...ZRawObject) (_ zodb.Tid, err error) { func ZPyCommitRaw(zurl string, at zodb.Tid, objv ...ZRawObject) (_ zodb.Tid, err error) {
defer xerr.Contextf(&err, "%s: zpycommit @%s", zurl, at) defer xerr.Contextf(&err, "%s: zpycommit @%s", zurl, at)
...@@ -109,9 +113,14 @@ func ZPyCommitRaw(zurl string, at zodb.Tid, objv ...ZRawObject) (_ zodb.Tid, err ...@@ -109,9 +113,14 @@ func ZPyCommitRaw(zurl string, at zodb.Tid, objv ...ZRawObject) (_ zodb.Tid, err
fmt.Fprintf(zin, "description %q\n", fmt.Sprintf("test commit; at=%s", at)) fmt.Fprintf(zin, "description %q\n", fmt.Sprintf("test commit; at=%s", at))
fmt.Fprintf(zin, "extension %q\n", "") fmt.Fprintf(zin, "extension %q\n", "")
for _, obj := range objv { for _, obj := range objv {
fmt.Fprintf(zin, "obj %s %d null:00\n", obj.Oid, len(obj.Data)) // !data -> delete
zin.Write(obj.Data) if obj.Data == nil {
zin.WriteString("\n") fmt.Fprintf(zin, "obj %s delete\n", obj.Oid)
} else {
fmt.Fprintf(zin, "obj %s %d null:00\n", obj.Oid, len(obj.Data))
zin.Write(obj.Data)
zin.WriteString("\n")
}
} }
zin.WriteString("\n") zin.WriteString("\n")
...@@ -135,6 +144,33 @@ func ZPyCommitRaw(zurl string, at zodb.Tid, objv ...ZRawObject) (_ zodb.Tid, err ...@@ -135,6 +144,33 @@ func ZPyCommitRaw(zurl string, at zodb.Tid, objv ...ZRawObject) (_ zodb.Tid, err
// XXX + ZPyCommitSrv ? // XXX + ZPyCommitSrv ?
// ZPyRestore restores transactions specified by zin in zodbdump format.
//
// The restore is performed via zodbtools/py.
func ZPyRestore(zurl string, zin string) (tidv []zodb.Tid, err error) {
defer xerr.Contextf(&err, "%s: zpyrestore", zurl)
// run py `zodb restore`
cmd:= exec.Command("python", "-m", "zodbtools.zodb", "restore", zurl)
cmd.Stdin = strings.NewReader(zin)
cmd.Stderr = os.Stderr
out, err := cmd.Output()
if err != nil {
return nil, err
}
for _, line := range xstrings.SplitLines(string(out), "\n") {
tid, err := zodb.ParseTid(line)
if err != nil {
return nil, fmt.Errorf("restored, but invalid output: %s", err)
}
tidv = append(tidv, tid)
}
return tidv, nil
}
// ---- tests for storage drivers ---- // ---- tests for storage drivers ----
...@@ -352,7 +388,7 @@ func DrvTestWatch(t *testing.T, zurl string, zdrvOpen zodb.DriverOpener, bugv .. ...@@ -352,7 +388,7 @@ func DrvTestWatch(t *testing.T, zurl string, zdrvOpen zodb.DriverOpener, bugv ..
ctx := context.Background() ctx := context.Background()
watchq := make(chan zodb.Event) watchq := make(chan zodb.Event)
zdrv, at0, err := zdrvOpen(ctx, u, &zodb.DriverOptions{ReadOnly: true, Watchq: watchq}) zdrv, at0, err := zdrvOpen(ctx, u, &zodb.DriverOptions{ReadOnly: true, Watchq: watchq}); X(err)
if at0 != at { if at0 != at {
t.Fatalf("opened @ %s ; want %s", at0, at) t.Fatalf("opened @ %s ; want %s", at0, at)
} }
...@@ -438,6 +474,11 @@ func DrvTestWatch(t *testing.T, zurl string, zdrvOpen zodb.DriverOpener, bugv .. ...@@ -438,6 +474,11 @@ func DrvTestWatch(t *testing.T, zurl string, zdrvOpen zodb.DriverOpener, bugv ..
} }
} }
// commit something more and wait a bit to raise chances the driver enqueues to watchq<- .
_ = xcommit(at, ZRawObject{0, b("at the end")})
time.Sleep(1*time.Second)
// the driver must handle Close and cancel that watchq<-
err = zdrv.Close(); X(err) err = zdrv.Close(); X(err)
e, ok := <-watchq e, ok := <-watchq
...@@ -450,6 +491,7 @@ func DrvTestWatch(t *testing.T, zurl string, zdrvOpen zodb.DriverOpener, bugv .. ...@@ -450,6 +491,7 @@ func DrvTestWatch(t *testing.T, zurl string, zdrvOpen zodb.DriverOpener, bugv ..
// FatalIf(t) returns function f(err), which call t.Fatal if err != nil. // FatalIf(t) returns function f(err), which call t.Fatal if err != nil.
func FatalIf(t *testing.T) func(error) { func FatalIf(t *testing.T) func(error) {
return func(err error) { return func(err error) {
t.Helper()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
......
...@@ -68,6 +68,9 @@ type Client struct { ...@@ -68,6 +68,9 @@ type Client struct {
at0Initialized bool // true after .at0 is initialized at0Initialized bool // true after .at0 is initialized
at0Ready chan(struct{}) // ready after .at0 is initialized at0Ready chan(struct{}) // ready after .at0 is initialized
closeOnce sync.Once
closed chan struct{} // ready when Closed
ownNet bool // true if Client "owns" networker and should release it on Close ownNet bool // true if Client "owns" networker and should release it on Close
} }
...@@ -79,8 +82,9 @@ var _ zodb.IStorageDriver = (*Client)(nil) ...@@ -79,8 +82,9 @@ var _ zodb.IStorageDriver = (*Client)(nil)
// Use Run to actually start running the node. // Use Run to actually start running the node.
func NewClient(clusterName, masterAddr string, net xnet.Networker) *Client { func NewClient(clusterName, masterAddr string, net xnet.Networker) *Client {
c := &Client{ c := &Client{
node: newMasteredNode(proto.CLIENT, clusterName, net, masterAddr), node: newMasteredNode(proto.CLIENT, clusterName, net, masterAddr),
at0Ready: make(chan struct{}), at0Ready: make(chan struct{}),
closed: make(chan struct{}),
} }
var runCtx context.Context var runCtx context.Context
...@@ -91,6 +95,9 @@ func NewClient(clusterName, masterAddr string, net xnet.Networker) *Client { ...@@ -91,6 +95,9 @@ func NewClient(clusterName, masterAddr string, net xnet.Networker) *Client {
// Close implements zodb.IStorageDriver. // Close implements zodb.IStorageDriver.
func (c *Client) Close() (err error) { func (c *Client) Close() (err error) {
c.closeOnce.Do(func() {
close(c.closed)
})
c.runCancel() c.runCancel()
err = c.runWG.Wait() err = c.runWG.Wait()
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
...@@ -190,7 +197,7 @@ func (c *Client) invalidateObjects(msg *proto.InvalidateObjects) error { ...@@ -190,7 +197,7 @@ func (c *Client) invalidateObjects(msg *proto.InvalidateObjects) error {
defer c.at0Mu.Unlock() defer c.at0Mu.Unlock()
// queue initial events until .at0 is initialized after register // queue initial events until .at0 is initialized after register
// queued events will be sent to watchq by zeo ctor after initializing .at0 // queued events will be sent to watchq by syncMaster after initializing .at0
if !c.at0Initialized { if !c.at0Initialized {
c.eventq0 = append(c.eventq0, event) c.eventq0 = append(c.eventq0, event)
return nil return nil
...@@ -198,7 +205,13 @@ func (c *Client) invalidateObjects(msg *proto.InvalidateObjects) error { ...@@ -198,7 +205,13 @@ func (c *Client) invalidateObjects(msg *proto.InvalidateObjects) error {
// at0 is initialized - ok to send current event if it goes > at0 // at0 is initialized - ok to send current event if it goes > at0
if tid > c.at0 { if tid > c.at0 {
c.watchq <- event select {
case <-c.closed:
// closed - client does not read watchq anymore
case c.watchq <- event:
// ok
}
} }
return nil return nil
} }
...@@ -250,7 +263,13 @@ func (c *Client) flushEventq0() { ...@@ -250,7 +263,13 @@ func (c *Client) flushEventq0() {
if c.watchq != nil { if c.watchq != nil {
for _, e := range c.eventq0 { for _, e := range c.eventq0 {
if e.Tid > c.at0 { if e.Tid > c.at0 {
c.watchq <- e select {
case <-c.closed:
// closed - client does not read watchq anymore
case c.watchq <- e:
// ok
}
} }
} }
} }
...@@ -410,6 +429,8 @@ func openClientByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) ( ...@@ -410,6 +429,8 @@ func openClientByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) (
} }
cred := u.User.String() cred := u.User.String()
// ca=ca.crt;cert=my.crt;key=my.key
cred = strings.ReplaceAll(cred, ";", "&") // ; is no longer in default separators set https://github.com/golang/go/issues/25192
x, err := xurl.ParseQuery(cred) x, err := xurl.ParseQuery(cred)
if err != nil { if err != nil {
return nil, zodb.InvalidTid, fmt.Errorf("credentials: %s", err) return nil, zodb.InvalidTid, fmt.Errorf("credentials: %s", err)
...@@ -442,9 +463,7 @@ func openClientByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) ( ...@@ -442,9 +463,7 @@ func openClientByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) (
} }
name := u.Path name := u.Path
if strings.HasPrefix(name, "/") { name = strings.TrimPrefix(name, "/")
name = name[1:]
}
if name == "" { if name == "" {
return nil, zodb.InvalidTid, fmt.Errorf("cluster name not specified") return nil, zodb.InvalidTid, fmt.Errorf("cluster name not specified")
} }
...@@ -491,14 +510,18 @@ func openClientByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) ( ...@@ -491,14 +510,18 @@ func openClientByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) (
// close .watchq after serve is over // close .watchq after serve is over
c.at0Mu.Lock() c.at0Mu.Lock()
defer c.at0Mu.Unlock() defer c.at0Mu.Unlock()
if c.at0Initialized {
c.flushEventq0()
}
if c.watchq != nil { if c.watchq != nil {
if err != nil { if err != nil && /* already flushed .eventq0 */c.at0Initialized {
c.watchq <- &zodb.EventError{Err: err} select {
case <-c.closed:
// closed - client does not read watchq anymore
case c.watchq <- &zodb.EventError{Err: err}:
// ok
}
} }
close(c.watchq) close(c.watchq)
c.watchq = nil // prevent flushEventq0 to send to closed chan
} }
errq <- err errq <- err
......
...@@ -362,6 +362,7 @@ func (opt NEOSrvOptions) URLPrefix() string { ...@@ -362,6 +362,7 @@ func (opt NEOSrvOptions) URLPrefix() string {
// ---------------- // ----------------
// tOptions represents options for testing. // tOptions represents options for testing.
// TODO -> xtesting
type tOptions struct { type tOptions struct {
Preload string // preload database with data from this location Preload string // preload database with data from this location
} }
......
...@@ -76,12 +76,12 @@ type _MasteredNode struct { ...@@ -76,12 +76,12 @@ type _MasteredNode struct {
type _MasteredNodeFlags int type _MasteredNodeFlags int
const ( const (
// δPartTabPassThrough tells mlink.Recv1 not to filter out messages related // δPartTabPassThrough tells mlink.Recv1 not to filter out messages related
// to partition table changes. When mlink.Recv1 receives such messages there // to partition table changes. When mlink.Recv1 receives such messages they
// are already processed internally to update .state.PartTab correspondingly. // are already processed internally to update .state.PartTab correspondingly.
// //
// Storage uses this mode to receive δPartTab notifications to know // Storage uses this mode to receive δPartTab notifications to know
// when to persist it. // when to persist it.
δPartTabPassThrough _MasteredNodeFlags = iota δPartTabPassThrough _MasteredNodeFlags = 1 << iota
) )
// newMasteredNode creates new _MasteredNode that connects to masterAddr/cluster via net. // newMasteredNode creates new _MasteredNode that connects to masterAddr/cluster via net.
......
#!/bin/bash -e #!/bin/bash -e
# neotest: run tests and benchmarks against FileStorage, ZEO and various NEO/py and NEO/go clusters # neotest: run tests and benchmarks against FileStorage, ZEO and various NEO/py and NEO/go clusters
# Copyright (C) 2017-2020 Nexedi SA and Contributors. # Copyright (C) 2017-2021 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com> # Kirill Smelkov <kirr@nexedi.com>
# #
# This program is free software: you can Use, Study, Modify and Redistribute # This program is free software: you can Use, Study, Modify and Redistribute
...@@ -141,7 +141,10 @@ $@ ...@@ -141,7 +141,10 @@ $@
# ---- go/py unit tests ---- # ---- go/py unit tests ----
cmd_test-go() { cmd_test-go() {
go test -count=1 lab.nexedi.com/kirr/neo/go/... # -count=1 disables tests caching (
cd $NEOt
go test -count=1 lab.nexedi.com/kirr/neo/go/... # -count=1 disables tests caching
)
} }
cmd_test-py() { cmd_test-py() {
...@@ -1503,18 +1506,22 @@ cpustat) f=( );; ...@@ -1503,18 +1506,22 @@ cpustat) f=( );;
esac esac
NEOt=$(cd `dirname $0` && pwd)
for flag in ${f[*]}; do for flag in ${f[*]}; do
case "$flag" in case "$flag" in
build) build)
# make sure tzodb*, tcpu* and zgenprod are on PATH (because we could be invoked from another dir) # make sure tzodb*, tcpu* and zgenprod are on PATH (because we could be invoked from another dir)
X=$(cd `dirname $0` && pwd) export PATH=$NEOt:$PATH
export PATH=$X:$PATH
# rebuild go bits # rebuild go bits
# neo/py, wendelin.core, ... - must be pip install'ed - `neotest deploy` cares about that # neo/py, wendelin.core, ... - must be pip install'ed - `neotest deploy` cares about that
go install -v lab.nexedi.com/kirr/neo/go/... (
go build -o $X/tzodb_go $X/tzodb.go cd $NEOt
go build -o $X/tcpu_go $X/tcpu.go go install -v lab.nexedi.com/kirr/neo/go/...
go build -o tzodb_go tzodb.go
go build -o tcpu_go tcpu.go
)
;; ;;
net) net)
......
...@@ -431,7 +431,7 @@ func zwrkPreconnect(ctx context.Context, url string, at zodb.Tid, nwrk int) (_ [ ...@@ -431,7 +431,7 @@ func zwrkPreconnect(ctx context.Context, url string, at zodb.Tid, nwrk int) (_ [
if err != nil { if err != nil {
for _, stor := range storv { for _, stor := range storv {
if stor != nil { if stor != nil {
xio.LClose(stor) xio.LClose(ctx, stor)
} }
} }
return nil, err return nil, err
......
// Copyright (c) 2001, 2002 Zope Foundation and Contributors. // Copyright (c) 2001, 2002 Zope Foundation and Contributors.
// All Rights Reserved. // All Rights Reserved.
// //
// Copyright (C) 2018-2019 Nexedi SA and Contributors. // Copyright (C) 2018-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This software is subject to the provisions of the Zope Public License, // This software is subject to the provisions of the Zope Public License,
...@@ -65,7 +65,7 @@ type BTree struct { ...@@ -65,7 +65,7 @@ type BTree struct {
// Entry is one BTree node entry. // Entry is one BTree node entry.
// //
// It contains key and child, who is either BTree or Bucket. // It contains key and child, which is either BTree or Bucket.
// //
// Key limits child's keys - see BTree.Entryv for details. // Key limits child's keys - see BTree.Entryv for details.
type Entry struct { type Entry struct {
...@@ -102,6 +102,15 @@ type BucketEntry struct { ...@@ -102,6 +102,15 @@ type BucketEntry struct {
value interface{} value interface{}
} }
// KeyRange represents [lo,hi) key range.
type KeyRange struct {
Lo KEY
Hi_ KEY // NOTE _not_ hi) to avoid overflow at ∞; hi = hi_ + 1
}
const _KeyMin KEY = math.Min<Key>
const _KeyMax KEY = math.Max<Key>
// ---- access []entry ---- // ---- access []entry ----
// Key returns BTree entry key. // Key returns BTree entry key.
...@@ -123,7 +132,7 @@ func (e *Entry) Child() Node { return e.child } ...@@ -123,7 +132,7 @@ func (e *Entry) Child() Node { return e.child }
// Children of all entries are guaranteed to be of the same kind - either all BTree, or all Bucket. // Children of all entries are guaranteed to be of the same kind - either all BTree, or all Bucket.
// //
// The caller must not modify returned array. // The caller must not modify returned array.
func (t *BTree) Entryv() []Entry { func (t *BTree) Entryv() /*readonly*/ []Entry {
return t.data return t.data
} }
...@@ -134,7 +143,7 @@ func (e *BucketEntry) Key() KEY { return e.key } ...@@ -134,7 +143,7 @@ func (e *BucketEntry) Key() KEY { return e.key }
func (e *BucketEntry) Value() interface{} { return e.value } func (e *BucketEntry) Value() interface{} { return e.value }
// Entryv returns entries of a Bucket node. // Entryv returns entries of a Bucket node.
func (b *Bucket) Entryv() []BucketEntry { func (b *Bucket) Entryv() /*readonly*/ []BucketEntry {
ev := make([]BucketEntry, len(b.keys)) ev := make([]BucketEntry, len(b.keys))
for i, k := range b.keys { for i, k := range b.keys {
ev[i] = BucketEntry{k, b.values[i]} ev[i] = BucketEntry{k, b.values[i]}
...@@ -168,36 +177,54 @@ func (t *BTree) Get(ctx context.Context, key KEY) (_ interface{}, _ bool, err er ...@@ -168,36 +177,54 @@ func (t *BTree) Get(ctx context.Context, key KEY) (_ interface{}, _ bool, err er
// VGet is like Get but also calls visit while traversing the tree. // VGet is like Get but also calls visit while traversing the tree.
// //
// Visit is called with node being activated. // Visit is called with node being activated.
func (t *BTree) VGet(ctx context.Context, key KEY, visit func(node Node)) (_ interface{}, _ bool, err error) { func (t *BTree) VGet(ctx context.Context, key KEY, visit func(node Node, keycov KeyRange)) (_ interface{}, _ bool, err error) {
defer xerr.Contextf(&err, "btree(%s): get %v", t.POid(), key) defer xerr.Contextf(&err, "btree(%s): get %v", t.POid(), key)
err = t.PActivate(ctx) err = t.PActivate(ctx)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
keycov := KeyRange{Lo: _KeyMin, Hi_: _KeyMax}
if visit != nil { if visit != nil {
visit(t) visit(t, keycov)
}
if len(t.data) == 0 {
// empty btree
t.PDeactivate()
return nil, false, nil
} }
for { for {
l := len(t.data)
if l == 0 {
// empty btree (top, or in leaf)
t.PDeactivate()
return nil, false, nil
}
// search i: K(i) ≤ k < K(i+1) ; K(0) = -∞, K(len) = +∞ // search i: K(i) ≤ k < K(i+1) ; K(0) = -∞, K(len) = +∞
i := sort.Search(len(t.data), func(i int) bool { i := sort.Search(l, func(i int) bool {
j := i + 1 j := i + 1
if j == len(t.data) { if j == len(t.data) {
return true // [len].key = +∞ return true // [len].key = +∞
} }
return key < t.data[j].key return key < t.data[j].key
}) })
// i < l
// FIXME panic index out of range (empty T without children;
// logically incorrect, but Restructure generated it once)
child := t.data[i].child child := t.data[i].child
// shorten global keycov by local [lo,hi) for this child
lo := _KeyMin
if i > 0 {
lo = t.data[i].key
}
i++
hi_ := _KeyMax
if i < l {
hi_ = t.data[i].key
}
if hi_ != _KeyMax {
hi_--
}
keycov.Lo = kmax(keycov.Lo, lo)
keycov.Hi_ = kmin(keycov.Hi_, hi_)
t.PDeactivate() t.PDeactivate()
err = child.PActivate(ctx) err = child.PActivate(ctx)
if err != nil { if err != nil {
...@@ -207,7 +234,7 @@ func (t *BTree) VGet(ctx context.Context, key KEY, visit func(node Node)) (_ int ...@@ -207,7 +234,7 @@ func (t *BTree) VGet(ctx context.Context, key KEY, visit func(node Node)) (_ int
// XXX verify child keys are in valid range according to parent // XXX verify child keys are in valid range according to parent
if visit != nil { if visit != nil {
visit(child) visit(child, keycov)
} }
switch child := child.(type) { switch child := child.(type) {
...@@ -250,26 +277,40 @@ func (t *BTree) MinKey(ctx context.Context) (_ KEY, ok bool, err error) { ...@@ -250,26 +277,40 @@ func (t *BTree) MinKey(ctx context.Context) (_ KEY, ok bool, err error) {
// VMinKey is like MinKey but also calls visit while traversing the tree. // VMinKey is like MinKey but also calls visit while traversing the tree.
// //
// Visit is called with node being activated. // Visit is called with node being activated.
func (t *BTree) VMinKey(ctx context.Context, visit func(node Node)) (_ KEY, ok bool, err error) { func (t *BTree) VMinKey(ctx context.Context, visit func(node Node, keycov KeyRange)) (_ KEY, ok bool, err error) {
defer xerr.Contextf(&err, "btree(%s): minkey", t.POid()) defer xerr.Contextf(&err, "btree(%s): minkey", t.POid())
err = t.PActivate(ctx) err = t.PActivate(ctx)
if err != nil { if err != nil {
return 0, false, err return 0, false, err
} }
keycov := KeyRange{Lo: _KeyMin, Hi_: _KeyMax}
if visit != nil { if visit != nil {
visit(t) visit(t, keycov)
}
if len(t.data) == 0 {
// empty btree
t.PDeactivate()
return 0, false, nil
} }
// NOTE -> can also use t.firstbucket // NOTE -> can also use t.firstbucket
for { for {
l := len(t.data)
if l == 0 {
// empty btree (top, or in leaf)
t.PDeactivate()
return 0, false, nil
}
child := t.data[0].child child := t.data[0].child
// shorten global keycov by local hi) for this child
hi_ := _KeyMax
if 1 < l {
hi_ = t.data[1].key
}
if hi_ != _KeyMax {
hi_--
}
// keycov.Lo stays -∞
keycov.Hi_ = kmin(keycov.Hi_, hi_)
t.PDeactivate() t.PDeactivate()
err = child.PActivate(ctx) err = child.PActivate(ctx)
if err != nil { if err != nil {
...@@ -277,7 +318,7 @@ func (t *BTree) VMinKey(ctx context.Context, visit func(node Node)) (_ KEY, ok b ...@@ -277,7 +318,7 @@ func (t *BTree) VMinKey(ctx context.Context, visit func(node Node)) (_ KEY, ok b
} }
if visit != nil { if visit != nil {
visit(child) visit(child, keycov)
} }
// XXX verify child keys are in valid range according to parent // XXX verify child keys are in valid range according to parent
...@@ -305,26 +346,36 @@ func (t *BTree) MaxKey(ctx context.Context) (_ KEY, _ bool, err error) { ...@@ -305,26 +346,36 @@ func (t *BTree) MaxKey(ctx context.Context) (_ KEY, _ bool, err error) {
// VMaxKey is like MaxKey but also calls visit while traversing the tree. // VMaxKey is like MaxKey but also calls visit while traversing the tree.
// //
// Visit is called with node being activated. // Visit is called with node being activated.
func (t *BTree) VMaxKey(ctx context.Context, visit func(node Node)) (_ KEY, _ bool, err error) { func (t *BTree) VMaxKey(ctx context.Context, visit func(node Node, keycov KeyRange)) (_ KEY, _ bool, err error) {
defer xerr.Contextf(&err, "btree(%s): maxkey", t.POid()) defer xerr.Contextf(&err, "btree(%s): maxkey", t.POid())
err = t.PActivate(ctx) err = t.PActivate(ctx)
if err != nil { if err != nil {
return 0, false, err return 0, false, err
} }
keycov := KeyRange{Lo: _KeyMin, Hi_: _KeyMax}
if visit != nil { if visit != nil {
visit(t) visit(t, keycov)
}
l := len(t.data)
if l == 0 {
// empty btree
t.PDeactivate()
return 0, false, nil
} }
for { for {
l := len(t.data)
if l == 0 {
// empty btree (top, or in leaf)
t.PDeactivate()
return 0, false, nil
}
child := t.data[l-1].child child := t.data[l-1].child
// shorten global keycov by local [lo for this chile
lo := _KeyMin
if l-1 > 0 {
lo = t.data[l-1].key
}
keycov.Lo = kmax(keycov.Lo, lo)
// keycov.Hi_ stays ∞
t.PDeactivate() t.PDeactivate()
err = child.PActivate(ctx) err = child.PActivate(ctx)
if err != nil { if err != nil {
...@@ -332,7 +383,7 @@ func (t *BTree) VMaxKey(ctx context.Context, visit func(node Node)) (_ KEY, _ bo ...@@ -332,7 +383,7 @@ func (t *BTree) VMaxKey(ctx context.Context, visit func(node Node)) (_ KEY, _ bo
} }
if visit != nil { if visit != nil {
visit(child) visit(child, keycov)
} }
// XXX verify child keys are in valid range according to parent // XXX verify child keys are in valid range according to parent
...@@ -591,7 +642,7 @@ func (bt *btreeState) PySetState(pystate interface{}) (err error) { ...@@ -591,7 +642,7 @@ func (bt *btreeState) PySetState(pystate interface{}) (err error) {
var kprev int64 var kprev int64
var childrenKind int // 1 - BTree, 2 - Bucket var childrenKind int // 1 - BTree, 2 - Bucket
for i, idx := 0, 0; i < n; i++ { for i, idx := 0, 0; i < n; i++ {
key := int64(math.Min<Key>) // KEY(-∞) (qualifies for ≤) key := int64(_KeyMin) // KEY(-∞) (qualifies for ≤)
if i > 0 { if i > 0 {
// key[0] is unused and not saved // key[0] is unused and not saved
key, ok = t[idx].(int64) // XXX Xint key, ok = t[idx].(int64) // XXX Xint
...@@ -609,7 +660,7 @@ func (bt *btreeState) PySetState(pystate interface{}) (err error) { ...@@ -609,7 +660,7 @@ func (bt *btreeState) PySetState(pystate interface{}) (err error) {
} }
if i > 1 && !(key > kprev) { if i > 1 && !(key > kprev) {
fmt.Errorf("data: [%d]: key not ↑", i) return fmt.Errorf("data: [%d]: key not ↑", i)
} }
kprev = key kprev = key
...@@ -629,7 +680,7 @@ func (bt *btreeState) PySetState(pystate interface{}) (err error) { ...@@ -629,7 +680,7 @@ func (bt *btreeState) PySetState(pystate interface{}) (err error) {
childrenKind = kind childrenKind = kind
} }
if kind != childrenKind { if kind != childrenKind {
fmt.Errorf("data: [%d]: children must be of the same type", i) return fmt.Errorf("data: [%d]: children must be of the same type", i)
} }
bt.data = append(bt.data, Entry{key: kkey, child: child.(Node)}) bt.data = append(bt.data, Entry{key: kkey, child: child.(Node)})
...@@ -646,3 +697,60 @@ func init() { ...@@ -646,3 +697,60 @@ func init() {
zodb.RegisterClass("BTrees.BTree.BTree", t(BTree{}), t(btreeState{})) zodb.RegisterClass("BTrees.BTree.BTree", t(BTree{}), t(btreeState{}))
zodb.RegisterClass("BTrees.BTree.Bucket", t(Bucket{}), t(bucketState{})) zodb.RegisterClass("BTrees.BTree.Bucket", t(Bucket{}), t(bucketState{}))
} }
// ---- misc ----
// Has returns whether key k belongs to the range.
func (r *KeyRange) Has(k KEY) bool {
return (r.Lo <= k && k <= r.Hi_)
}
// Empty returns whether key range is empty.
func (r *KeyRange) Empty() bool {
hi := r.Hi_
if hi == _KeyMax {
// [x,∞] cannot be empty because max x is ∞ and [∞,∞] has one element: ∞
return false
}
hi++ // no overflow
return r.Lo >= hi
}
func (r KeyRange) String() string {
var shi string
if r.Hi_ == _KeyMax {
shi = kstr(r.Hi_) // ∞
} else {
shi = fmt.Sprintf("%d", r.Hi_+1)
}
return fmt.Sprintf("[%s,%s)", kstr(r.Lo), shi)
}
func kmin(a, b KEY) KEY {
if a < b {
return a
} else {
return b
}
}
func kmax(a, b KEY) KEY {
if a > b {
return a
} else {
return b
}
}
// kstr formats key as string.
func kstr(k KEY) string {
if k == _KeyMin {
return "-∞"
}
if k == _KeyMax {
return "∞"
}
return fmt.Sprintf("%d", k)
}
// Copyright (C) 2018-2019 Nexedi SA and Contributors. // Copyright (C) 2018-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
...@@ -23,6 +23,7 @@ package btree ...@@ -23,6 +23,7 @@ package btree
import ( import (
"context" "context"
"reflect"
"testing" "testing"
"lab.nexedi.com/kirr/go123/exc" "lab.nexedi.com/kirr/go123/exc"
...@@ -98,6 +99,12 @@ func (b *bucketWrap) MaxKey(ctx context.Context) (k int64, ok bool, err error) { ...@@ -98,6 +99,12 @@ func (b *bucketWrap) MaxKey(ctx context.Context) (k int64, ok bool, err error) {
return return
} }
// tVisit is information about one visit call.
type tVisit struct {
node zodb.Oid
keycov LKeyRange
}
func TestBTree(t *testing.T) { func TestBTree(t *testing.T) {
X := exc.Raiseif X := exc.Raiseif
ctx := context.Background() ctx := context.Background()
...@@ -105,7 +112,11 @@ func TestBTree(t *testing.T) { ...@@ -105,7 +112,11 @@ func TestBTree(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
db := zodb.NewDB(stor) defer func() {
err := stor.Close(); X(err)
}()
db := zodb.NewDB(stor, &zodb.DBOptions{})
defer func() { defer func() {
err := db.Close(); X(err) err := db.Close(); X(err)
}() }()
...@@ -118,8 +129,6 @@ func TestBTree(t *testing.T) { ...@@ -118,8 +129,6 @@ func TestBTree(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// XXX close db/stor
// go through small test Buckets/BTrees and verify that Get(key) is as expected. // go through small test Buckets/BTrees and verify that Get(key) is as expected.
for _, tt := range smallTestv { for _, tt := range smallTestv {
xobj, err := conn.Get(ctx, tt.oid) xobj, err := conn.Get(ctx, tt.oid)
...@@ -262,4 +271,39 @@ func TestBTree(t *testing.T) { ...@@ -262,4 +271,39 @@ func TestBTree(t *testing.T) {
// XXX verify FirstBucket / Next ? // XXX verify FirstBucket / Next ?
verifyFirstBucket(B3) verifyFirstBucket(B3)
// verify nodes/keycov visited through VGet/V{Min,Max}Key
xBv, err := conn.Get(ctx, Bv_oid); X(err)
Bv, ok := xBv.(*LOBTree)
if !ok {
t.Fatalf("Bv: %v; got %T; want LOBTree", Bv_oid, xBv)
}
for k, visitOK := range Bvdict {
visit := []tVisit{}
_, _, err := Bv.VGet(ctx, k, func(node LONode, keycov LKeyRange) {
visit = append(visit, tVisit{node.POid(), keycov})
}); X(err)
if !reflect.DeepEqual(visit, visitOK) {
t.Errorf("VGet(%d): visit:\nhave: %v\nwant: %v", k, visit, visitOK)
}
}
visitMinOK := Bvdict[Bv_kmin]
visitMaxOK := Bvdict[Bv_kmax]
visitMin := []tVisit{}
visitMax := []tVisit{}
_, _, err = Bv.VMinKey(ctx, func(node LONode, keycov LKeyRange) {
visitMin = append(visitMin, tVisit{node.POid(), keycov})
}); X(err)
_, _, err = Bv.VMaxKey(ctx, func(node LONode, keycov LKeyRange) {
visitMax = append(visitMax, tVisit{node.POid(), keycov})
}); X(err)
if !reflect.DeepEqual(visitMin, visitMinOK) {
t.Errorf("VMinKey(): visit:\nhave: %v\nwant: %v", visitMin, visitMinOK)
}
if !reflect.DeepEqual(visitMax, visitMaxOK) {
t.Errorf("VMaxKey(): visit:\nhave: %v\nwant: %v", visitMax, visitMaxOK)
}
} }
...@@ -28,6 +28,9 @@ out=$3 ...@@ -28,6 +28,9 @@ out=$3
kind=${KIND,,} # IO -> io kind=${KIND,,} # IO -> io
Key=${KEY^} Key=${KEY^}
KEYKIND=${KIND:0:1} # IO -> I
keykind=${KEYKIND,,} # I -> i
input=$(dirname $0)/btree.go.in input=$(dirname $0)/btree.go.in
echo "// Code generated by gen-btree; DO NOT EDIT." >$out echo "// Code generated by gen-btree; DO NOT EDIT." >$out
...@@ -45,4 +48,10 @@ sed \ ...@@ -45,4 +48,10 @@ sed \
-e "s/\bBucketEntry\b/${KIND}BucketEntry/g" \ -e "s/\bBucketEntry\b/${KIND}BucketEntry/g" \
-e "s/\bbtreeState\b/${kind}btreeState/g" \ -e "s/\bbtreeState\b/${kind}btreeState/g" \
-e "s/\bbucketState\b/${kind}bucketState/g" \ -e "s/\bbucketState\b/${kind}bucketState/g" \
-e "s/\b_KeyMin\b/_${KEYKIND}KeyMin/g" \
-e "s/\b_KeyMax\b/_${KEYKIND}KeyMax/g" \
-e "s/\bKeyRange\b/${KEYKIND}KeyRange/g" \
-e "s/\bkmin\b/${keykind}kmin/g" \
-e "s/\bkmax\b/${keykind}kmax/g" \
-e "s/\bkstr\b/${keykind}kstr/g" \
$input >>$out $input >>$out
#!/usr/bin/env python2 #!/usr/bin/env python2
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (C) 2018-2020 Nexedi SA and Contributors. # Copyright (C) 2018-2021 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com> # Kirill Smelkov <kirr@nexedi.com>
# #
# This program is free software: you can Use, Study, Modify and Redistribute # This program is free software: you can Use, Study, Modify and Redistribute
...@@ -22,8 +22,9 @@ ...@@ -22,8 +22,9 @@
from ZODB.DB import DB from ZODB.DB import DB
from BTrees.LOBTree import LOBucket, LOBTree from BTrees.LOBTree import LOBucket, LOBTree
from BTrees.check import check as bcheck
from ZODB.utils import u64 from ZODB.utils import u64
from zodbtools.test.gen_testdata import run_with_zodb3py2_compat from zodbtools.test.gen_testdata import run_with_zodb4py2_compat
import os, os.path, transaction import os, os.path, transaction
from golang.gcompat import qq from golang.gcompat import qq
...@@ -52,14 +53,37 @@ def main2(): ...@@ -52,14 +53,37 @@ def main2():
root['B3'] = B3 = LOBTree(dict([(_, _) for _ in range(10000)])) root['B3'] = B3 = LOBTree(dict([(_, _) for _ in range(10000)]))
# T4/T2-T/B1-B2-T7,9/B5-B8-B10 (to verify VGet->visit)
# TODO use xbtree.py:Restructure after gimport works through modules and
# xbtree.py is moved from wcfs to zodb/go.
v1 = LOBucket([(1,"a")])
v2 = LOBucket([(2,"b")])
v5 = LOBucket([(5,"c")])
v8 = LOBucket([(8,"d")])
v9 = LOBucket([(9,"e")])
T2, T79, T, T4 = LOBTree(), LOBTree(), LOBTree(), LOBTree()
T2.__setstate__ (((v1, 2, v2), v1)) # (child, key, child, ...), firstbucket
T79.__setstate__(((v5, 7, v8, 9, v9), v5))
T.__setstate__ (((T79,), v5))
T4.__setstate__ (((T2, 4, T), v1))
root['Bv'] = Bv = T4
transaction.commit() transaction.commit()
bcheck(Bv)
assert Bv[1] == "a"
assert Bv[2] == "b"
assert Bv[5] == "c"
assert Bv[8] == "d"
assert Bv[9] == "e"
with open("ztestdata_expect_test.go", "w") as f: with open("ztestdata_expect_test.go", "w") as f:
def emit(v): def emit(v):
print >>f, v print >>f, v
emit("// Code generated by %s; DO NOT EDIT." % __file__) emit("// Code generated by %s; DO NOT EDIT." % __file__)
emit("package btree\n") emit("package btree\n")
#emit("import \"lab.nexedi.com/kirr/neo/go/zodb\"\n")
def emititems(b): def emititems(b):
s = "testEntry{oid: %s, kind: %s, itemv: []kv{" \ s = "testEntry{oid: %s, kind: %s, itemv: []kv{" \
...@@ -84,12 +108,34 @@ def main2(): ...@@ -84,12 +108,34 @@ def main2():
emit("\nconst B3_oid = %s" % u64(B3._p_oid)) emit("\nconst B3_oid = %s" % u64(B3._p_oid))
emit("const B3_maxkey = %d" % B3.maxKey()) emit("const B3_maxkey = %d" % B3.maxKey())
emit("\nconst Bv_oid = %s" % u64(Bv._p_oid))
emit("const Bv_kmin = %d" % Bv.minKey())
emit("const Bv_kmax = %d" % Bv.maxKey())
emit("var Bvdict = map[int64][]tVisit{")
noo = "_LKeyMin"
oo = "_LKeyMax"
def emitVisit(key, *visitv): # visitv = [](node, lo,hi)
vstr = []
for node, lo,hi in visitv:
if isinstance(hi, str):
hi_ = hi # oo or noo
else:
hi_ = hi-1
vstr.append("{%d, LKeyRange{%s, %s}}" % (u64(node._p_oid), lo, hi_))
emit("\t%d: []tVisit{%s}," % (key, ", ".join(vstr)))
emitVisit(1, (T4, noo,oo), (T2, noo,4), (v1, noo,2))
emitVisit(2, (T4, noo,oo), (T2, noo,4), (v2, 2,4))
emitVisit(5, (T4, noo,oo), (T, 4,oo), (T79, 4,oo), (v5, 4,7))
emitVisit(8, (T4, noo,oo), (T, 4,oo), (T79, 4,oo), (v8, 7,9))
emitVisit(9, (T4, noo,oo), (T, 4,oo), (T79, 4,oo), (v9, 9,oo))
emit("}")
conn.close() conn.close()
db.close() db.close()
def main(): def main():
run_with_zodb3py2_compat(main2) run_with_zodb4py2_compat(main2)
if __name__ == '__main__': if __name__ == '__main__':
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// Copyright (c) 2001, 2002 Zope Foundation and Contributors. // Copyright (c) 2001, 2002 Zope Foundation and Contributors.
// All Rights Reserved. // All Rights Reserved.
// //
// Copyright (C) 2018-2019 Nexedi SA and Contributors. // Copyright (C) 2018-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This software is subject to the provisions of the Zope Public License, // This software is subject to the provisions of the Zope Public License,
...@@ -67,7 +67,7 @@ type IOBTree struct { ...@@ -67,7 +67,7 @@ type IOBTree struct {
// IOEntry is one IOBTree node entry. // IOEntry is one IOBTree node entry.
// //
// It contains key and child, who is either IOBTree or IOBucket. // It contains key and child, which is either IOBTree or IOBucket.
// //
// Key limits child's keys - see IOBTree.Entryv for details. // Key limits child's keys - see IOBTree.Entryv for details.
type IOEntry struct { type IOEntry struct {
...@@ -104,6 +104,15 @@ type IOBucketEntry struct { ...@@ -104,6 +104,15 @@ type IOBucketEntry struct {
value interface{} value interface{}
} }
// IKeyRange represents [lo,hi) key range.
type IKeyRange struct {
Lo int32
Hi_ int32 // NOTE _not_ hi) to avoid overflow at ∞; hi = hi_ + 1
}
const _IKeyMin int32 = math.MinInt32
const _IKeyMax int32 = math.MaxInt32
// ---- access []entry ---- // ---- access []entry ----
// Key returns IOBTree entry key. // Key returns IOBTree entry key.
...@@ -125,7 +134,7 @@ func (e *IOEntry) Child() IONode { return e.child } ...@@ -125,7 +134,7 @@ func (e *IOEntry) Child() IONode { return e.child }
// Children of all entries are guaranteed to be of the same kind - either all IOBTree, or all IOBucket. // Children of all entries are guaranteed to be of the same kind - either all IOBTree, or all IOBucket.
// //
// The caller must not modify returned array. // The caller must not modify returned array.
func (t *IOBTree) Entryv() []IOEntry { func (t *IOBTree) Entryv() /*readonly*/ []IOEntry {
return t.data return t.data
} }
...@@ -136,7 +145,7 @@ func (e *IOBucketEntry) Key() int32 { return e.key } ...@@ -136,7 +145,7 @@ func (e *IOBucketEntry) Key() int32 { return e.key }
func (e *IOBucketEntry) Value() interface{} { return e.value } func (e *IOBucketEntry) Value() interface{} { return e.value }
// Entryv returns entries of a IOBucket node. // Entryv returns entries of a IOBucket node.
func (b *IOBucket) Entryv() []IOBucketEntry { func (b *IOBucket) Entryv() /*readonly*/ []IOBucketEntry {
ev := make([]IOBucketEntry, len(b.keys)) ev := make([]IOBucketEntry, len(b.keys))
for i, k := range b.keys { for i, k := range b.keys {
ev[i] = IOBucketEntry{k, b.values[i]} ev[i] = IOBucketEntry{k, b.values[i]}
...@@ -170,34 +179,54 @@ func (t *IOBTree) Get(ctx context.Context, key int32) (_ interface{}, _ bool, er ...@@ -170,34 +179,54 @@ func (t *IOBTree) Get(ctx context.Context, key int32) (_ interface{}, _ bool, er
// VGet is like Get but also calls visit while traversing the tree. // VGet is like Get but also calls visit while traversing the tree.
// //
// Visit is called with node being activated. // Visit is called with node being activated.
func (t *IOBTree) VGet(ctx context.Context, key int32, visit func(node IONode)) (_ interface{}, _ bool, err error) { func (t *IOBTree) VGet(ctx context.Context, key int32, visit func(node IONode, keycov IKeyRange)) (_ interface{}, _ bool, err error) {
defer xerr.Contextf(&err, "btree(%s): get %v", t.POid(), key) defer xerr.Contextf(&err, "btree(%s): get %v", t.POid(), key)
err = t.PActivate(ctx) err = t.PActivate(ctx)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
keycov := IKeyRange{Lo: _IKeyMin, Hi_: _IKeyMax}
if visit != nil { if visit != nil {
visit(t) visit(t, keycov)
}
if len(t.data) == 0 {
// empty btree
t.PDeactivate()
return nil, false, nil
} }
for { for {
l := len(t.data)
if l == 0 {
// empty btree (top, or in leaf)
t.PDeactivate()
return nil, false, nil
}
// search i: K(i) ≤ k < K(i+1) ; K(0) = -∞, K(len) = +∞ // search i: K(i) ≤ k < K(i+1) ; K(0) = -∞, K(len) = +∞
i := sort.Search(len(t.data), func(i int) bool { i := sort.Search(l, func(i int) bool {
j := i + 1 j := i + 1
if j == len(t.data) { if j == len(t.data) {
return true // [len].key = +∞ return true // [len].key = +∞
} }
return key < t.data[j].key return key < t.data[j].key
}) })
// i < l
child := t.data[i].child child := t.data[i].child
// shorten global keycov by local [lo,hi) for this child
lo := _IKeyMin
if i > 0 {
lo = t.data[i].key
}
i++
hi_ := _IKeyMax
if i < l {
hi_ = t.data[i].key
}
if hi_ != _IKeyMax {
hi_--
}
keycov.Lo = ikmax(keycov.Lo, lo)
keycov.Hi_ = ikmin(keycov.Hi_, hi_)
t.PDeactivate() t.PDeactivate()
err = child.PActivate(ctx) err = child.PActivate(ctx)
if err != nil { if err != nil {
...@@ -207,7 +236,7 @@ func (t *IOBTree) VGet(ctx context.Context, key int32, visit func(node IONode)) ...@@ -207,7 +236,7 @@ func (t *IOBTree) VGet(ctx context.Context, key int32, visit func(node IONode))
// XXX verify child keys are in valid range according to parent // XXX verify child keys are in valid range according to parent
if visit != nil { if visit != nil {
visit(child) visit(child, keycov)
} }
switch child := child.(type) { switch child := child.(type) {
...@@ -250,26 +279,40 @@ func (t *IOBTree) MinKey(ctx context.Context) (_ int32, ok bool, err error) { ...@@ -250,26 +279,40 @@ func (t *IOBTree) MinKey(ctx context.Context) (_ int32, ok bool, err error) {
// VMinKey is like MinKey but also calls visit while traversing the tree. // VMinKey is like MinKey but also calls visit while traversing the tree.
// //
// Visit is called with node being activated. // Visit is called with node being activated.
func (t *IOBTree) VMinKey(ctx context.Context, visit func(node IONode)) (_ int32, ok bool, err error) { func (t *IOBTree) VMinKey(ctx context.Context, visit func(node IONode, keycov IKeyRange)) (_ int32, ok bool, err error) {
defer xerr.Contextf(&err, "btree(%s): minkey", t.POid()) defer xerr.Contextf(&err, "btree(%s): minkey", t.POid())
err = t.PActivate(ctx) err = t.PActivate(ctx)
if err != nil { if err != nil {
return 0, false, err return 0, false, err
} }
keycov := IKeyRange{Lo: _IKeyMin, Hi_: _IKeyMax}
if visit != nil { if visit != nil {
visit(t) visit(t, keycov)
}
if len(t.data) == 0 {
// empty btree
t.PDeactivate()
return 0, false, nil
} }
// NOTE -> can also use t.firstbucket // NOTE -> can also use t.firstbucket
for { for {
l := len(t.data)
if l == 0 {
// empty btree (top, or in leaf)
t.PDeactivate()
return 0, false, nil
}
child := t.data[0].child child := t.data[0].child
// shorten global keycov by local hi) for this child
hi_ := _IKeyMax
if 1 < l {
hi_ = t.data[1].key
}
if hi_ != _IKeyMax {
hi_--
}
// keycov.Lo stays -∞
keycov.Hi_ = ikmin(keycov.Hi_, hi_)
t.PDeactivate() t.PDeactivate()
err = child.PActivate(ctx) err = child.PActivate(ctx)
if err != nil { if err != nil {
...@@ -277,7 +320,7 @@ func (t *IOBTree) VMinKey(ctx context.Context, visit func(node IONode)) (_ int32 ...@@ -277,7 +320,7 @@ func (t *IOBTree) VMinKey(ctx context.Context, visit func(node IONode)) (_ int32
} }
if visit != nil { if visit != nil {
visit(child) visit(child, keycov)
} }
// XXX verify child keys are in valid range according to parent // XXX verify child keys are in valid range according to parent
...@@ -305,26 +348,36 @@ func (t *IOBTree) MaxKey(ctx context.Context) (_ int32, _ bool, err error) { ...@@ -305,26 +348,36 @@ func (t *IOBTree) MaxKey(ctx context.Context) (_ int32, _ bool, err error) {
// VMaxKey is like MaxKey but also calls visit while traversing the tree. // VMaxKey is like MaxKey but also calls visit while traversing the tree.
// //
// Visit is called with node being activated. // Visit is called with node being activated.
func (t *IOBTree) VMaxKey(ctx context.Context, visit func(node IONode)) (_ int32, _ bool, err error) { func (t *IOBTree) VMaxKey(ctx context.Context, visit func(node IONode, keycov IKeyRange)) (_ int32, _ bool, err error) {
defer xerr.Contextf(&err, "btree(%s): maxkey", t.POid()) defer xerr.Contextf(&err, "btree(%s): maxkey", t.POid())
err = t.PActivate(ctx) err = t.PActivate(ctx)
if err != nil { if err != nil {
return 0, false, err return 0, false, err
} }
keycov := IKeyRange{Lo: _IKeyMin, Hi_: _IKeyMax}
if visit != nil { if visit != nil {
visit(t) visit(t, keycov)
}
l := len(t.data)
if l == 0 {
// empty btree
t.PDeactivate()
return 0, false, nil
} }
for { for {
l := len(t.data)
if l == 0 {
// empty btree (top, or in leaf)
t.PDeactivate()
return 0, false, nil
}
child := t.data[l-1].child child := t.data[l-1].child
// shorten global keycov by local [lo for this chile
lo := _IKeyMin
if l-1 > 0 {
lo = t.data[l-1].key
}
keycov.Lo = ikmax(keycov.Lo, lo)
// keycov.Hi_ stays ∞
t.PDeactivate() t.PDeactivate()
err = child.PActivate(ctx) err = child.PActivate(ctx)
if err != nil { if err != nil {
...@@ -332,7 +385,7 @@ func (t *IOBTree) VMaxKey(ctx context.Context, visit func(node IONode)) (_ int32 ...@@ -332,7 +385,7 @@ func (t *IOBTree) VMaxKey(ctx context.Context, visit func(node IONode)) (_ int32
} }
if visit != nil { if visit != nil {
visit(child) visit(child, keycov)
} }
// XXX verify child keys are in valid range according to parent // XXX verify child keys are in valid range according to parent
...@@ -591,7 +644,7 @@ func (bt *iobtreeState) PySetState(pystate interface{}) (err error) { ...@@ -591,7 +644,7 @@ func (bt *iobtreeState) PySetState(pystate interface{}) (err error) {
var kprev int64 var kprev int64
var childrenKind int // 1 - IOBTree, 2 - IOBucket var childrenKind int // 1 - IOBTree, 2 - IOBucket
for i, idx := 0, 0; i < n; i++ { for i, idx := 0, 0; i < n; i++ {
key := int64(math.MinInt32) // int32(-∞) (qualifies for ≤) key := int64(_IKeyMin) // int32(-∞) (qualifies for ≤)
if i > 0 { if i > 0 {
// key[0] is unused and not saved // key[0] is unused and not saved
key, ok = t[idx].(int64) // XXX Xint key, ok = t[idx].(int64) // XXX Xint
...@@ -609,7 +662,7 @@ func (bt *iobtreeState) PySetState(pystate interface{}) (err error) { ...@@ -609,7 +662,7 @@ func (bt *iobtreeState) PySetState(pystate interface{}) (err error) {
} }
if i > 1 && !(key > kprev) { if i > 1 && !(key > kprev) {
fmt.Errorf("data: [%d]: key not ↑", i) return fmt.Errorf("data: [%d]: key not ↑", i)
} }
kprev = key kprev = key
...@@ -629,7 +682,7 @@ func (bt *iobtreeState) PySetState(pystate interface{}) (err error) { ...@@ -629,7 +682,7 @@ func (bt *iobtreeState) PySetState(pystate interface{}) (err error) {
childrenKind = kind childrenKind = kind
} }
if kind != childrenKind { if kind != childrenKind {
fmt.Errorf("data: [%d]: children must be of the same type", i) return fmt.Errorf("data: [%d]: children must be of the same type", i)
} }
bt.data = append(bt.data, IOEntry{key: kkey, child: child.(IONode)}) bt.data = append(bt.data, IOEntry{key: kkey, child: child.(IONode)})
...@@ -646,3 +699,60 @@ func init() { ...@@ -646,3 +699,60 @@ func init() {
zodb.RegisterClass("BTrees.IOBTree.IOBTree", t(IOBTree{}), t(iobtreeState{})) zodb.RegisterClass("BTrees.IOBTree.IOBTree", t(IOBTree{}), t(iobtreeState{}))
zodb.RegisterClass("BTrees.IOBTree.IOBucket", t(IOBucket{}), t(iobucketState{})) zodb.RegisterClass("BTrees.IOBTree.IOBucket", t(IOBucket{}), t(iobucketState{}))
} }
// ---- misc ----
// Has returns whether key k belongs to the range.
func (r *IKeyRange) Has(k int32) bool {
return (r.Lo <= k && k <= r.Hi_)
}
// Empty returns whether key range is empty.
func (r *IKeyRange) Empty() bool {
hi := r.Hi_
if hi == _IKeyMax {
// [x,∞] cannot be empty because max x is ∞ and [∞,∞] has one element: ∞
return false
}
hi++ // no overflow
return r.Lo >= hi
}
func (r IKeyRange) String() string {
var shi string
if r.Hi_ == _IKeyMax {
shi = ikstr(r.Hi_) // ∞
} else {
shi = fmt.Sprintf("%d", r.Hi_+1)
}
return fmt.Sprintf("[%s,%s)", ikstr(r.Lo), shi)
}
func ikmin(a, b int32) int32 {
if a < b {
return a
} else {
return b
}
}
func ikmax(a, b int32) int32 {
if a > b {
return a
} else {
return b
}
}
// ikstr formats key as string.
func ikstr(k int32) string {
if k == _IKeyMin {
return "-∞"
}
if k == _IKeyMax {
return "∞"
}
return fmt.Sprintf("%d", k)
}
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// Copyright (c) 2001, 2002 Zope Foundation and Contributors. // Copyright (c) 2001, 2002 Zope Foundation and Contributors.
// All Rights Reserved. // All Rights Reserved.
// //
// Copyright (C) 2018-2019 Nexedi SA and Contributors. // Copyright (C) 2018-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This software is subject to the provisions of the Zope Public License, // This software is subject to the provisions of the Zope Public License,
...@@ -67,7 +67,7 @@ type LOBTree struct { ...@@ -67,7 +67,7 @@ type LOBTree struct {
// LOEntry is one LOBTree node entry. // LOEntry is one LOBTree node entry.
// //
// It contains key and child, who is either LOBTree or LOBucket. // It contains key and child, which is either LOBTree or LOBucket.
// //
// Key limits child's keys - see LOBTree.Entryv for details. // Key limits child's keys - see LOBTree.Entryv for details.
type LOEntry struct { type LOEntry struct {
...@@ -104,6 +104,15 @@ type LOBucketEntry struct { ...@@ -104,6 +104,15 @@ type LOBucketEntry struct {
value interface{} value interface{}
} }
// LKeyRange represents [lo,hi) key range.
type LKeyRange struct {
Lo int64
Hi_ int64 // NOTE _not_ hi) to avoid overflow at ∞; hi = hi_ + 1
}
const _LKeyMin int64 = math.MinInt64
const _LKeyMax int64 = math.MaxInt64
// ---- access []entry ---- // ---- access []entry ----
// Key returns LOBTree entry key. // Key returns LOBTree entry key.
...@@ -125,7 +134,7 @@ func (e *LOEntry) Child() LONode { return e.child } ...@@ -125,7 +134,7 @@ func (e *LOEntry) Child() LONode { return e.child }
// Children of all entries are guaranteed to be of the same kind - either all LOBTree, or all LOBucket. // Children of all entries are guaranteed to be of the same kind - either all LOBTree, or all LOBucket.
// //
// The caller must not modify returned array. // The caller must not modify returned array.
func (t *LOBTree) Entryv() []LOEntry { func (t *LOBTree) Entryv() /*readonly*/ []LOEntry {
return t.data return t.data
} }
...@@ -136,7 +145,7 @@ func (e *LOBucketEntry) Key() int64 { return e.key } ...@@ -136,7 +145,7 @@ func (e *LOBucketEntry) Key() int64 { return e.key }
func (e *LOBucketEntry) Value() interface{} { return e.value } func (e *LOBucketEntry) Value() interface{} { return e.value }
// Entryv returns entries of a LOBucket node. // Entryv returns entries of a LOBucket node.
func (b *LOBucket) Entryv() []LOBucketEntry { func (b *LOBucket) Entryv() /*readonly*/ []LOBucketEntry {
ev := make([]LOBucketEntry, len(b.keys)) ev := make([]LOBucketEntry, len(b.keys))
for i, k := range b.keys { for i, k := range b.keys {
ev[i] = LOBucketEntry{k, b.values[i]} ev[i] = LOBucketEntry{k, b.values[i]}
...@@ -170,36 +179,54 @@ func (t *LOBTree) Get(ctx context.Context, key int64) (_ interface{}, _ bool, er ...@@ -170,36 +179,54 @@ func (t *LOBTree) Get(ctx context.Context, key int64) (_ interface{}, _ bool, er
// VGet is like Get but also calls visit while traversing the tree. // VGet is like Get but also calls visit while traversing the tree.
// //
// Visit is called with node being activated. // Visit is called with node being activated.
func (t *LOBTree) VGet(ctx context.Context, key int64, visit func(node LONode)) (_ interface{}, _ bool, err error) { func (t *LOBTree) VGet(ctx context.Context, key int64, visit func(node LONode, keycov LKeyRange)) (_ interface{}, _ bool, err error) {
defer xerr.Contextf(&err, "btree(%s): get %v", t.POid(), key) defer xerr.Contextf(&err, "btree(%s): get %v", t.POid(), key)
err = t.PActivate(ctx) err = t.PActivate(ctx)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
keycov := LKeyRange{Lo: _LKeyMin, Hi_: _LKeyMax}
if visit != nil { if visit != nil {
visit(t) visit(t, keycov)
}
if len(t.data) == 0 {
// empty btree
t.PDeactivate()
return nil, false, nil
} }
for { for {
l := len(t.data)
if l == 0 {
// empty btree (top, or in leaf)
t.PDeactivate()
return nil, false, nil
}
// search i: K(i) ≤ k < K(i+1) ; K(0) = -∞, K(len) = +∞ // search i: K(i) ≤ k < K(i+1) ; K(0) = -∞, K(len) = +∞
i := sort.Search(len(t.data), func(i int) bool { i := sort.Search(l, func(i int) bool {
j := i + 1 j := i + 1
if j == len(t.data) { if j == len(t.data) {
return true // [len].key = +∞ return true // [len].key = +∞
} }
return key < t.data[j].key return key < t.data[j].key
}) })
// i < l
// FIXME panic index out of range (empty T without children;
// logically incorrect, but Restructure generated it once)
child := t.data[i].child child := t.data[i].child
// shorten global keycov by local [lo,hi) for this child
lo := _LKeyMin
if i > 0 {
lo = t.data[i].key
}
i++
hi_ := _LKeyMax
if i < l {
hi_ = t.data[i].key
}
if hi_ != _LKeyMax {
hi_--
}
keycov.Lo = lkmax(keycov.Lo, lo)
keycov.Hi_ = lkmin(keycov.Hi_, hi_)
t.PDeactivate() t.PDeactivate()
err = child.PActivate(ctx) err = child.PActivate(ctx)
if err != nil { if err != nil {
...@@ -209,7 +236,7 @@ func (t *LOBTree) VGet(ctx context.Context, key int64, visit func(node LONode)) ...@@ -209,7 +236,7 @@ func (t *LOBTree) VGet(ctx context.Context, key int64, visit func(node LONode))
// XXX verify child keys are in valid range according to parent // XXX verify child keys are in valid range according to parent
if visit != nil { if visit != nil {
visit(child) visit(child, keycov)
} }
switch child := child.(type) { switch child := child.(type) {
...@@ -252,26 +279,40 @@ func (t *LOBTree) MinKey(ctx context.Context) (_ int64, ok bool, err error) { ...@@ -252,26 +279,40 @@ func (t *LOBTree) MinKey(ctx context.Context) (_ int64, ok bool, err error) {
// VMinKey is like MinKey but also calls visit while traversing the tree. // VMinKey is like MinKey but also calls visit while traversing the tree.
// //
// Visit is called with node being activated. // Visit is called with node being activated.
func (t *LOBTree) VMinKey(ctx context.Context, visit func(node LONode)) (_ int64, ok bool, err error) { func (t *LOBTree) VMinKey(ctx context.Context, visit func(node LONode, keycov LKeyRange)) (_ int64, ok bool, err error) {
defer xerr.Contextf(&err, "btree(%s): minkey", t.POid()) defer xerr.Contextf(&err, "btree(%s): minkey", t.POid())
err = t.PActivate(ctx) err = t.PActivate(ctx)
if err != nil { if err != nil {
return 0, false, err return 0, false, err
} }
keycov := LKeyRange{Lo: _LKeyMin, Hi_: _LKeyMax}
if visit != nil { if visit != nil {
visit(t) visit(t, keycov)
}
if len(t.data) == 0 {
// empty btree
t.PDeactivate()
return 0, false, nil
} }
// NOTE -> can also use t.firstbucket // NOTE -> can also use t.firstbucket
for { for {
l := len(t.data)
if l == 0 {
// empty btree (top, or in leaf)
t.PDeactivate()
return 0, false, nil
}
child := t.data[0].child child := t.data[0].child
// shorten global keycov by local hi) for this child
hi_ := _LKeyMax
if 1 < l {
hi_ = t.data[1].key
}
if hi_ != _LKeyMax {
hi_--
}
// keycov.Lo stays -∞
keycov.Hi_ = lkmin(keycov.Hi_, hi_)
t.PDeactivate() t.PDeactivate()
err = child.PActivate(ctx) err = child.PActivate(ctx)
if err != nil { if err != nil {
...@@ -279,7 +320,7 @@ func (t *LOBTree) VMinKey(ctx context.Context, visit func(node LONode)) (_ int64 ...@@ -279,7 +320,7 @@ func (t *LOBTree) VMinKey(ctx context.Context, visit func(node LONode)) (_ int64
} }
if visit != nil { if visit != nil {
visit(child) visit(child, keycov)
} }
// XXX verify child keys are in valid range according to parent // XXX verify child keys are in valid range according to parent
...@@ -307,26 +348,36 @@ func (t *LOBTree) MaxKey(ctx context.Context) (_ int64, _ bool, err error) { ...@@ -307,26 +348,36 @@ func (t *LOBTree) MaxKey(ctx context.Context) (_ int64, _ bool, err error) {
// VMaxKey is like MaxKey but also calls visit while traversing the tree. // VMaxKey is like MaxKey but also calls visit while traversing the tree.
// //
// Visit is called with node being activated. // Visit is called with node being activated.
func (t *LOBTree) VMaxKey(ctx context.Context, visit func(node LONode)) (_ int64, _ bool, err error) { func (t *LOBTree) VMaxKey(ctx context.Context, visit func(node LONode, keycov LKeyRange)) (_ int64, _ bool, err error) {
defer xerr.Contextf(&err, "btree(%s): maxkey", t.POid()) defer xerr.Contextf(&err, "btree(%s): maxkey", t.POid())
err = t.PActivate(ctx) err = t.PActivate(ctx)
if err != nil { if err != nil {
return 0, false, err return 0, false, err
} }
keycov := LKeyRange{Lo: _LKeyMin, Hi_: _LKeyMax}
if visit != nil { if visit != nil {
visit(t) visit(t, keycov)
}
l := len(t.data)
if l == 0 {
// empty btree
t.PDeactivate()
return 0, false, nil
} }
for { for {
l := len(t.data)
if l == 0 {
// empty btree (top, or in leaf)
t.PDeactivate()
return 0, false, nil
}
child := t.data[l-1].child child := t.data[l-1].child
// shorten global keycov by local [lo for this chile
lo := _LKeyMin
if l-1 > 0 {
lo = t.data[l-1].key
}
keycov.Lo = lkmax(keycov.Lo, lo)
// keycov.Hi_ stays ∞
t.PDeactivate() t.PDeactivate()
err = child.PActivate(ctx) err = child.PActivate(ctx)
if err != nil { if err != nil {
...@@ -334,7 +385,7 @@ func (t *LOBTree) VMaxKey(ctx context.Context, visit func(node LONode)) (_ int64 ...@@ -334,7 +385,7 @@ func (t *LOBTree) VMaxKey(ctx context.Context, visit func(node LONode)) (_ int64
} }
if visit != nil { if visit != nil {
visit(child) visit(child, keycov)
} }
// XXX verify child keys are in valid range according to parent // XXX verify child keys are in valid range according to parent
...@@ -593,7 +644,7 @@ func (bt *lobtreeState) PySetState(pystate interface{}) (err error) { ...@@ -593,7 +644,7 @@ func (bt *lobtreeState) PySetState(pystate interface{}) (err error) {
var kprev int64 var kprev int64
var childrenKind int // 1 - LOBTree, 2 - LOBucket var childrenKind int // 1 - LOBTree, 2 - LOBucket
for i, idx := 0, 0; i < n; i++ { for i, idx := 0, 0; i < n; i++ {
key := int64(math.MinInt64) // int64(-∞) (qualifies for ≤) key := int64(_LKeyMin) // int64(-∞) (qualifies for ≤)
if i > 0 { if i > 0 {
// key[0] is unused and not saved // key[0] is unused and not saved
key, ok = t[idx].(int64) // XXX Xint key, ok = t[idx].(int64) // XXX Xint
...@@ -611,7 +662,7 @@ func (bt *lobtreeState) PySetState(pystate interface{}) (err error) { ...@@ -611,7 +662,7 @@ func (bt *lobtreeState) PySetState(pystate interface{}) (err error) {
} }
if i > 1 && !(key > kprev) { if i > 1 && !(key > kprev) {
fmt.Errorf("data: [%d]: key not ↑", i) return fmt.Errorf("data: [%d]: key not ↑", i)
} }
kprev = key kprev = key
...@@ -631,7 +682,7 @@ func (bt *lobtreeState) PySetState(pystate interface{}) (err error) { ...@@ -631,7 +682,7 @@ func (bt *lobtreeState) PySetState(pystate interface{}) (err error) {
childrenKind = kind childrenKind = kind
} }
if kind != childrenKind { if kind != childrenKind {
fmt.Errorf("data: [%d]: children must be of the same type", i) return fmt.Errorf("data: [%d]: children must be of the same type", i)
} }
bt.data = append(bt.data, LOEntry{key: kkey, child: child.(LONode)}) bt.data = append(bt.data, LOEntry{key: kkey, child: child.(LONode)})
...@@ -648,3 +699,60 @@ func init() { ...@@ -648,3 +699,60 @@ func init() {
zodb.RegisterClass("BTrees.LOBTree.LOBTree", t(LOBTree{}), t(lobtreeState{})) zodb.RegisterClass("BTrees.LOBTree.LOBTree", t(LOBTree{}), t(lobtreeState{}))
zodb.RegisterClass("BTrees.LOBTree.LOBucket", t(LOBucket{}), t(lobucketState{})) zodb.RegisterClass("BTrees.LOBTree.LOBucket", t(LOBucket{}), t(lobucketState{}))
} }
// ---- misc ----
// Has returns whether key k belongs to the range.
func (r *LKeyRange) Has(k int64) bool {
return (r.Lo <= k && k <= r.Hi_)
}
// Empty returns whether key range is empty.
func (r *LKeyRange) Empty() bool {
hi := r.Hi_
if hi == _LKeyMax {
// [x,∞] cannot be empty because max x is ∞ and [∞,∞] has one element: ∞
return false
}
hi++ // no overflow
return r.Lo >= hi
}
func (r LKeyRange) String() string {
var shi string
if r.Hi_ == _LKeyMax {
shi = lkstr(r.Hi_) // ∞
} else {
shi = fmt.Sprintf("%d", r.Hi_+1)
}
return fmt.Sprintf("[%s,%s)", lkstr(r.Lo), shi)
}
func lkmin(a, b int64) int64 {
if a < b {
return a
} else {
return b
}
}
func lkmax(a, b int64) int64 {
if a > b {
return a
} else {
return b
}
}
// lkstr formats key as string.
func lkstr(k int64) string {
if k == _LKeyMin {
return "-∞"
}
if k == _LKeyMax {
return "∞"
}
return fmt.Sprintf("%d", k)
}
...@@ -3,13 +3,24 @@ package btree ...@@ -3,13 +3,24 @@ package btree
var smallTestv = [...]testEntry{ var smallTestv = [...]testEntry{
testEntry{oid: 6, kind: kindBucket, itemv: []kv{}}, testEntry{oid: 7, kind: kindBucket, itemv: []kv{}},
testEntry{oid: 3, kind: kindBucket, itemv: []kv{{10, int64(17)}, }}, testEntry{oid: 4, kind: kindBucket, itemv: []kv{{10, int64(17)}, }},
testEntry{oid: 1, kind: kindBucket, itemv: []kv{{15, int64(1)}, {23, "hello"}, }}, testEntry{oid: 1, kind: kindBucket, itemv: []kv{{15, int64(1)}, {23, "hello"}, }},
testEntry{oid: 2, kind: kindBTree, itemv: []kv{}}, testEntry{oid: 3, kind: kindBTree, itemv: []kv{}},
testEntry{oid: 7, kind: kindBTree, itemv: []kv{{5, int64(4)}, }}, testEntry{oid: 8, kind: kindBTree, itemv: []kv{{5, int64(4)}, }},
testEntry{oid: 4, kind: kindBTree, itemv: []kv{{7, int64(3)}, {9, "world"}, }}, testEntry{oid: 5, kind: kindBTree, itemv: []kv{{7, int64(3)}, {9, "world"}, }},
} }
const B3_oid = 5 const B3_oid = 6
const B3_maxkey = 9999 const B3_maxkey = 9999
const Bv_oid = 2
const Bv_kmin = 1
const Bv_kmax = 9
var Bvdict = map[int64][]tVisit{
1: []tVisit{{2, LKeyRange{_LKeyMin, _LKeyMax}}, {342, LKeyRange{_LKeyMin, 3}}, {344, LKeyRange{_LKeyMin, 1}}},
2: []tVisit{{2, LKeyRange{_LKeyMin, _LKeyMax}}, {342, LKeyRange{_LKeyMin, 3}}, {349, LKeyRange{2, 3}}},
5: []tVisit{{2, LKeyRange{_LKeyMin, _LKeyMax}}, {343, LKeyRange{4, _LKeyMax}}, {345, LKeyRange{4, _LKeyMax}}, {346, LKeyRange{4, 6}}},
8: []tVisit{{2, LKeyRange{_LKeyMin, _LKeyMax}}, {343, LKeyRange{4, _LKeyMax}}, {345, LKeyRange{4, _LKeyMax}}, {347, LKeyRange{7, 8}}},
9: []tVisit{{2, LKeyRange{_LKeyMin, _LKeyMax}}, {343, LKeyRange{4, _LKeyMax}}, {345, LKeyRange{4, _LKeyMax}}, {348, LKeyRange{9, _LKeyMax}}},
}
// Copyright (C) 2018-2020 Nexedi SA and Contributors. // Copyright (C) 2018-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
...@@ -180,7 +180,7 @@ const ( ...@@ -180,7 +180,7 @@ const (
// newConnection creates new Connection associated with db. // newConnection creates new Connection associated with db.
func newConnection(db *DB, at Tid) *Connection { func newConnection(db *DB, at Tid) *Connection {
return &Connection{ conn := &Connection{
db: db, db: db,
at: at, at: at,
cache: LiveCache{ cache: LiveCache{
...@@ -188,6 +188,10 @@ func newConnection(db *DB, at Tid) *Connection { ...@@ -188,6 +188,10 @@ func newConnection(db *DB, at Tid) *Connection {
objtab: make(map[Oid]*weak.Ref), objtab: make(map[Oid]*weak.Ref),
}, },
} }
if cc := db.opt.CacheControl; cc != nil {
conn.cache.SetControl(cc)
}
return conn
} }
// DB returns database handle under which the connection was opened. // DB returns database handle under which the connection was opened.
......
// Copyright (C) 2018-2020 Nexedi SA and Contributors. // Copyright (C) 2018-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
...@@ -48,6 +48,8 @@ type DB struct { ...@@ -48,6 +48,8 @@ type DB struct {
stor IStorage stor IStorage
watchq chan Event // we are watching .stor via here watchq chan Event // we are watching .stor via here
opt *DBOptions
down chan struct{} // ready when DB is no longer operational down chan struct{} // ready when DB is no longer operational
downOnce sync.Once // shutdown may be due to both Close and IO error in watcher downOnce sync.Once // shutdown may be due to both Close and IO error in watcher
downErr error // reason for shutdown downErr error // reason for shutdown
...@@ -121,15 +123,23 @@ type DB struct { ...@@ -121,15 +123,23 @@ type DB struct {
// (so it is not duplicated many times for many DB case) // (so it is not duplicated many times for many DB case)
} }
// DBOptions describes options to NewDB.
type DBOptions struct {
// CacheControl, if !nil, is set as default live cache control for
// newly created connections.
CacheControl LiveCacheControl
}
// NewDB creates new database handle. // NewDB creates new database handle.
// //
// Created database handle must be closed when no longer needed. // Created database handle must be closed when no longer needed.
func NewDB(stor IStorage) *DB { func NewDB(stor IStorage, opt *DBOptions) *DB {
// XXX db options? // copy opts in case caller will change them later
opt_ := *opt
db := &DB{ db := &DB{
stor: stor, stor: stor,
watchq: make(chan Event), watchq: make(chan Event),
opt: &opt_,
down: make(chan struct{}), down: make(chan struct{}),
hwait: make(map[hwaiter]struct{}), hwait: make(map[hwaiter]struct{}),
......
// Copyright (C) 2019 Nexedi SA and Contributors. // Copyright (C) 2019-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your // it under the terms of the GNU General Public License version 3, or (at your
...@@ -25,10 +25,16 @@ import ( ...@@ -25,10 +25,16 @@ import (
// imported at runtime via import_x_test due to cyclic dependency: // imported at runtime via import_x_test due to cyclic dependency:
var ZPyCommit func(string, Tid, ...IPersistent) (Tid, error) var ZPyCommit func(string, Tid, ...IPersistent) (Tid, error)
var ZPyCommitRaw func(string, Tid, ...ZRawObject) (Tid, error)
// exported for zodb_test package: // exported for zodb_test package:
type ZRawObject struct { // keep in sync with xtesting.ZRawObject
Oid Oid
Data []byte
}
func PSerialize(obj IPersistent) *mem.Buf { func PSerialize(obj IPersistent) *mem.Buf {
return obj.persistent().pSerialize() return obj.persistent().pSerialize()
} }
// Copyright (C) 2019 Nexedi SA and Contributors. // Copyright (C) 2019-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your // it under the terms of the GNU General Public License version 3, or (at your
...@@ -33,7 +33,8 @@ import ( ...@@ -33,7 +33,8 @@ import (
// due to cyclic dependency. // due to cyclic dependency.
func init() { func init() {
zodb.ZPyCommit = ZPyCommit zodb.ZPyCommit = ZPyCommit
zodb.ZPyCommitRaw = ZPyCommitRaw
} }
// ZPyCommit commits new transaction with specified objects. // ZPyCommit commits new transaction with specified objects.
...@@ -41,8 +42,8 @@ func init() { ...@@ -41,8 +42,8 @@ func init() {
// The objects need to be alive, but do not need to be marked as changed. // The objects need to be alive, but do not need to be marked as changed.
// The commit is performed via zodb/py. // The commit is performed via zodb/py.
func ZPyCommit(zurl string, at zodb.Tid, objv ...zodb.IPersistent) (zodb.Tid, error) { func ZPyCommit(zurl string, at zodb.Tid, objv ...zodb.IPersistent) (zodb.Tid, error) {
var rawobjv []xtesting.ZRawObject // raw zodb objects data to commit var rawobjv []zodb.ZRawObject // raw zodb objects data to commit
var bufv []*mem.Buf // buffers to release var bufv []*mem.Buf // buffers to release
defer func() { defer func() {
for _, buf := range bufv { for _, buf := range bufv {
buf.Release() buf.Release()
...@@ -51,7 +52,7 @@ func ZPyCommit(zurl string, at zodb.Tid, objv ...zodb.IPersistent) (zodb.Tid, er ...@@ -51,7 +52,7 @@ func ZPyCommit(zurl string, at zodb.Tid, objv ...zodb.IPersistent) (zodb.Tid, er
for _, obj := range objv { for _, obj := range objv {
buf := zodb.PSerialize(obj) buf := zodb.PSerialize(obj)
rawobj := xtesting.ZRawObject{ rawobj := zodb.ZRawObject{
Oid: obj.POid(), Oid: obj.POid(),
Data: buf.Data, Data: buf.Data,
} }
...@@ -59,5 +60,13 @@ func ZPyCommit(zurl string, at zodb.Tid, objv ...zodb.IPersistent) (zodb.Tid, er ...@@ -59,5 +60,13 @@ func ZPyCommit(zurl string, at zodb.Tid, objv ...zodb.IPersistent) (zodb.Tid, er
bufv = append(bufv, buf) bufv = append(bufv, buf)
} }
return xtesting.ZPyCommitRaw(zurl, at, rawobjv...) return ZPyCommitRaw(zurl, at, rawobjv...)
}
func ZPyCommitRaw(zurl string, at zodb.Tid, rawobjv ...zodb.ZRawObject) (zodb.Tid, error) {
var xrawobjv []xtesting.ZRawObject
for _, obj := range rawobjv {
xrawobjv = append(xrawobjv, xtesting.ZRawObject{Oid: obj.Oid, Data: obj.Data})
}
return xtesting.ZPyCommitRaw(zurl, at, xrawobjv...)
} }
// Copyright (C) 2018-2019 Nexedi SA and Contributors. // Copyright (C) 2018-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
...@@ -163,7 +163,7 @@ func (obj *Persistent) pSerialize() *mem.Buf { ...@@ -163,7 +163,7 @@ func (obj *Persistent) pSerialize() *mem.Buf {
func (obj *Persistent) PActivate(ctx context.Context) (err error) { func (obj *Persistent) PActivate(ctx context.Context) (err error) {
obj.mu.Lock() obj.mu.Lock()
obj.refcnt++ obj.refcnt++
doload := (obj.refcnt == 1 && obj.state == GHOST) doload := (obj.state == GHOST && obj.loading == nil)
defer func() { defer func() {
if err != nil { if err != nil {
obj.PDeactivate() obj.PDeactivate()
...@@ -208,10 +208,10 @@ func (obj *Persistent) PActivate(ctx context.Context) (err error) { ...@@ -208,10 +208,10 @@ func (obj *Persistent) PActivate(ctx context.Context) (err error) {
"%v (want %v); .loading = %p (want %p)", s, GHOST, l, loading)) "%v (want %v); .loading = %p (want %p)", s, GHOST, l, loading))
} }
obj.serial = serial
// try to pass loaded state to object // try to pass loaded state to object
if err == nil { if err == nil {
obj.serial = serial
switch istate := obj.istate().(type) { switch istate := obj.istate().(type) {
case Stateful: case Stateful:
err = istate.SetState(state) err = istate.SetState(state)
...@@ -229,9 +229,13 @@ func (obj *Persistent) PActivate(ctx context.Context) (err error) { ...@@ -229,9 +229,13 @@ func (obj *Persistent) PActivate(ctx context.Context) (err error) {
if err == nil { if err == nil {
obj.state = UPTODATE obj.state = UPTODATE
} }
} else {
obj.serial = InvalidTid
// force reload on next activate if it was an error
obj.loading = nil
} }
// XXX set state to load error? (to avoid panic on second activate after load error)
loading.err = err loading.err = err
obj.mu.Unlock() obj.mu.Unlock()
...@@ -257,7 +261,7 @@ func (obj *Persistent) PDeactivate() { ...@@ -257,7 +261,7 @@ func (obj *Persistent) PDeactivate() {
if obj.state >= CHANGED { if obj.state >= CHANGED {
return return
} }
if obj.oid == InvalidOid { // newly created not-yet committed object // TODO tests if obj.oid == InvalidOid { // newly created not-yet committed object
return return
} }
......
// Copyright (C) 2018-2020 Nexedi SA and Contributors. // Copyright (C) 2018-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
...@@ -21,6 +21,7 @@ package zodb ...@@ -21,6 +21,7 @@ package zodb
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
...@@ -276,7 +277,7 @@ func (t *tDB) Reopen() { ...@@ -276,7 +277,7 @@ func (t *tDB) Reopen() {
ReadOnly: true, ReadOnly: true,
NoCache: !t.rawcache, NoCache: !t.rawcache,
}); X(err) }); X(err)
db := NewDB(stor) db := NewDB(stor, &DBOptions{})
t.stor = stor t.stor = stor
t.db = db t.db = db
} }
...@@ -317,7 +318,7 @@ func (t *tDB) Add(oid Oid, value string) { ...@@ -317,7 +318,7 @@ func (t *tDB) Add(oid Oid, value string) {
} }
// Commit commits objects queued by Add. // Commit commits objects queued by Add.
func (t *tDB) Commit() { func (t *tDB) Commit() Tid {
t.Helper() t.Helper()
head, err := ZPyCommit(t.zurl, t.head, t.commitq...) head, err := ZPyCommit(t.zurl, t.head, t.commitq...)
...@@ -326,6 +327,19 @@ func (t *tDB) Commit() { ...@@ -326,6 +327,19 @@ func (t *tDB) Commit() {
} }
t.head = head t.head = head
t.commitq = nil t.commitq = nil
return head
}
// CommitRaw commits raw changes.
func (t *tDB) CommitRaw(rawobjv ...ZRawObject) Tid {
t.Helper()
head, err := ZPyCommitRaw(t.zurl, t.head, rawobjv...)
if err != nil {
t.Fatal(err)
}
t.head = head
return head
} }
// Open opens new test transaction/connection. // Open opens new test transaction/connection.
...@@ -443,12 +457,14 @@ func (t *tConnection) Abort() { ...@@ -443,12 +457,14 @@ func (t *tConnection) Abort() {
} }
func (t *tDB) fatalif(err error) { func (t *tDB) fatalif(err error) {
t.Helper()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }
func (t *tConnection) fatalif(err error) { func (t *tConnection) fatalif(err error) {
t.Helper()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -473,13 +489,11 @@ func testPersistentDB(t0 *testing.T, rawcache bool) { ...@@ -473,13 +489,11 @@ func testPersistentDB(t0 *testing.T, rawcache bool) {
tdb.Add(101, "bonjour") tdb.Add(101, "bonjour")
tdb.Add(102, "monde") tdb.Add(102, "monde")
tdb.Commit() at0 := tdb.Commit()
at0 := tdb.head
tdb.Add(101, "hello") tdb.Add(101, "hello")
tdb.Add(102, "world") tdb.Add(102, "world")
tdb.Commit() at1 := tdb.Commit()
at1 := tdb.head
tdb.Reopen() // so that at0 is not covered by db.δtail tdb.Reopen() // so that at0 is not covered by db.δtail
db := tdb.db db := tdb.db
...@@ -541,8 +555,7 @@ func testPersistentDB(t0 *testing.T, rawcache bool) { ...@@ -541,8 +555,7 @@ func testPersistentDB(t0 *testing.T, rawcache bool) {
// commit change to obj2 from external process // commit change to obj2 from external process
tdb.Add(102, "kitty") tdb.Add(102, "kitty")
tdb.Commit() at2 := tdb.Commit()
at2 := tdb.head
// new db connection should see the change // new db connection should see the change
t2 := tdb.Open(&ConnOptions{}) t2 := tdb.Open(&ConnOptions{})
...@@ -608,6 +621,18 @@ func testPersistentDB(t0 *testing.T, rawcache bool) { ...@@ -608,6 +621,18 @@ func testPersistentDB(t0 *testing.T, rawcache bool) {
t.checkObj(obj2, 102, at2, UPTODATE, 0, "kitty") t.checkObj(obj2, 102, at2, UPTODATE, 0, "kitty")
// newly created object should not go to ghost on deactivate
obj3 := NewMyObject(t.conn)
obj3.value = "new"
checkObj(t0, obj3, t.conn, InvalidOid, InvalidTid, UPTODATE, 0)
assert.Equal(obj3.value, "new")
t.PActivate(obj3)
checkObj(t0, obj3, t.conn, InvalidOid, InvalidTid, UPTODATE, 1)
obj3.PDeactivate()
checkObj(t0, obj3, t.conn, InvalidOid, InvalidTid, UPTODATE, 0)
assert.Equal(obj3.value, "new")
// finish tnx3 and txn2 - conn1 and conn2 go back to db pool // finish tnx3 and txn2 - conn1 and conn2 go back to db pool
t.Abort() t.Abort()
t2.Abort() t2.Abort()
...@@ -719,6 +744,69 @@ func testPersistentDB(t0 *testing.T, rawcache bool) { ...@@ -719,6 +744,69 @@ func testPersistentDB(t0 *testing.T, rawcache bool) {
t.checkObj(robj2, 102, InvalidTid, GHOST, 0) t.checkObj(robj2, 102, InvalidTid, GHOST, 0)
} }
// Verify that PActivate works correctly after hitting an error from the storage.
// In this test the error is "object was deleted".
func TestActivateAfterDelete(t0 *testing.T) {
assert := assert.New(t0)
tdb := testdb(t0, /*rawcache=*/false)
defer tdb.Close()
db := tdb.db
tdb.Add(101, "object")
at0 := tdb.Commit()
t := tdb.Open(&ConnOptions{})
// do not evict the object from live cache.
zcc := &zcacheControl{map[Oid]PCachePolicy{
101: PCachePinObject | PCacheKeepState,
}}
zcache := t.conn.Cache()
zcache.Lock()
zcache.SetControl(zcc)
zcache.Unlock()
// load the object
obj := t.Get(101)
t.checkObj(obj, 101, InvalidTid, GHOST, 0)
t.PActivate(obj)
t.checkObj(obj, 101, at0, UPTODATE, 1, "object")
obj.PDeactivate()
t.checkObj(obj, 101, at0, UPTODATE, 0, "object")
// delete obj
at1 := tdb.CommitRaw(ZRawObject{Oid: 101, Data: nil})
// conn stays at older view with obj pinned into the cache
t.checkObj(obj, 101, at0, UPTODATE, 0, "object")
// finish transaction and reopen new connection - it should be the same conn
t.Abort()
assert.Equal(db.pool, []*Connection{t.conn})
t_ := tdb.Open(&ConnOptions{})
assert.Same(t_.conn, t.conn)
t = t_
assert.Equal(t.conn.At(), at1)
// obj should be invalidated but present in the cache
t.checkObj(obj, 101, InvalidTid, GHOST, 0)
// activating obj should give "no data" error
// loop because second activate used to panic
for i := 0; i < 10; i++ {
err := obj.PActivate(t.ctx)
eok := &NoDataError{Oid: obj.POid(), DeletedAt: at1}
var e *NoDataError
errors.As(err, &e)
if !reflect.DeepEqual(e, eok) {
t.Fatalf("(%d) after delete: err:\nhave: %s\nwant cause: %s", i, err, eok)
}
// obj should stay in the cache in ghost state
t.checkObj(obj, 101, InvalidTid, GHOST, 0)
}
}
// Test details of how LiveCache handles live caching policy. // Test details of how LiveCache handles live caching policy.
func TestLiveCache(t0 *testing.T) { func TestLiveCache(t0 *testing.T) {
assert := assert.New(t0) assert := assert.New(t0)
...@@ -730,8 +818,7 @@ func TestLiveCache(t0 *testing.T) { ...@@ -730,8 +818,7 @@ func TestLiveCache(t0 *testing.T) {
tdb.Add(102, "труд") tdb.Add(102, "труд")
tdb.Add(103, "май") tdb.Add(103, "май")
tdb.Add(104, "весна") tdb.Add(104, "весна")
tdb.Commit() at1 := tdb.Commit()
at1 := tdb.head
zcc := &zcacheControl{map[Oid]PCachePolicy{ zcc := &zcacheControl{map[Oid]PCachePolicy{
// obj1 - default (currently: don't pin and don't keep state) // obj1 - default (currently: don't pin and don't keep state)
......
#!/usr/bin/env python2 #!/usr/bin/env python2
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (C) 2017-2020 Nexedi SA and Contributors. # Copyright (C) 2017-2021 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com> # Kirill Smelkov <kirr@nexedi.com>
# #
# This program is free software: you can Use, Study, Modify and Redistribute # This program is free software: you can Use, Study, Modify and Redistribute
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
"""generate reference pickle objects encoding for tests""" """generate reference pickle objects encoding for tests"""
from ZODB import serialize from ZODB import serialize
from zodbtools.test.gen_testdata import run_with_zodb3py2_compat from zodbtools.test.gen_testdata import run_with_zodb4py2_compat
from golang.gcompat import qq from golang.gcompat import qq
def main2(): def main2():
...@@ -56,7 +56,7 @@ def main2(): ...@@ -56,7 +56,7 @@ def main2():
emit("}") emit("}")
def main(): def main():
run_with_zodb3py2_compat(main2) run_with_zodb4py2_compat(main2)
if __name__ == '__main__': if __name__ == '__main__':
main() main()
// Copyright (C) 2017-2019 Nexedi SA and Contributors. // Copyright (C) 2017-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
...@@ -46,12 +46,30 @@ type DriverOptions struct { ...@@ -46,12 +46,30 @@ type DriverOptions struct {
// //
// Watchq can be nil to ignore such events. However if Watchq != nil, the events // Watchq can be nil to ignore such events. However if Watchq != nil, the events
// have to be consumed or else the storage driver will misbehave - e.g. // have to be consumed or else the storage driver will misbehave - e.g.
// it can get out of sync with the on-disk database file. // it can get out of sync with the on-disk database file, or deadlock
// on any user-called operation.
// //
// The storage driver closes !nil Watchq when the driver is closed. // The storage driver closes !nil Watchq when the driver is closed.
// //
// The storage driver will send only and all events in (at₀, +∞] range, // The storage driver will send only and all events in (at₀, +∞] range,
// where at₀ is at returned by driver open. // where at₀ is at returned by driver open.
//
// The storage driver will stop sending events after call to Close.
// In particular the following example is valid and safe from deadlock:
//
// watchq := make(chan zodb.Event)
// stor, at0, err := zodb.OpenDriver(..., &DriverOptions{Watchq: watchq})
// defer stor.Close()
//
// for {
// select {
// case <-ctx.Done():
// return ctx.Err()
//
// case <-watchq:
// ...
// }
// }
Watchq chan<- Event Watchq chan<- Event
} }
...@@ -73,10 +91,13 @@ func RegisterDriver(scheme string, opener DriverOpener) { ...@@ -73,10 +91,13 @@ func RegisterDriver(scheme string, opener DriverOpener) {
driverRegistry[scheme] = opener driverRegistry[scheme] = opener
} }
// XXX // OpenDriver opens ZODB storage driver by URL.
func openDriver(ctx context.Context, zurl string, opt *DriverOptions) (_ IStorageDriver, at0 Tid, _ error) { //
// It is similar to Open but returns low-level IStorageDriver instead of IStorage.
// Most users should use Open.
func OpenDriver(ctx context.Context, zurl string, opt *DriverOptions) (_ IStorageDriver, at0 Tid, _ error) {
// no scheme -> file:// // no scheme -> file://
if !strings.Contains(zurl, "://") { if !strings.Contains(zurl, ":") {
zurl = "file://" + zurl zurl = "file://" + zurl
} }
...@@ -91,7 +112,7 @@ func openDriver(ctx context.Context, zurl string, opt *DriverOptions) (_ IStorag ...@@ -91,7 +112,7 @@ func openDriver(ctx context.Context, zurl string, opt *DriverOptions) (_ IStorag
opener, ok := driverRegistry[u.Scheme] opener, ok := driverRegistry[u.Scheme]
if !ok { if !ok {
return nil, InvalidTid, fmt.Errorf("zodb: URL scheme \"%s://\" not supported", u.Scheme) return nil, InvalidTid, fmt.Errorf("zodb: URL scheme \"%s:\" not supported", u.Scheme)
} }
storDriver, at0, err := opener(ctx, u, opt) storDriver, at0, err := opener(ctx, u, opt)
...@@ -108,7 +129,7 @@ func openDriver(ctx context.Context, zurl string, opt *DriverOptions) (_ IStorag ...@@ -108,7 +129,7 @@ func openDriver(ctx context.Context, zurl string, opt *DriverOptions) (_ IStorag
// Users should import in storage packages they use or zodb/wks package to // Users should import in storage packages they use or zodb/wks package to
// get support for well-known storages. // get support for well-known storages.
// //
// Storage authors should register their storages with RegisterStorage. // Storage authors should register their storages with RegisterDriver.
func Open(ctx context.Context, zurl string, opt *OpenOptions) (IStorage, error) { func Open(ctx context.Context, zurl string, opt *OpenOptions) (IStorage, error) {
drvWatchq := make(chan Event) drvWatchq := make(chan Event)
drvOpt := &DriverOptions{ drvOpt := &DriverOptions{
...@@ -116,7 +137,7 @@ func Open(ctx context.Context, zurl string, opt *OpenOptions) (IStorage, error) ...@@ -116,7 +137,7 @@ func Open(ctx context.Context, zurl string, opt *OpenOptions) (IStorage, error)
Watchq: drvWatchq, Watchq: drvWatchq,
} }
storDriver, at0, err := openDriver(ctx, zurl, drvOpt) storDriver, at0, err := OpenDriver(ctx, zurl, drvOpt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -160,9 +181,10 @@ type storage struct { ...@@ -160,9 +181,10 @@ type storage struct {
driver IStorageDriver driver IStorageDriver
l1cache *Cache // can be =nil, if opened with NoCache l1cache *Cache // can be =nil, if opened with NoCache
down chan struct{} // ready when no longer operational down chan struct{} // ready when no longer operational
downOnce sync.Once // shutdown may be due to both Close and IO error in watcher|Sync downOnce sync.Once // shutdown may be due to both Close and IO error in watcher|Sync
downErr error // reason for shutdown downErr error // reason for shutdown
drvCloseErr error // err from .driver.Close()
// watcher // watcher
...@@ -185,6 +207,11 @@ func (s *storage) shutdown(reason error) { ...@@ -185,6 +207,11 @@ func (s *storage) shutdown(reason error) {
s.downOnce.Do(func() { s.downOnce.Do(func() {
close(s.down) close(s.down)
s.downErr = fmt.Errorf("not operational due: %s", reason) s.downErr = fmt.Errorf("not operational due: %s", reason)
// - if called by Close or failed Sync: driver.Close will close
// drvWatchq and cause watcher to stop.
// - if called by failed watcher: closing driver will prevent
// drvWatchq<- deadlock in driver because we no longer read from it.
s.drvCloseErr = s.driver.Close()
}) })
} }
...@@ -198,7 +225,7 @@ func (s *storage) Iterate(ctx context.Context, tidMin, tidMax Tid) ITxnIterator ...@@ -198,7 +225,7 @@ func (s *storage) Iterate(ctx context.Context, tidMin, tidMax Tid) ITxnIterator
func (s *storage) Close() error { func (s *storage) Close() error {
s.shutdown(fmt.Errorf("closed")) s.shutdown(fmt.Errorf("closed"))
return s.driver.Close() // this will close drvWatchq and cause watcher stop return s.drvCloseErr
} }
// loading goes through cache - this way prefetching can work // loading goes through cache - this way prefetching can work
...@@ -369,7 +396,7 @@ func (s *storage) _watcher() error { ...@@ -369,7 +396,7 @@ func (s *storage) _watcher() error {
func (s *storage) AddWatch(watchq chan<- Event) (at0 Tid) { func (s *storage) AddWatch(watchq chan<- Event) (at0 Tid) {
ack := make(chan Tid) ack := make(chan Tid)
select { select {
// no longer operational: behave if watchq was registered before that // no longer operational: behave as if watchq was registered before that
// and then seen down/close events. Interact with DelWatch directly. // and then seen down/close events. Interact with DelWatch directly.
case <-s.down: case <-s.down:
s.headMu.Lock() // shutdown may be due to Close call and watcher might be s.headMu.Lock() // shutdown may be due to Close call and watcher might be
...@@ -471,6 +498,14 @@ func (s *storage) Sync(ctx context.Context) (err error) { ...@@ -471,6 +498,14 @@ func (s *storage) Sync(ctx context.Context) (err error) {
} }
// wait till .head >= head // wait till .head >= head
// XXX instead require from drivers that `drv.Sync() -> head`
// guarantees that all EventCommit with .tid <= head were sent to
// watchq
//
// https://lab.nexedi.com/nexedi/ZODB/commit/40116375
// https://github.com/zopefoundation/ZODB/commit/4a6b0283#diff-d2a01f71a79ac2b379e218cf72fa1205d3426cad19e7b72d71899f643be4bb73
//
// ?
watchq := make(chan Event) watchq := make(chan Event)
at = s.AddWatch(watchq) at = s.AddWatch(watchq)
defer s.DelWatch(watchq) defer s.DelWatch(watchq)
......
This diff is collapsed.
// Copyright (C) 2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
package demo
import (
"bytes"
"context"
"io/ioutil"
"fmt"
"net/url"
"os"
"reflect"
"regexp"
"testing"
"lab.nexedi.com/kirr/go123/xerr"
"lab.nexedi.com/kirr/neo/go/internal/xtesting"
"lab.nexedi.com/kirr/neo/go/zodb"
"lab.nexedi.com/kirr/neo/go/zodb/zodbtools"
// for file: scheme support
_ "lab.nexedi.com/kirr/neo/go/zodb/storage/fs1"
)
// DemoData represents data for a demo: storage.
type DemoData struct {
base string // url for base.fs
δ string // ----/---- δ.fs
}
func (ddat *DemoData) URL() string {
return fmt.Sprintf("demo:(%s)/(%s)", ddat.base, ddat.δ)
}
// tOptions represents options for testing.
// TODO -> xtesting
type tOptions struct {
Preload string // preload database with data from this location
}
// withDemoData tests f with all kinds of opt.Preload data split into base + δ.
func withDemoData(t *testing.T, f func(t *testing.T, ddat *DemoData), optv ...tOptions) {
t.Helper()
X := xtesting.FatalIf(t)
opt := tOptions{}
if len(optv) > 1 {
panic("multiple tOptions not allowed")
}
if len(optv) == 1 {
opt = optv[0]
}
// retrieve zdump of Preload
zdump := ""
if opt.Preload != "" {
ctx := context.Background()
buf := &bytes.Buffer{}
stor, err := zodb.Open(ctx, opt.Preload, &zodb.OpenOptions{ReadOnly: true}); X(err)
err = zodbtools.Dump(ctx, buf, stor, 0, zodb.TidMax, /*hashonly=*/false)
stor.Close()
X(err)
zdump = buf.String()
}
// split zdump into transactions
// XXX hacky; TODO -> zodbtools.DumpReader
txnRe := regexp.MustCompile(`(?m)^txn (?P<tid>[0-9a-f]{16}) "(?P<status>.)"$`)
type zdumpTxn struct {
tid zodb.Tid
pos int // where this transaction starts in the dump
}
var txnv []zdumpTxn
for _, m := range txnRe.FindAllStringSubmatchIndex(zdump, -1) {
// [m[0]:m[1]] refers to whole txn line
__ := zdump[m[2]:m[3]]
tid, err := zodb.ParseTid(__); X(err)
txnv = append(txnv, zdumpTxn{tid, m[0]})
}
// verify f on all combinations of preload being split into base+δ
work := xtempdir(t)
defer os.RemoveAll(work)
test1 := func(δstart zodb.Tid, zdumpBase, zdumpδ string) {
t.Helper()
t.Run(fmt.Sprintf("δstart=%s", δstart), func(t *testing.T) {
t.Helper()
X := xtesting.FatalIf(t)
work1 := work + "/δ" + δstart.String()
err := os.Mkdir(work1, 0777); X(err)
base := "file://"+work1+"/base.fs"
δ := "file://"+work1+"/δ.fs"
ddat := &DemoData{base, δ}
_, err = xtesting.ZPyRestore(base, zdumpBase); X(err)
// restore δ part via `demo:(base)/(δ)` - not `file:δ`.
// The reason we do this is because restoring δ via
// just its file will fail when restoring copy data
// record with copy_from transaction being in base.
_, err = xtesting.ZPyRestore(ddat.URL(), zdumpδ); X(err)
f(t, ddat)
})
}
for i := 0; i < len(txnv); i++ {
δtail := txnv[i]
test1(δtail.tid, zdump[:δtail.pos], zdump[δtail.pos:])
}
test1(zodb.TidMax, zdump, "")
}
// withDemo tests f with demo: client connected to all kind of demo data splits.
func withDemo(t *testing.T, f func(t *testing.T, ddat *DemoData, ddrv *Storage), optv ...tOptions) {
t.Helper()
withDemoData(t, func(t *testing.T, ddat *DemoData) {
t.Helper()
X := xtesting.FatalIf(t)
ddrv, _, err := demoOpen(ddat.URL(), &zodb.DriverOptions{ReadOnly: true}); X(err)
defer func() {
err := ddrv.Close(); X(err)
}()
f(t, ddat, ddrv)
}, optv...)
}
func TestURL(t *testing.T) {
withDemo(t, func(t *testing.T, ddat *DemoData, ddrv *Storage) {
zurl := ddrv.URL()
zurlOk := ddat.URL()
if zurl != zurlOk {
t.Fatalf("bad zurl:\nhave: %s\nwant: %s", zurl, zurlOk)
}
})
}
func TestEmptyDB(t *testing.T) {
withDemo(t, func(t *testing.T, _ *DemoData, ddrv *Storage) {
xtesting.DrvTestEmptyDB(t, ddrv)
})
}
func TestLoad(t *testing.T) {
X := xtesting.FatalIf(t)
data := "../fs1/testdata/1.fs"
txnvOk, err := xtesting.LoadDBHistory(data); X(err)
withDemo(t, func(t *testing.T, _ *DemoData, ddrv *Storage) {
xtesting.DrvTestLoad(t, ddrv, txnvOk)
}, tOptions{
Preload: data,
})
}
func TestWatch(t *testing.T) {
withDemoData(t, func(t *testing.T, ddat *DemoData) {
xtesting.DrvTestWatch(t, ddat.URL(), openByURL)
})
}
// MutateBase mutates ddat.base with new commit.
func (ddat *DemoData) MutateBase() (zodb.Tid, error) {
return xtesting.ZPyCommitRaw(ddat.base, 0, xtesting.ZRawObject{
Oid: 1,
Data: []byte("ZZZ"),
})
}
// TestSync_vs_BaseMutate verifies Sync wrt base mutation.
func TestSync_vs_BaseMutate(t *testing.T) {
withDemo(t, func(t *testing.T, ddat *DemoData, ddrv *Storage) {
X := xtesting.FatalIf(t)
head, err := ddrv.Sync(context.Background())
if !(head == 0 && err == nil) {
t.Fatalf("sync0: head=%s err=%s", head, err)
}
tid, err := ddat.MutateBase(); X(err)
head, err = ddrv.Sync(context.Background())
errOk := &zodb.OpError{URL: ddrv.URL(), Op: "sync", Err: &baseMutatedError{
baseAt0: 0,
baseHead: tid,
}}
if !reflect.DeepEqual(err, errOk) {
t.Fatalf("after base mutate: sync: unexpected error:\nhave: %s\nwant: %s",
err, errOk)
}
})
}
// TestWatchLoad_vs_BaseMutate verifies Watch and Load wrt base mutation.
func TestWatchLoad_vs_BaseMutate(t *testing.T) {
withDemoData(t, func(t *testing.T, ddat *DemoData) {
X := xtesting.FatalIf(t)
watchq := make(chan zodb.Event)
ddrv, at0, err := demoOpen(ddat.URL(), &zodb.DriverOptions{
ReadOnly: true,
Watchq: watchq,
}); X(err)
defer func() {
err := ddrv.Close(); X(err)
}()
tid, err := ddat.MutateBase(); X(err)
// first wait for error from watchq
event, ok := <-watchq
if !ok {
t.Fatal("after base mutate: premature watchq close")
}
evErr, ok := event.(*zodb.EventError)
if !ok {
t.Fatalf("after base mutate: unexpected event: %T", event)
}
errBaseMutated := &baseMutatedError{
baseAt0: 0,
baseHead: tid,
}
evErrOk := &zodb.EventError{&zodb.OpError{URL: ddrv.URL(), Op: "watcher", Err: errBaseMutated}}
if !reflect.DeepEqual(evErr, evErrOk) {
t.Fatalf("after base mutate: unexpected event:\nhave: %s\nwant: %s", evErr, evErrOk)
}
// now make sure Load fails with "base mutated" error
xid := zodb.Xid{Oid: 1, At: at0}
data, serial, err := ddrv.Load(context.Background(), xid)
errOk := &zodb.OpError{URL: ddrv.URL(), Op: "load", Args: xid, Err: errBaseMutated}
if !reflect.DeepEqual(err, errOk) {
t.Fatalf("after base mutate: load: unexpected error:\nhave: %s\nwant: %s",
err, errOk)
}
if !(data == nil && serial == zodb.InvalidTid) {
t.Fatalf("after base mutate: load: unexpected data=%v serial=%v", data, serial)
}
})
}
func demoOpen(zurl string, opt *zodb.DriverOptions) (_ *Storage, at0 zodb.Tid, err error) {
defer xerr.Contextf(&err, "opendemo %s", zurl)
u, err := url.Parse(zurl)
if err != nil {
return nil, 0, err
}
d, at0, err := openByURL(context.Background(), u, opt)
if err != nil {
return nil, 0, err
}
return d.(*Storage), at0, nil
}
func xtempdir(t *testing.T) string {
t.Helper()
tmpd, err := ioutil.TempDir("", "demo")
if err != nil {
t.Fatal(err)
}
return tmpd
}
// Copyright (C) 2017-2020 Nexedi SA and Contributors. // Copyright (C) 2017-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
...@@ -109,6 +109,9 @@ type FileStorage struct { ...@@ -109,6 +109,9 @@ type FileStorage struct {
downOnce sync.Once // shutdown may be due to both Close and IO error in watcher downOnce sync.Once // shutdown may be due to both Close and IO error in watcher
errClose error // error from .file.Close() errClose error // error from .file.Close()
watchWg sync.WaitGroup // to wait for watcher finish watchWg sync.WaitGroup // to wait for watcher finish
closed chan struct{} // ready when storage was Closed
closeOnce sync.Once
} }
// IStorageDriver // IStorageDriver
...@@ -132,7 +135,7 @@ func (fs *FileStorage) LastOid(_ context.Context) (zodb.Oid, error) { ...@@ -132,7 +135,7 @@ func (fs *FileStorage) LastOid(_ context.Context) (zodb.Oid, error) {
} }
func (fs *FileStorage) URL() string { func (fs *FileStorage) URL() string {
return fs.file.Name() return "file://" + fs.file.Name()
} }
// freelist(DataHeader) // freelist(DataHeader)
...@@ -468,7 +471,7 @@ func (fs *FileStorage) watcher(w *fsnotify.Watcher, errFirstRead chan<- error) { ...@@ -468,7 +471,7 @@ func (fs *FileStorage) watcher(w *fsnotify.Watcher, errFirstRead chan<- error) {
// XXX it can also be internal.poll.ErrFileClosing // XXX it can also be internal.poll.ErrFileClosing
e.Err.Error() == "use of closed file") { e.Err.Error() == "use of closed file") {
select { select {
case <-fs.down: case <-fs.closed:
err = nil err = nil
default: default:
} }
...@@ -485,7 +488,13 @@ func (fs *FileStorage) watcher(w *fsnotify.Watcher, errFirstRead chan<- error) { ...@@ -485,7 +488,13 @@ func (fs *FileStorage) watcher(w *fsnotify.Watcher, errFirstRead chan<- error) {
if fs.watchq != nil { if fs.watchq != nil {
if err != nil { if err != nil {
fs.watchq <- &zodb.EventError{err} select {
case <-fs.closed:
// closed - skip send to watchq
case fs.watchq <- &zodb.EventError{err}:
// ok
}
} }
close(fs.watchq) close(fs.watchq)
} }
...@@ -535,9 +544,9 @@ mainloop: ...@@ -535,9 +544,9 @@ mainloop:
if !first { if !first {
traceWatch("select ...") traceWatch("select ...")
select { select {
case <-fs.down: case <-fs.closed:
// closed // closed
traceWatch("down") traceWatch("closed")
return nil return nil
case err := <-w.Errors: case err := <-w.Errors:
...@@ -694,7 +703,7 @@ mainloop: ...@@ -694,7 +703,7 @@ mainloop:
// notify client // notify client
if fs.watchq != nil { if fs.watchq != nil {
select { select {
case <-fs.down: case <-fs.closed:
return nil return nil
case fs.watchq <- &zodb.EventCommit{it.Txnh.Tid, δoid}: case fs.watchq <- &zodb.EventCommit{it.Txnh.Tid, δoid}:
...@@ -772,6 +781,9 @@ func (fs *FileStorage) shutdown(reason error) { ...@@ -772,6 +781,9 @@ func (fs *FileStorage) shutdown(reason error) {
} }
func (fs *FileStorage) Close() error { func (fs *FileStorage) Close() error {
fs.closeOnce.Do(func() {
close(fs.closed)
})
fs.shutdown(fmt.Errorf("closed")) fs.shutdown(fmt.Errorf("closed"))
fs.watchWg.Wait() fs.watchWg.Wait()
...@@ -794,6 +806,7 @@ func Open(ctx context.Context, path string, opt *zodb.DriverOptions) (_ *FileSto ...@@ -794,6 +806,7 @@ func Open(ctx context.Context, path string, opt *zodb.DriverOptions) (_ *FileSto
fs := &FileStorage{ fs := &FileStorage{
watchq: opt.Watchq, watchq: opt.Watchq,
down: make(chan struct{}), down: make(chan struct{}),
closed: make(chan struct{}),
} }
f, err := os.Open(path) f, err := os.Open(path)
......
// Copyright (C) 2017-2020 Nexedi SA and Contributors. // Copyright (C) 2017-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
...@@ -370,3 +370,37 @@ func TestOpenRecovery(t *testing.T) { ...@@ -370,3 +370,37 @@ func TestOpenRecovery(t *testing.T) {
}) })
} }
} }
// TestLoadWhiteout verifies access to whiteout data record.
//
// Whiteout is data record with deletion when object was not previously there.
// It has both len(data)=0 and backpointer=0.
//
// TODO merge into regular tests on testdata/1.fs when
// FileStorage/py.deleteObject allows to create whiteouts instead of raising
// POSKeyError.
func TestLoadWhiteout(t *testing.T) {
fs, _ := xfsopen(t, "testdata/whiteout.fs")
defer exc.XRun(fs.Close)
xid := zodb.Xid{At: zodb.Tid(0x17), Oid: zodb.Oid(1)}
buf, serial, err := fs.Load(context.Background(), xid)
errOk := &zodb.OpError{
URL: fs.URL(),
Op: "load",
Args: xid,
Err: &zodb.NoDataError{Oid: xid.Oid, DeletedAt: xid.At},
}
if !reflect.DeepEqual(err, errOk) {
t.Errorf("load %s: bad err:\nhave: %v\nwant: %v", xid, err, errOk)
}
if buf != nil {
t.Errorf("load %s: buf != nil", xid)
}
if serial != 0 {
t.Errorf("load %s: bad serial %s ; want 0", xid, serial)
}
}
// Copyright (C) 2017-2020 Nexedi SA and Contributors. // Copyright (C) 2017-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
...@@ -207,7 +207,7 @@ func (txnh *TxnHeader) CloneFrom(txnh2 *TxnHeader) { ...@@ -207,7 +207,7 @@ func (txnh *TxnHeader) CloneFrom(txnh2 *TxnHeader) {
type TxnLoadFlags int type TxnLoadFlags int
const ( const (
LoadAll TxnLoadFlags = 0x00 // load whole transaction header LoadAll TxnLoadFlags = 0x00 // load whole transaction header
LoadNoStrings = 0x01 // do not load user/desc/ext strings LoadNoStrings TxnLoadFlags = 0x01 // do not load user/desc/ext strings
) )
// Load reads and decodes transaction record header @ pos. // Load reads and decodes transaction record header @ pos.
...@@ -575,7 +575,10 @@ func (dh *DataHeader) LoadBackRef(r io.ReaderAt) (backPos int64, err error) { ...@@ -575,7 +575,10 @@ func (dh *DataHeader) LoadBackRef(r io.ReaderAt) (backPos int64, err error) {
} }
backPos = int64(binary.BigEndian.Uint64(dh.workMem[0:])) backPos = int64(binary.BigEndian.Uint64(dh.workMem[0:]))
if !(backPos == 0 || backPos >= dataValidFrom) { if backPos == 0 {
return 0, nil // deletion
}
if backPos < dataValidFrom {
return 0, checkErr(r, dh, "invalid backpointer: %v", backPos) return 0, checkErr(r, dh, "invalid backpointer: %v", backPos)
} }
if backPos + DataHeaderSize > dh.TxnPos - 8 { if backPos + DataHeaderSize > dh.TxnPos - 8 {
......
// Copyright (C) 2017 Nexedi SA and Contributors. // Copyright (C) 2017-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your // it under the terms of the GNU General Public License version 3, or (at your
...@@ -51,6 +51,9 @@ type Dumper interface { ...@@ -51,6 +51,9 @@ type Dumper interface {
// //
// If dumper return io.EOF the whole dumping process finishes. // If dumper return io.EOF the whole dumping process finishes.
DumpTxn(buf *xfmt.Buffer, it *fs1.Iter) error DumpTxn(buf *xfmt.Buffer, it *fs1.Iter) error
// DumpEndOK is called at the end of successfull dump.
DumpEndOK(buf *xfmt.Buffer) error
} }
// Dump dumps content of a FileStorage file @ path. // Dump dumps content of a FileStorage file @ path.
...@@ -101,7 +104,7 @@ func Dump(w io.Writer, path string, dir fs1.IterDir, d Dumper) (err error) { ...@@ -101,7 +104,7 @@ func Dump(w io.Writer, path string, dir fs1.IterDir, d Dumper) (err error) {
err = it.NextTxn(fs1.LoadAll) err = it.NextTxn(fs1.LoadAll)
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
err = nil break
} }
return err return err
} }
...@@ -109,7 +112,7 @@ func Dump(w io.Writer, path string, dir fs1.IterDir, d Dumper) (err error) { ...@@ -109,7 +112,7 @@ func Dump(w io.Writer, path string, dir fs1.IterDir, d Dumper) (err error) {
err = d.DumpTxn(buf, it) err = d.DumpTxn(buf, it)
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
err = nil break
} }
return err return err
} }
...@@ -119,6 +122,12 @@ func Dump(w io.Writer, path string, dir fs1.IterDir, d Dumper) (err error) { ...@@ -119,6 +122,12 @@ func Dump(w io.Writer, path string, dir fs1.IterDir, d Dumper) (err error) {
return err return err
} }
} }
err = d.DumpEndOK(buf)
if err != nil {
return err
}
return nil
} }
// ---------------------------------------- // ----------------------------------------
...@@ -198,6 +207,10 @@ func (d *DumperFsDump) DumpTxn(buf *xfmt.Buffer, it *fs1.Iter) error { ...@@ -198,6 +207,10 @@ func (d *DumperFsDump) DumpTxn(buf *xfmt.Buffer, it *fs1.Iter) error {
} }
} }
func (d *DumperFsDump) DumpEndOK(buf *xfmt.Buffer) error {
return nil
}
// DumperFsDumpVerbose implements a very verbose dumper with output identical // DumperFsDumpVerbose implements a very verbose dumper with output identical
// to fsdump.Dumper in zodb/py originally written by Jeremy Hylton: // to fsdump.Dumper in zodb/py originally written by Jeremy Hylton:
// //
...@@ -281,6 +294,10 @@ func (d *DumperFsDumpVerbose) dumpData(buf *xfmt.Buffer, it *fs1.Iter) error { ...@@ -281,6 +294,10 @@ func (d *DumperFsDumpVerbose) dumpData(buf *xfmt.Buffer, it *fs1.Iter) error {
return nil return nil
} }
func (d *DumperFsDumpVerbose) DumpEndOK(buf *xfmt.Buffer) error {
return nil
}
const dumpSummary = "dump database transactions" const dumpSummary = "dump database transactions"
func dumpUsage(w io.Writer) { func dumpUsage(w io.Writer) {
...@@ -383,6 +400,10 @@ func (d *DumperFsTail) DumpTxn(buf *xfmt.Buffer, it *fs1.Iter) error { ...@@ -383,6 +400,10 @@ func (d *DumperFsTail) DumpTxn(buf *xfmt.Buffer, it *fs1.Iter) error {
return nil return nil
} }
func (d *DumperFsTail) DumpEndOK(buf *xfmt.Buffer) error {
return nil
}
const tailSummary = "dump last few transactions of a database" const tailSummary = "dump last few transactions of a database"
const ntxnDefault = 10 const ntxnDefault = 10
......
// Copyright (C) 2017 Nexedi SA and Contributors. // Copyright (C) 2017-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your // it under the terms of the GNU General Public License version 3, or (at your
...@@ -31,6 +31,10 @@ package fs1tools ...@@ -31,6 +31,10 @@ package fs1tools
//go:generate sh -c "python2 -c 'from ZODB.FileStorage import fsdump; fsdump.main()' ../testdata/empty.fs >testdata/empty.fsdump.ok" //go:generate sh -c "python2 -c 'from ZODB.FileStorage import fsdump; fsdump.main()' ../testdata/empty.fs >testdata/empty.fsdump.ok"
//go:generate sh -c "python2 -c 'from ZODB.FileStorage.fsdump import Dumper; import sys; d = Dumper(sys.argv[1]); d.dump()' ../testdata/empty.fs >testdata/empty.fsdumpv.ok" //go:generate sh -c "python2 -c 'from ZODB.FileStorage.fsdump import Dumper; import sys; d = Dumper(sys.argv[1]); d.dump()' ../testdata/empty.fs >testdata/empty.fsdumpv.ok"
//go:generate sh -c "python2 -m ZODB.scripts.fstail -n 1000000 ../testdata/whiteout.fs >testdata/whiteout.fstail.ok"
//go:generate sh -c "python2 -c 'from ZODB.FileStorage import fsdump; fsdump.main()' ../testdata/whiteout.fs >testdata/whiteout.fsdump.ok"
//go:generate sh -c "python2 -c 'from ZODB.FileStorage.fsdump import Dumper; import sys; d = Dumper(sys.argv[1]); d.dump()' ../testdata/whiteout.fs >testdata/whiteout.fsdumpv.ok"
import ( import (
"bytes" "bytes"
"fmt" "fmt"
...@@ -50,10 +54,11 @@ func loadFile(t *testing.T, path string) string { ...@@ -50,10 +54,11 @@ func loadFile(t *testing.T, path string) string {
return string(data) return string(data)
} }
func testDump(t *testing.T, dir fs1.IterDir, d Dumper) { func testDump(t *testing.T, dir fs1.IterDir, newd func() Dumper) {
testv := []string{"1", "empty"} testv := []string{"1", "empty", "whiteout"}
for _, tt := range testv { for _, tt := range testv {
t.Run("db=" + tt, func(t *testing.T) { t.Run("db=" + tt, func(t *testing.T) {
d := newd()
buf := bytes.Buffer{} buf := bytes.Buffer{}
err := Dump(&buf, fmt.Sprintf("../testdata/%s.fs", tt), dir, d) err := Dump(&buf, fmt.Sprintf("../testdata/%s.fs", tt), dir, d)
...@@ -70,9 +75,13 @@ func testDump(t *testing.T, dir fs1.IterDir, d Dumper) { ...@@ -70,9 +75,13 @@ func testDump(t *testing.T, dir fs1.IterDir, d Dumper) {
} }
} }
func TestFsDump(t *testing.T) { testDump(t, fs1.IterForward, &DumperFsDump{}) } func newFsDump() Dumper { return &DumperFsDump{} }
func TestFsDumpv(t *testing.T) { testDump(t, fs1.IterForward, &DumperFsDumpVerbose{}) } func newFsDumpv() Dumper { return &DumperFsDumpVerbose{} }
func TestFsTail(t *testing.T) { testDump(t, fs1.IterBackward, &DumperFsTail{Ntxn: 1000000}) } func newFsTail() Dumper { return &DumperFsTail{Ntxn: 1000000} }
func TestFsDump(t *testing.T) { testDump(t, fs1.IterForward, newFsDump) }
func TestFsDumpv(t *testing.T) { testDump(t, fs1.IterForward, newFsDumpv) }
func TestFsTail(t *testing.T) { testDump(t, fs1.IterBackward, newFsTail) }
func BenchmarkTail(b *testing.B) { func BenchmarkTail(b *testing.B) {
// FIXME small testdata/1.fs is not representative for benchmarking // FIXME small testdata/1.fs is not representative for benchmarking
......
// Copyright (C) 2017 Nexedi SA and Contributors. // Copyright (C) 2017-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your // it under the terms of the GNU General Public License version 3, or (at your
...@@ -30,7 +30,7 @@ var commands = prog.CommandRegistry{ ...@@ -30,7 +30,7 @@ var commands = prog.CommandRegistry{
{"reindex", reindexSummary, reindexUsage, reindexMain}, {"reindex", reindexSummary, reindexUsage, reindexMain},
{"verify-index", verifyIdxSummary, verifyIdxUsage, verifyIdxMain}, {"verify-index", verifyIdxSummary, verifyIdxUsage, verifyIdxMain},
// recover (fsrecover.py) // recover (fsrecover.py)
// verify (fstest.py) {"verify", verifySummary, verifyUsage, verifyMain},
// XXX repozo ? // XXX repozo ?
} }
......
Trans #00000 tid=0000000000000017 time=1900-01-01 00:00:00.000000 offset=27
status=' ' user='' description=''
data #00000 oid=0000000000000001 class=undo or abort of object creation
************************************************************
file identifier: 'FS21'
============================================================
offset: 4
end pos: 77
transaction id: 0000000000000017
trec len: 73
status: ' '
user: ''
description: ''
len(extra): 0
------------------------------------------------------------
offset: 27
oid: 0000000000000001
revid: 0000000000000017
previous record offset: 0
transaction offset: 4
len(data): 0
backpointer: 0
redundant trec len: 73
1900-01-01 00:00:00.000000: hash=e3e0ff6c686f2fa02266e744f6629d592d432061
user='' description='' length=73 offset=4 (+23)
// Copyright (C) 2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
package fs1tools
// verify subcommand
//
// verification output mimics fstest from ZODB/py as originally written by Jeremy Hylton:
// https://github.com/zopefoundation/ZODB/blob/5.6.0-35-g1fb097b41/src/ZODB/scripts/fstest.py
import (
"flag"
"fmt"
"io"
"os"
"time"
"lab.nexedi.com/kirr/neo/go/zodb/storage/fs1"
"lab.nexedi.com/kirr/go123/prog"
"lab.nexedi.com/kirr/go123/xflag"
"lab.nexedi.com/kirr/go123/xfmt"
)
// Verify verifies content of a FileStorage file @ path.
//
// Only data part of the database is verified (the *.fs file).
// Use VerifyIndexFor to verify the index part (*.fs.index).
func Verify(w io.Writer, path string, verbose int, progress bool) (err error) {
// just iterate through the file and emit progress.
// the FileStorage driver implements all consistency checks by itself.
fi, err := os.Stat(path)
if err != nil {
return fmt.Errorf("verify: %s: %s", path, err)
}
fsize := fi.Size()
v := &Verifier{verbose: verbose}
// display progress updates once per tick
if progress {
tick := time.NewTicker(time.Second / 4)
defer tick.Stop()
xcr := ""
if verbose > 0 {
xcr = "\n"
}
v.progress = func(force bool) error {
if !force {
select {
case <-tick.C:
default:
return nil
}
}
_, err := fmt.Fprintf(w,
"\rVerified data bytes: %.1f%% (%d/%d); #txn: %d%s",
100 * float64(v.donePos) / float64(fsize),
v.donePos, fsize,
v.ntxn,
xcr)
return err
}
}
return Dump(w, path, fs1.IterForward, v)
}
// Verifier implements Dumper that is used by Verify.
type Verifier struct {
ntxn int // current transaction record #
verbose int // >=1 (print txn) >=2 (print objects)
// for loading data
dhLoading fs1.DataHeader
donePos int64 // done verifying till this position
progress func(force bool) error // called after each transaction if !nil
}
func (v *Verifier) DumperName() string {
return "fsverify"
}
func (v *Verifier) DumpFileHeader(buf *xfmt.Buffer, fh *fs1.FileHeader) error {
return nil
}
func (v *Verifier) DumpTxn(buf *xfmt.Buffer, it *fs1.Iter) error {
txnh := &it.Txnh
for i := 0; ; i++ {
err := it.NextData()
if err != nil {
if err == io.EOF {
break
}
return err
}
dh := &it.Datah
// load data
v.dhLoading = *dh
dbuf, err := v.dhLoading.LoadData(it.R)
if err != nil {
return err
}
if v.verbose >= 2 {
buf .S(fmt.Sprintf("%10d: object oid 0x%s #%d\n", dh.Pos, dh.Oid, i))
}
dbuf.Release()
}
if v.verbose >= 1 {
buf .S(fmt.Sprintf("%10d: transaction tid 0x%s #%d \n", txnh.Pos, txnh.Tid, v.ntxn))
}
v.ntxn++
if v.progress != nil {
v.donePos = txnh.Pos + txnh.Len
err := v.progress(/*force=*/false)
if err != nil {
return err
}
}
return nil
}
func (v *Verifier) DumpEndOK(buf *xfmt.Buffer) error {
if v.progress != nil {
err := v.progress(/*force=*/true)
if err != nil {
return err
}
}
if v.verbose >= 1 {
buf .S("no errors detected\n")
}
return nil
}
// ----------------------------------------
const verifySummary = "verify database content"
func verifyUsage(w io.Writer) {
fmt.Fprintf(w,
`Usage: fs1 verify [options] <storage>
Verify FileStorage records for consistency
<storage> is a path to FileStorage
options:
-h --help this help text.
-v increase verbosity.
-p display progress.
`)
}
func verifyMain(argv []string) {
verbose := 0
var progress bool
flags := flag.FlagSet{Usage: func() { verifyUsage(os.Stderr) }}
flags.Init("", flag.ExitOnError)
flags.Var((*xflag.Count)(&verbose), "v", "verbosity level")
flags.BoolVar(&progress, "p", false, "display progress")
flags.Parse(argv[1:])
argv = flags.Args()
if len(argv) < 1 {
flags.Usage()
prog.Exit(2)
}
storPath := argv[0]
err := Verify(os.Stdout, storPath, verbose, progress)
if err != nil {
prog.Fatal(err)
}
}
#!/usr/bin/env python2 #!/usr/bin/env python2
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (C) 2017-2020 Nexedi SA and Contributors. # Copyright (C) 2017-2021 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com> # Kirill Smelkov <kirr@nexedi.com>
# #
# This program is free software: you can Use, Study, Modify and Redistribute # This program is free software: you can Use, Study, Modify and Redistribute
...@@ -21,9 +21,10 @@ ...@@ -21,9 +21,10 @@
"""generate reference fs1 database and index for tests""" """generate reference fs1 database and index for tests"""
from ZODB.FileStorage import FileStorage from ZODB.FileStorage import FileStorage
from ZODB.FileStorage.FileStorage import FILESTORAGE_MAGIC, TxnHeader, DataHeader, TRANS_HDR_LEN
from ZODB import DB from ZODB import DB
from ZODB.Connection import TransactionMetaData from ZODB.Connection import TransactionMetaData
from zodbtools.test.gen_testdata import gen_testdb, precommit, run_with_zodb3py2_compat from zodbtools.test.gen_testdata import gen_testdb, precommit, run_with_zodb4py2_compat
from os import stat, remove from os import stat, remove
from shutil import copyfile from shutil import copyfile
from golang.gcompat import qq from golang.gcompat import qq
...@@ -156,7 +157,7 @@ def main(): ...@@ -156,7 +157,7 @@ def main():
vstor.store(vroot._p_oid, vroot._p_serial, '000 data 000', '', txn_stormeta) vstor.store(vroot._p_oid, vroot._p_serial, '000 data 000', '', txn_stormeta)
vstor.tpc_vote(txn_stormeta) vstor.tpc_vote(txn_stormeta)
# NO tpc_finish here so that status remain 'c' (voted) instead of ' ' (committed) # NO tpc_finish here so that status remain 'c' (voted) instead of ' ' (committed)
run_with_zodb3py2_compat(_) run_with_zodb4py2_compat(_)
st = stat(outfs) st = stat(outfs)
l = st.st_size l = st.st_size
...@@ -174,5 +175,36 @@ def main(): ...@@ -174,5 +175,36 @@ def main():
remove(voted+".lock") remove(voted+".lock")
# prepare file with whiteout (deletion of previously non-existing object)
whiteout = "testdata/whiteout.fs"
# as of 20210317 FileStorage.deleteObject verifies that object exists
# -> prepare magic/transaction/data records manually
with open(whiteout, "wb") as f:
oid = p64(1)
tid = p64(0x17)
# file header
f.write(FILESTORAGE_MAGIC)
tpos = f.tell()
# data record (see FileStorage.deleteObject)
dh = DataHeader(oid, tid, 0, tpos, 0, 0)
drec = dh.asString() + p64(0)
# emit txn header (see FileStorage.tpc_vote)
tlen = TRANS_HDR_LEN + 0 + 0 + 0 + len(drec) # empty u,d,e
th = TxnHeader(tid, tlen, ' ', 0, 0, 0)
th.user = b''
th.descr = b''
th.ext = b''
f.write(th.asString())
# emit data record
f.write(drec)
# emit txn tail
f.write(p64(tlen))
if __name__ == '__main__': if __name__ == '__main__':
main() main()
// Copyright (C) 2018-2020 Nexedi SA and Contributors. // Copyright (C) 2018-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
...@@ -54,7 +54,7 @@ type msg struct { ...@@ -54,7 +54,7 @@ type msg struct {
type msgFlags int64 type msgFlags int64
const ( const (
msgAsync msgFlags = 1 // message does not need a reply msgAsync msgFlags = 1 // message does not need a reply
msgExcept = 2 // exception was raised on remote side (ZEO5) msgExcept msgFlags = 2 // exception was raised on remote side (ZEO5)
) )
// encoding represents messages encoding. // encoding represents messages encoding.
...@@ -104,7 +104,7 @@ func pktEncodeM(m msg) *pktBuf { ...@@ -104,7 +104,7 @@ func pktEncodeM(m msg) *pktBuf {
// arg // arg
// it is interface{} - use shamaton/msgpack since msgp does not handle // it is interface{} - use shamaton/msgpack since msgp does not handle
// arbitrary interfaces well. // arbitrary interfaces well.
dataArg, err := msgpack.Encode(m.arg) dataArg, err := msgpack.Marshal(m.arg)
if err != nil { if err != nil {
panic(err) // all our types are expected to be supported by msgpack panic(err) // all our types are expected to be supported by msgpack
} }
...@@ -240,7 +240,7 @@ func pktDecodeM(pkb *pktBuf) (msg, error) { ...@@ -240,7 +240,7 @@ func pktDecodeM(pkb *pktBuf) (msg, error) {
if len(btail) != 0 { if len(btail) != 0 {
return m, derrf(".%d: payload has extra data after message") return m, derrf(".%d: payload has extra data after message")
} }
err = msgpack.Decode(b, &m.arg) err = msgpack.Unmarshal(b, &m.arg)
if err != nil { if err != nil {
return m, derrf(".%d: arg: %s", m.msgid, err) return m, derrf(".%d: arg: %s", m.msgid, err)
} }
......
...@@ -52,6 +52,9 @@ type zeo struct { ...@@ -52,6 +52,9 @@ type zeo struct {
// becomes ready when serve loop finishes // becomes ready when serve loop finishes
serveWG sync.WaitGroup serveWG sync.WaitGroup
closeOnce sync.Once
closed chan struct{} // ready when Closed
url string // we were opened via this url string // we were opened via this
} }
...@@ -174,7 +177,13 @@ func (z *zeo) invalidateTransaction(arg interface{}) (err error) { ...@@ -174,7 +177,13 @@ func (z *zeo) invalidateTransaction(arg interface{}) (err error) {
// at0 is initialized - ok to send current event if it goes > at0 // at0 is initialized - ok to send current event if it goes > at0
if tid > z.at0 { if tid > z.at0 {
z.watchq <- event select {
case <-z.closed:
// closed - client does not read watchq anymore
case z.watchq <- event:
// ok
}
} }
return nil return nil
} }
...@@ -188,7 +197,13 @@ func (z *zeo) flushEventq0() { ...@@ -188,7 +197,13 @@ func (z *zeo) flushEventq0() {
if z.watchq != nil { if z.watchq != nil {
for _, e := range z.eventq0 { for _, e := range z.eventq0 {
if e.Tid > z.at0 { if e.Tid > z.at0 {
z.watchq <- e select {
case <-z.closed:
// closed - client does not read watchq anymore
case z.watchq <- e:
// ok
}
} }
} }
} }
...@@ -264,7 +279,7 @@ func (r rpc) call(ctx context.Context, argv ...interface{}) (interface{}, error) ...@@ -264,7 +279,7 @@ func (r rpc) call(ctx context.Context, argv ...interface{}) (interface{}, error)
// excError returns error corresponding to an exception. // excError returns error corresponding to an exception.
// //
// well-known exceptions are mapped to corresponding well-known errors - e.g. // well-known exceptions are mapped to corresponding well-known errors - e.g.
// POSKeyError -> zodb.NoObjectError, and rest are returned wrapper into rpcExcept. // POSKeyError -> zodb.NoObjectError, and rest are returned wrapped into rpcExcept.
func (r rpc) excError(exc string, argv tuple) error { func (r rpc) excError(exc string, argv tuple) error {
// translate well-known exceptions // translate well-known exceptions
switch exc { switch exc {
...@@ -438,7 +453,7 @@ func openByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) (_ zodb ...@@ -438,7 +453,7 @@ func openByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) (_ zodb
}() }()
z := &zeo{link: zlink, watchq: opt.Watchq, url: url} z := &zeo{link: zlink, watchq: opt.Watchq, closed: make(chan struct{}), url: url}
// start serve loop on the link // start serve loop on the link
z.serveWG.Add(1) z.serveWG.Add(1)
...@@ -456,14 +471,18 @@ func openByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) (_ zodb ...@@ -456,14 +471,18 @@ func openByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) (_ zodb
// close .watchq after serve is over // close .watchq after serve is over
z.at0Mu.Lock() z.at0Mu.Lock()
defer z.at0Mu.Unlock() defer z.at0Mu.Unlock()
if z.at0Initialized {
z.flushEventq0()
}
if z.watchq != nil { if z.watchq != nil {
if err != nil { if err != nil && /* already flushed .eventq0 */z.at0Initialized {
z.watchq <- &zodb.EventError{Err: err} select {
case <-z.closed:
// closed - client does not read watchq anymore
case z.watchq <- &zodb.EventError{Err: err}:
// ok
}
} }
close(z.watchq) close(z.watchq)
z.watchq = nil // prevent flushEventq0 to send to closed chan
} }
}() }()
...@@ -496,13 +515,16 @@ func openByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) (_ zodb ...@@ -496,13 +515,16 @@ func openByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) (_ zodb
// "invalidateTransaction" server notification. // "invalidateTransaction" server notification.
// //
// filter-out first < at0 messages for this reason. // filter-out first < at0 messages for this reason.
z.at0Mu.Lock() //
z.at0 = lastTid // do this in separate task not to deadlock in watchq<- : we did not
z.at0Initialized = true // yet returned z to caller and so noone might be yet reading from watchq.
z.flushEventq0() go func() {
z.at0Mu.Unlock() z.at0Mu.Lock()
z.at0 = lastTid
z.at0Initialized = true
z.flushEventq0()
z.at0Mu.Unlock()
}()
//call('get_info') -> {}str->str, ex // XXX can be omitted //call('get_info') -> {}str->str, ex // XXX can be omitted
/* /*
...@@ -520,11 +542,15 @@ func openByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) (_ zodb ...@@ -520,11 +542,15 @@ func openByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) (_ zodb
'supports_record_iternext': True}) 'supports_record_iternext': True})
*/ */
return z, z.at0, nil return z, lastTid, nil
} }
func (z *zeo) Close() error { func (z *zeo) Close() error {
err := z.link.Close() var err error
z.closeOnce.Do(func() {
close(z.closed)
err = z.link.Close()
})
z.serveWG.Wait() z.serveWG.Wait()
return err return err
} }
......
...@@ -153,7 +153,7 @@ func (z *ZEOPySrv) Encoding() encoding { ...@@ -153,7 +153,7 @@ func (z *ZEOPySrv) Encoding() encoding {
// ---------------- // ----------------
// tOptions represents options for testing. // tOptions represents options for testing.
// XXX dup in NEO // TODO -> xtesting
type tOptions struct { type tOptions struct {
Preload string // preload database with data from this location Preload string // preload database with data from this location
} }
......
// Copyright (C) 2017 Nexedi SA and Contributors. // Copyright (C) 2017-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your // it under the terms of the GNU General Public License version 3, or (at your
...@@ -31,4 +31,5 @@ import ( ...@@ -31,4 +31,5 @@ import (
_ "lab.nexedi.com/kirr/neo/go/zodb/storage/fs1" _ "lab.nexedi.com/kirr/neo/go/zodb/storage/fs1"
_ "lab.nexedi.com/kirr/neo/go/zodb/storage/zeo" _ "lab.nexedi.com/kirr/neo/go/zodb/storage/zeo"
_ "lab.nexedi.com/kirr/neo/go/neo" _ "lab.nexedi.com/kirr/neo/go/neo"
_ "lab.nexedi.com/kirr/neo/go/zodb/storage/demo"
) )
// Copyright (C) 2016-2020 Nexedi SA and Contributors. // Copyright (C) 2016-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
...@@ -275,7 +275,7 @@ type NoObjectError struct { ...@@ -275,7 +275,7 @@ type NoObjectError struct {
Oid Oid Oid Oid
} }
func (e NoObjectError) Error() string { func (e *NoObjectError) Error() string {
return fmt.Sprintf("%s: no such object", e.Oid) return fmt.Sprintf("%s: no such object", e.Oid)
} }
......
...@@ -41,9 +41,10 @@ and using path to that file with zconfig:// schema: ...@@ -41,9 +41,10 @@ and using path to that file with zconfig:// schema:
There are also following simpler ways: There are also following simpler ways:
- neo://<master>/<db> for a NEO database XXX + neos:// ? - neo://<master>/<db> for a NEO database XXX + neos:// ?
- zeo://<host>:<port> for a ZEO database - zeo://<host>:<port> for a ZEO database
- /path/to/file for a FileStorage database - /path/to/file for a FileStorage database
- demo:(zurl_base)/(zurl_δ) for a DemoStorage overlay
Please see zodburi documentation for full details: Please see zodburi documentation for full details:
......
// Copyright (C) 2018-2020 Nexedi SA and Contributors. // Copyright (C) 2018-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
...@@ -202,46 +202,35 @@ func (δtail *ΔTail) ForgetPast(revCut Tid) { ...@@ -202,46 +202,35 @@ func (δtail *ΔTail) ForgetPast(revCut Tid) {
// XXX -> RevAt ? // XXX -> RevAt ?
// LastRevOf tries to return what was the last revision that changed id as of at database state. // LastRevOf tries to return what was the last revision that changed id as of at database state.
// //
// Depending on current information in δtail it returns either exact result, or // it must be called with the following condition:
// an upper-bound estimate for the last id revision, assuming id was changed ≤ at:
// //
// 1) if δtail does not cover at, at is returned: // tail ≤ at ≤ head
// //
// # at ∉ [min(rev ∈ δtail), max(rev ∈ δtail)] // Depending on current information in δtail it returns either exact result, or
// LastRevOf(id, at) = at // an upper-bound estimate for the last id revision:
// //
// 2) if δtail has an entry corresponding to id change, it gives exactly the // 1) if δtail has an entry corresponding to id change, it gives exactly the
// last revision that changed id: // last revision that changed id:
// //
// # at ∈ [min(rev ∈ δtail), max(rev ∈ δtail)]
// # ∃ rev ∈ δtail: rev changed id && rev ≤ at // # ∃ rev ∈ δtail: rev changed id && rev ≤ at
// LastRevOf(id, at) = max(rev: rev changed id && rev ≤ at) // LastRevOf(id, at) = max(rev: rev changed id && rev ≤ at), true
// //
// 3) if δtail does not contain appropriate record with id - it returns δtail's // 2) if δtail does not contain appropriate record with id - it returns δtail's
// lower bound as the estimate for the upper bound of the last id revision: // lower bound as the estimate for the upper bound of the last id revision:
// //
// # at ∈ [min(rev ∈ δtail), max(rev ∈ δtail)]
// # ∄ rev ∈ δtail: rev changed id && rev ≤ at // # ∄ rev ∈ δtail: rev changed id && rev ≤ at
// LastRevOf(id, at) = min(rev ∈ δtail) // LastRevOf(id, at) = δtail.tail, false
// //
// On return exact indicates whether returned revision is exactly the last // On return exact indicates whether returned revision is exactly the last
// revision of id, or only an upper-bound estimate of it. // revision of id, or only an upper-bound estimate of it.
func (δtail *ΔTail) LastRevOf(id Oid, at Tid) (_ Tid, exact bool) { func (δtail *ΔTail) LastRevOf(id Oid, at Tid) (_ Tid, exact bool) {
// check if we have no coverage at all if !(δtail.tail <= at && at <= δtail.head) {
l := len(δtail.tailv) panic(fmt.Sprintf("at out of bounds: at: @%s, (tail, head] = (@%s, @%s]", at, δtail.tail, δtail.head))
if l == 0 { }
return at, false
}
revMin := δtail.tailv[0].Rev
revMax := δtail.tailv[l-1].Rev
if !(revMin <= at && at <= revMax) {
return at, false
}
// we have the coverage
rev, ok := δtail.lastRevOf[id] rev, ok := δtail.lastRevOf[id]
if !ok { if !ok {
return δtail.tailv[0].Rev, false return δtail.tail, false
} }
if rev <= at { if rev <= at {
...@@ -250,7 +239,7 @@ func (δtail *ΔTail) LastRevOf(id Oid, at Tid) (_ Tid, exact bool) { ...@@ -250,7 +239,7 @@ func (δtail *ΔTail) LastRevOf(id Oid, at Tid) (_ Tid, exact bool) {
// what's in index is after at - scan tailv back to find appropriate entry // what's in index is after at - scan tailv back to find appropriate entry
// XXX linear scan - see .lastRevOf comment. // XXX linear scan - see .lastRevOf comment.
for i := l - 1; i >= 0; i-- { for i := len(δtail.tailv) - 1; i >= 0; i-- {
δ := δtail.tailv[i] δ := δtail.tailv[i]
if δ.Rev > at { if δ.Rev > at {
continue continue
...@@ -264,5 +253,5 @@ func (δtail *ΔTail) LastRevOf(id Oid, at Tid) (_ Tid, exact bool) { ...@@ -264,5 +253,5 @@ func (δtail *ΔTail) LastRevOf(id Oid, at Tid) (_ Tid, exact bool) {
} }
// nothing found // nothing found
return δtail.tailv[0].Rev, false return δtail.tail, false
} }
// Copyright (C) 2018-2019 Nexedi SA and Contributors. // Copyright (C) 2018-2021 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
...@@ -174,14 +174,13 @@ func TestΔTail(t *testing.T) { ...@@ -174,14 +174,13 @@ func TestΔTail(t *testing.T) {
δtail = NewΔTail(3) δtail = NewΔTail(3)
δCheck(3,3) δCheck(3,3)
δCheckLastUP(4, 12, 12) // δtail = ø δCheckLastUP(3, 3, 3) // δtail = ø
δAppend(R(10, 3,5)) δAppend(R(10, 3,5))
δCheck(3,10, R(10, 3,5)) δCheck(3,10, R(10, 3,5))
δCheckLastUP(3, 2, 2) // at < δtail δCheckLastUP(3, 9, 3) // id ∈ δtail, but has no entry with rev ≤ at
δCheckLastUP(3, 12, 12) // at > δtail δCheckLastUP(4, 10, 3) // id ∉ δtail
δCheckLastUP(4, 10, 10) // id ∉ δtail
δAppend(R(11, 7)) δAppend(R(11, 7))
δCheck(3,11, R(10, 3,5), R(11, 7)) δCheck(3,11, R(10, 3,5), R(11, 7))
...@@ -192,7 +191,7 @@ func TestΔTail(t *testing.T) { ...@@ -192,7 +191,7 @@ func TestΔTail(t *testing.T) {
δAppend(R(14, 3,8)) δAppend(R(14, 3,8))
δCheck(3,14, R(10, 3,5), R(11, 7), R(12, 7), R(14, 3,8)) δCheck(3,14, R(10, 3,5), R(11, 7), R(12, 7), R(14, 3,8))
δCheckLastUP(8, 12, 10) // id ∈ δtail, but has no entry with rev ≤ at δCheckLastUP(8, 12, 3) // id ∈ δtail, but has no entry with rev ≤ at
δtail.ForgetPast(9) δtail.ForgetPast(9)
δCheck(9,14, R(10, 3,5), R(11, 7), R(12, 7), R(14, 3,8)) δCheck(9,14, R(10, 3,5), R(11, 7), R(12, 7), R(14, 3,8))
......
...@@ -84,6 +84,7 @@ def _resolve_uri(uri): ...@@ -84,6 +84,7 @@ def _resolve_uri(uri):
if scheme != "neos": if scheme != "neos":
raise ValueError("invalid uri: %s : credentials can be specified only with neos:// scheme" % uri) raise ValueError("invalid uri: %s : credentials can be specified only with neos:// scheme" % uri)
# ca=ca.crt;cert=my.crt;key=my.key # ca=ca.crt;cert=my.crt;key=my.key
cred = cred.replace(';', '&') # ; is no longer in default separators set bugs.python.org/issue42967
for k, v in OrderedDict(parse_qsl(cred)).items(): for k, v in OrderedDict(parse_qsl(cred)).items():
if k not in _credopts: if k not in _credopts:
raise ValueError("invalid uri: %s : unexpected credential %s" % (uri, k)) raise ValueError("invalid uri: %s : unexpected credential %s" % (uri, k))
......
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
MIIC7TCCAdWgAwIBAgIJAL8e44sA7PDMMA0GCSqGSIb3DQEBCwUAMA0xCzAJBgNV MIIC+zCCAeOgAwIBAgIUN2aMGxxsLX4IyuSR2DRfzbTumrAwDQYJKoZIhvcNAQEL
BAMMAkNBMB4XDTE1MDkzMDEzNTQzMFoXDTIxMDMyMjEzNTQzMFowDTELMAkGA1UE BQAwDTELMAkGA1UEAwwCQ0EwHhcNMjEwMzIyMTU1MzI5WhcNMjYwOTEyMTU1MzI5
AwwCQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgT7DVKM4ViQt3 WjANMQswCQYDVQQDDAJDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
B0oJC4RGi10dNfpNZZpgA5iC2UJ1u6AqqCf0PCQkrmuIzW3l1TenlOiLNdVASkkT AMVipTeJN6FEpdF6JJkkVw7vPR14pXCMehSWxBMrmICVAHct4fWz4sI/lp3VTiFl
wf1lekIgg4tR8/22oGTAnfY6R9r1C6jAMV72v1sffz8D6qfkMPzKchJt55zywdhm HnEBOhehLsKfZyosGIMpJ0zI2GsQopkHa5ZPf/cILrQ3ybu7ZsQITAyYKWqhMVdt
KscUsMGzXPGIeKrG20m83dSIO4RmCmq/f4BcuWJu6Kkq4n9Wc2IsvpKk+lqEUxI/ d+YRetfkE874gUHqTo+WSBpCbRvuYK4XhBEmrMiwIHV7nwuIaKMdG6FxNGn8du8R
QoqdT6OvMXooGs3t892uvKDu++muBj2Y/yyaXt1tCCjDFsRMLWl3Skks+4PeMCZ4 Ti96VduJGdjTFIRri2jNP47VKidioPkxecA/48yrDdewn/cxsU/yNCO9Uii/dTyS
wugyXEBk3d5Yzdv5NsFzFBjAuRCGxJXEOEcfHj4Xj9qTCErZ1jKzgnuxJCtgdqRC Ro/Jqxql6Leb7xaZlODS5QQAGWjAI6w0BCobwYaUuJOZ1HSUL6A42rnMqI6gGjCY
r4beX1U3AgMBAAGjUDBOMB0GA1UdDgQWBBSFY/jKvo0iSTEzzOIcZZUZCT8JfTAf Dju313RlE5jOilWQhodmawECAwEAAaNTMFEwHQYDVR0OBBYEFBfzeqZl9fHZR2bH
BgNVHSMEGDAWgBSFY/jKvo0iSTEzzOIcZZUZCT8JfTAMBgNVHRMEBTADAQH/MA0G rb+T6+O6mu1aMB8GA1UdIwQYMBaAFBfzeqZl9fHZR2bHrb+T6+O6mu1aMA8GA1Ud
CSqGSIb3DQEBCwUAA4IBAQAB0LKDAuhodpyNVwEE9Yl+Q/IiPEPCaix6URJnRn1O EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAK6+04BOOP/PF6BR6FaZjLHN
gQnXuZLo1xtJh6wJh1faG1/qNCFMxWEJ+0VkJ7r6v38cNXfYG9OcmD0S6YnNjSuO qhcSBfG4Y8RyLtERwGTQW68PwCU2TloJgpoY8updT0V9EsxnkWjsU/Wgnl8pdHjp
VliAtqVVtj8MppJ4vMatLrNi4cvyYucebtNyBCzSIAi+6bkkHeaVgi1EtxXvq+AS GMAivelTcDoYH4qn8E6mDEKE47Be4fE0FdGvU9QU7UBWG+Uf8pxgnFJcGou5grLG
iZp3gl84oXv/gV7Bz4SXmVpFJnhsDMoQZG2KAULAgfZ2Am2I+ffG90cD/oEnS/3O RXTSNCSrBnJG9tmSPA/aw/Q9MVfXFuB7K8aPJ/d2Z/8v/QFdBK25JH8I+ErFU3ys
k3btqTvgIO8MWt8PY3sUOhJEoJYKnC9DppmhOhUTn4zzIIDSluKEOBHZiFb9AcmF ufn58Jqsy4dp8R4qYVP1TKPfGsZ+RZeIIJ4cDIik/vbv9+Ymi+m8LZ66kUNq1d4X
PvzL+8xiORCdUe1d6ANQQlUd0MM810BXZFYEXFbgKg8o tGbPjcNt/qFQ/FG7T6ZUPe9vRpYAhIHGBYZFs59LJcQsH1DzSvei3141Gs7TYFg=
-----END CERTIFICATE----- -----END CERTIFICATE-----
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
MIICkDCCAXgCAQEwDQYJKoZIhvcNAQELBQAwDTELMAkGA1UEAwwCQ0EwHhcNMTUw MIICkDCCAXgCAQEwDQYJKoZIhvcNAQELBQAwDTELMAkGA1UEAwwCQ0EwHhcNMjEw
OTMwMTM1NDMwWhcNMjEwMzIyMTM1NDMwWjAPMQ0wCwYDVQQDDARub2RlMIIBIjAN MzIyMTU1MzI5WhcNMjYwOTEyMTU1MzI5WjAPMQ0wCwYDVQQDDARub2RlMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJ+ClJyjhJOJdGUyqHn79opMLP3m BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJ+ClJyjhJOJdGUyqHn79opMLP3m
1g27uBWKT+OGd4FcreVoRDxPVuxZxMtDCZcBfUHVvOoSlS06khwSxViEe1hxwHRa 1g27uBWKT+OGd4FcreVoRDxPVuxZxMtDCZcBfUHVvOoSlS06khwSxViEe1hxwHRa
n2qMWlwvaWeNY0CFH5V+DI4XSNojgny85Lb5jB69FuPcrHnwxLk2OFntrXEeNbEa n2qMWlwvaWeNY0CFH5V+DI4XSNojgny85Lb5jB69FuPcrHnwxLk2OFntrXEeNbEa
d7QSoNbPajbJIp5BS/WR9iu5Z5JYdumLWjTvOU+eZc4iA6Wa2kdDtbqkGi4wOJ1L d7QSoNbPajbJIp5BS/WR9iu5Z5JYdumLWjTvOU+eZc4iA6Wa2kdDtbqkGi4wOJ1L
/ggATL+p+QFcubVptztPT8vq7gvDdGJgXLJ2lPHV0V/sdJB1FB4mSJEDDjSm2Hpp /ggATL+p+QFcubVptztPT8vq7gvDdGJgXLJ2lPHV0V/sdJB1FB4mSJEDDjSm2Hpp
qPVJSO1GrAy5Ld+0SnXIZZejhIUJIumocY08r+vzDSQ/8NnqXR4Odz1TWwIDAQAB qPVJSO1GrAy5Ld+0SnXIZZejhIUJIumocY08r+vzDSQ/8NnqXR4Odz1TWwIDAQAB
MA0GCSqGSIb3DQEBCwUAA4IBAQBlYkkInDDDcgnNRdUmzxwejs1PmEehZ3H5FkMp MA0GCSqGSIb3DQEBCwUAA4IBAQAvtJztgZ8TuY5Q52Lv4hE2WX3/IZQPYc9cGQqv
TsmpoVC+oqM+QywMu8UJRtCjXnnJdAUbVYuZ1Tjm7qvFIhN+5OlIVxJ+8WcmZPSe sI0xKQEWD4hVHAmxL7bLFOUovzFqNMY0pYBGVJ53nypsKNorKbiepSMPosfcbk9o
lj0N7Dv2nE1diTDS+qPZVPZ0demo1LafRmPomPWiM/CQRlMPxXnimuiYOROhWGn6 U8xF9YDJO+yS6V2lWFi6iKe4WX2t+L2j5x+q4dptJoIvsM777xGatri59q8UK3ne
jsyoOwquMkAc6Ub++l4OCxLAP0eTgJFkivmqpaYZXG4o7zFvcQ3rQ66rQrMl69sR YTINui8+tsOIGEpcXMHMI/k3RvBUS3Mwy0pX6rsi8/YsUVUShpPDjuAqQIYxU4gP
8/MVqbT5Sq1CEJbepP4GaFfa5l3CVy7WH2MhCV1/9mNwcXafkTgx3q2HsPon4Dze qAIWrhXNYwOGuMQ1V+j83R5lqDoHrVauWznc0n0qg/EJArhwkLGfjQoqrogCQOVp
kNwiguNAM4L/j4dbIwz+CIVWcgpBCrfv2JYu+jGlRpxIDeWR iMEV6YDLC8EMqZfYVUT0i2+/bS737Z2lcdgz2essW45nk7+H
-----END CERTIFICATE----- -----END CERTIFICATE-----
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment