mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
This provides us with most of the existing Tx interface, split
across QueryRead and QueryWrite. The functions not included here
are the ones that are used *only* for anti-entropy (ForEach
and ForEachRange).
We add additional testing to verify that TxStores are getting
closed correctly, to go with cleaning up the test directories they're
made in.
We also introduce some test wrappers that can automatically
fail tests on error, so tests don't need to be full of error
checks.
Also, now that I'm starting to think more about the flow of
writing tests using QueryScope, we add the missing "full
database" scope option, and make the Add methods return
their operand so (1) you can chain them, (2) you can use
the AddIndex(...) inline in a NewWriteQueryContext.
Also addressed a plausible performance concern in shardList,
and some comments that were stale or incorrect.
The test coverage here is skimpy on the actual RBF-calling
functions because those are trivial. We do, however, significantly
expand coverage in the random write requests, which are now
a mix of random writes and random reads, and add test cases
that at least hit a lot of the error checks once.
The Error() method is changed to be like (testing.T).Error(),
taking ...interface{} and using fmt.Sprint on them.
There's also some minor tweaks such as making the visualizations
more consistent, testing visualization generation on two kinds
of keysplitter, and so on.
401 lines
9.7 KiB
Go
401 lines
9.7 KiB
Go
package querycontext
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// Rendering time!
|
|
// We want to be able to render an rbfTxStore, or a QueryContext, or the whole system.
|
|
//
|
|
// Here's the things we have:
|
|
// rbfDBQueryContexts
|
|
// rbfTxStore
|
|
// rbfTxWrappers
|
|
// rbfQueryContext
|
|
// rbfQueryRead
|
|
// rbfQueryWrite
|
|
// rbf.DB
|
|
// rbf.Tx
|
|
// QueryScope
|
|
//
|
|
// Relationships that exist:
|
|
// rbfTxStore -> map[dbKey]rbfDBQueryContexts -> {one rbf.DB, map[rbfTxWrappers]rbfQueryContext}
|
|
// rbfQueryContext -> many rbfTxWrappers -> {one rbf.Tx, one rbfDBQueryContext, map[txKey]QueryRead}
|
|
// rbfQueryRead -> rbfTxWrappers
|
|
//
|
|
// For a TxStore, it's useful to graph the set of rbfDBQueryContexts it
|
|
// has (which represent live databases), and the QueryContexts associated
|
|
// with them. For a QueryContext, it's useful to graph the set of
|
|
// rbfTxWrappers it has, and also its scope.
|
|
//
|
|
// It is also interesting to graph the whole thing. There should always
|
|
// be a round trip in these relations; an rbfTxWrapper always maps to
|
|
// an rbfDBQueryContext, which should always have a reference back to
|
|
// the rbfQueryContext that owns the rbfTxWrapper. Similarly, if
|
|
// you have an rbfDBQueryContext, every rbfTxWrapper it references
|
|
// has a Tx against the associated DB.
|
|
|
|
// colors: ['#d7191c','#fdae61','#ffffbf','#abd9e9','#2c7bb6'] is a
|
|
// colorblind-friendly palette obtained from ColorBrewer. We assign
|
|
// meanings:
|
|
const (
|
|
colorBlack = "#000000"
|
|
colorWhite = "#ffffff"
|
|
colorWrite = "#fdae61"
|
|
colorRead = "#abd9e9"
|
|
colorError = "#d7191c"
|
|
colorUnused1 = "#ffffbf"
|
|
colorUnused2 = "##2c7bb6"
|
|
)
|
|
|
|
var colorWritable = map[bool]string{
|
|
false: colorRead,
|
|
true: colorWrite,
|
|
}
|
|
|
|
type dotNode interface {
|
|
writeNode(io.Writer)
|
|
addTo(*dotGraph)
|
|
dotId() string
|
|
dotClass() string
|
|
}
|
|
|
|
type dotEdge interface {
|
|
writeEdge(io.Writer, *dotGraph)
|
|
}
|
|
|
|
// dotGraph stores a list of nodes and edges it's found so far.
|
|
// we also have a queue. the reason is that if you don't, descent
|
|
// is depth-first, and this can lead to finding a node with very
|
|
// little depth left before you would have found it with a higher
|
|
// depth, and potentially not recording its edges.
|
|
type dotGraph struct {
|
|
depth int
|
|
queue []dotNode
|
|
edges []dotEdge
|
|
nodes map[dotNode]string
|
|
orderedNodes []dotNode
|
|
clusters map[string][]dotNode
|
|
}
|
|
|
|
func (dg *dotGraph) enqueue(n dotNode) {
|
|
if _, ok := dg.nodes[n]; ok {
|
|
return
|
|
}
|
|
dg.queue = append(dg.queue, n)
|
|
}
|
|
|
|
// build starts with the enqueued roots and processes
|
|
// them, looking for new things, depth times.
|
|
func (dg *dotGraph) build(depth int) {
|
|
if dg.nodes == nil {
|
|
dg.nodes = make(map[dotNode]string)
|
|
}
|
|
dg.depth = depth
|
|
for len(dg.queue) > 0 && dg.depth > 0 {
|
|
working := dg.queue
|
|
dg.queue = []dotNode{}
|
|
for _, node := range working {
|
|
node.addTo(dg)
|
|
}
|
|
dg.depth--
|
|
}
|
|
}
|
|
|
|
// consider checks whether we're sufficiently done with this item, which
|
|
// means either we've already seen it, or we haven't but we're out of depth.
|
|
func (dg *dotGraph) consider(n dotNode) bool {
|
|
if _, ok := dg.nodes[n]; ok {
|
|
return true
|
|
}
|
|
dg.nodes[n] = n.dotId()
|
|
// maintain a list in the order we saw them, so items at the top of
|
|
// a given list will correspond to items at the top of the next list too
|
|
dg.orderedNodes = append(dg.orderedNodes, n)
|
|
return dg.depth < 1
|
|
}
|
|
|
|
var nodeQualities = map[string][2]map[string]string{
|
|
"rdqc": {{"rank": "same"}, {"shape": "cylinder"}},
|
|
"txw": {{"rank": "same"}, {"shape": "folder"}},
|
|
"qrw": {{"rank": "same"}, {"shape": "note"}},
|
|
"txstore": {{"rank": "same"}, {"shape": "hexagon"}},
|
|
"rqc": {{"rank": "same"}, {"shape": "box3d"}},
|
|
}
|
|
|
|
var nodeOrder = []string{"txstore", "rdqc", "txw", "qrw", "rqc"}
|
|
|
|
func (dg *dotGraph) WriteClass(w io.Writer, class string) error {
|
|
nodes := dg.clusters[class]
|
|
if len(nodes) == 0 {
|
|
return nil
|
|
}
|
|
fmt.Fprintf(w, "\tsubgraph %s {\n", class)
|
|
if qualities, ok := nodeQualities[class]; ok {
|
|
for k, v := range qualities[0] {
|
|
fmt.Fprintf(w, "\t\t%s=%q\n", k, v)
|
|
}
|
|
if len(qualities[1]) > 0 {
|
|
fmt.Fprintf(w, "\t\tnode [")
|
|
for k, v := range qualities[1] {
|
|
fmt.Fprintf(w, "%s=%q", k, v)
|
|
}
|
|
|
|
fmt.Fprintln(w, "]")
|
|
}
|
|
}
|
|
for _, node := range nodes {
|
|
fmt.Fprintf(w, "\t\t\"%s\" ", dg.nodes[node])
|
|
node.writeNode(w)
|
|
fmt.Fprintln(w, "")
|
|
}
|
|
fmt.Fprintf(w, "\t}\n")
|
|
return nil
|
|
}
|
|
|
|
func (dg *dotGraph) Write(w io.Writer) error {
|
|
w.Write([]byte(`
|
|
digraph g {
|
|
rankdir = "LR"
|
|
newrank = true
|
|
concentrate = true
|
|
`))
|
|
dg.clusters = make(map[string][]dotNode)
|
|
classes := make(map[string]struct{})
|
|
for _, node := range dg.orderedNodes {
|
|
class := node.dotClass()
|
|
dg.clusters[class] = append(dg.clusters[class], node)
|
|
classes[class] = struct{}{}
|
|
}
|
|
for _, class := range nodeOrder {
|
|
err := dg.WriteClass(w, class)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delete(classes, class)
|
|
}
|
|
if len(classes) > 0 {
|
|
return fmt.Errorf("unordered classes: %v", classes)
|
|
}
|
|
for _, edge := range dg.edges {
|
|
edge.writeEdge(w, dg)
|
|
}
|
|
fmt.Fprintln(w, "}")
|
|
return nil
|
|
}
|
|
|
|
// pointerEdge is a naive edge that just represents two pointer-shaped things
|
|
type pointerEdge struct {
|
|
from, to dotNode
|
|
label string
|
|
style string
|
|
}
|
|
|
|
func (d *pointerEdge) writeEdge(w io.Writer, dg *dotGraph) {
|
|
var style string
|
|
if d.style != "" {
|
|
style = fmt.Sprintf(" style=%q", d.style)
|
|
}
|
|
fmt.Fprintf(w, " %q -> %q [label=%q%s]\n", dg.nodes[d.from], dg.nodes[d.to], d.label, style)
|
|
}
|
|
|
|
func (dg *dotGraph) addEdge(from, to dotNode, extra ...string) {
|
|
var label, style string
|
|
switch len(extra) {
|
|
case 2:
|
|
style = extra[1]
|
|
fallthrough
|
|
case 1:
|
|
label = extra[0]
|
|
}
|
|
edge := &pointerEdge{from, to, label, style}
|
|
dg.edges = append(dg.edges, edge)
|
|
}
|
|
|
|
// RBF backend to Graphviz glue
|
|
|
|
func (r *rbfTxStore) addTo(dg *dotGraph) {
|
|
if dg.consider(r) {
|
|
return
|
|
}
|
|
for _, dbqc := range r.dbs {
|
|
dg.enqueue(dbqc)
|
|
dg.addEdge(r, dbqc)
|
|
}
|
|
}
|
|
|
|
func (r *rbfDBQueryContexts) addTo(dg *dotGraph) {
|
|
if dg.consider(r) {
|
|
return
|
|
}
|
|
for tx, qc := range r.queryContexts {
|
|
dg.enqueue(qc)
|
|
dg.enqueue(tx)
|
|
dg.addEdge(r, qc)
|
|
dg.addEdge(r, tx, qc.name)
|
|
}
|
|
}
|
|
|
|
func (r *rbfQueryContext) addTo(dg *dotGraph) {
|
|
if dg.consider(r) {
|
|
return
|
|
}
|
|
dg.addEdge(r, r.txStore, "", "dashed")
|
|
for _, wrappers := range r.queries {
|
|
dg.enqueue(wrappers)
|
|
dg.addEdge(r, wrappers)
|
|
}
|
|
}
|
|
|
|
func (r *rbfTxWrappers) addTo(dg *dotGraph) {
|
|
if dg.consider(r) {
|
|
return
|
|
}
|
|
dg.enqueue(r.db)
|
|
dg.addEdge(r, r.db, "", "dashed")
|
|
for _, reader := range r.queries {
|
|
switch v := reader.(type) {
|
|
case *rbfQueryRead:
|
|
dg.enqueue(v)
|
|
dg.addEdge(r, v)
|
|
case *rbfQueryWrite:
|
|
dg.enqueue(v)
|
|
dg.addEdge(r, v)
|
|
default:
|
|
unk := &unknownEntry{v}
|
|
dg.enqueue(unk)
|
|
dg.addEdge(r, unk)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *rbfTxStore) dotClass() string {
|
|
return "txstore"
|
|
}
|
|
|
|
func (r *rbfTxStore) dotId() string {
|
|
return fmt.Sprintf("txstore-%p", r)
|
|
}
|
|
|
|
func (r *rbfTxStore) writeNode(w io.Writer) {
|
|
// long root path is annoying to read, so
|
|
temp := os.TempDir()
|
|
path := r.rootPath
|
|
if strings.HasPrefix(r.rootPath, temp+"/") {
|
|
path = ".../" + strings.TrimPrefix(r.rootPath, temp+"/")
|
|
}
|
|
fmt.Fprintf(w, `[label=<rbfTxStore<BR/><FONT POINT-SIZE="10" FACE="courier">%s</FONT>>]`, path)
|
|
}
|
|
|
|
func (r *rbfDBQueryContexts) dotClass() string {
|
|
return "rdqc"
|
|
}
|
|
|
|
func (r *rbfDBQueryContexts) dotId() string {
|
|
return fmt.Sprintf("rdqc-%p", r)
|
|
}
|
|
|
|
func (r *rbfDBQueryContexts) writeNode(w io.Writer) {
|
|
var locked string
|
|
var maybeError = colorBlack
|
|
if r.lockCheck.TryLock() {
|
|
r.lockCheck.Unlock()
|
|
} else {
|
|
locked = fmt.Sprintf("<BR/><FONT COLOR=%q>[LOCKED]</FONT>", colorWrite)
|
|
}
|
|
if r.db == nil {
|
|
maybeError = colorError
|
|
}
|
|
fmt.Fprintf(w, `[label=<rbfDBQueryContexts<BR/><FONT POINT-SIZE="12" FACE="courier">%s</FONT><BR/><FONT POINT-SIZE="12" FACE="courier">%s</FONT>%s> color=%q]`, r.key, r.dbPath, locked, maybeError)
|
|
}
|
|
|
|
func (r *rbfTxWrappers) dotClass() string {
|
|
return "txw"
|
|
}
|
|
|
|
func (r *rbfTxWrappers) dotId() string {
|
|
return fmt.Sprintf("txw-%p", r)
|
|
}
|
|
|
|
func (r *rbfTxWrappers) writeNode(w io.Writer) {
|
|
var write string
|
|
if r.writeTx {
|
|
write = "write "
|
|
}
|
|
fmt.Fprintf(w, `[label=<rbfTxWrappers<BR/><FONT POINT-SIZE="12" FACE="courier">%s</FONT><BR/>[%sTx %p]> style="filled" fillcolor=%q]`, r.key, write, r.tx, colorWritable[r.writeTx])
|
|
}
|
|
|
|
func (r *rbfQueryContext) dotClass() string {
|
|
return "rqc"
|
|
}
|
|
|
|
func (r *rbfQueryContext) dotId() string {
|
|
return fmt.Sprintf("rqc-%p", r)
|
|
}
|
|
|
|
func (r *rbfQueryContext) writeNode(w io.Writer) {
|
|
writeScope := "[read only]"
|
|
if r.scope != nil {
|
|
writeScope = r.scope.String()
|
|
if len(writeScope) > 20 {
|
|
writeScope = writeScope[:20] + "..."
|
|
}
|
|
writeScope = fmt.Sprintf("scope: %s", writeScope)
|
|
}
|
|
fmt.Fprintf(w, `[label=<rbfQueryContext<BR/>%s<BR/>%s> style="filled" fillcolor=%q]`, r.name, writeScope, colorWritable[r.scope != nil])
|
|
}
|
|
|
|
func (r *rbfQueryRead) dotClass() string {
|
|
return "qrw"
|
|
}
|
|
|
|
func (r *rbfQueryRead) dotId() string {
|
|
return fmt.Sprintf("qrw-%p", r)
|
|
}
|
|
|
|
func (r *rbfQueryRead) addTo(dg *dotGraph) {
|
|
dg.consider(r)
|
|
}
|
|
|
|
func (r *rbfQueryRead) writeNode(w io.Writer) {
|
|
fmt.Fprintf(w, `[label=<rbfQueryRead<BR/>[<FONT POINT-SIZE="12" FACE="courier">%s</FONT>]> style="filled" fillcolor=%q]`, r.fk, colorRead)
|
|
}
|
|
|
|
func (r *rbfQueryWrite) dotClass() string {
|
|
return "qrw"
|
|
}
|
|
|
|
func (r *rbfQueryWrite) dotId() string {
|
|
return fmt.Sprintf("qrw-%p", r)
|
|
}
|
|
|
|
func (r *rbfQueryWrite) addTo(dg *dotGraph) {
|
|
dg.consider(r)
|
|
}
|
|
|
|
func (r *rbfQueryWrite) writeNode(w io.Writer) {
|
|
fmt.Fprintf(w, `[label=<rbfQueryWrite<BR/>[<FONT POINT-SIZE="12" FACE="courier">%s</FONT>]> style="filled" fillcolor=%q]`, r.fk, colorWrite)
|
|
}
|
|
|
|
type unknownEntry struct {
|
|
data interface{}
|
|
}
|
|
|
|
func (u *unknownEntry) dotClass() string {
|
|
return "unk"
|
|
}
|
|
|
|
func (u *unknownEntry) dotId() string {
|
|
return fmt.Sprintf("unk-%p", u)
|
|
}
|
|
|
|
func (u *unknownEntry) writeNode(w io.Writer) {
|
|
fmt.Fprintf(w, "[label=\"unknown %T\"]", u.data)
|
|
}
|
|
|
|
func (u *unknownEntry) addTo(dg *dotGraph) {
|
|
dg.consider(u)
|
|
}
|