Wrote write goroutine functions.

Refactored subpackages back to root package.
This commit is contained in:
Jay
2026-03-04 18:33:42 -05:00
parent f88982a0b7
commit 894eab5405
17 changed files with 450 additions and 217 deletions

View File

@@ -1,10 +1,8 @@
package graphstore package heartwood
import ( import (
"context" "context"
"fmt" "fmt"
"git.wisehodl.dev/jay/go-heartwood/cypher"
"git.wisehodl.dev/jay/go-heartwood/graph"
"github.com/neo4j/neo4j-go-driver/v6/neo4j" "github.com/neo4j/neo4j-go-driver/v6/neo4j"
"sort" "sort"
"strings" "strings"
@@ -16,7 +14,7 @@ type NodeBatch struct {
MatchLabel string MatchLabel string
Labels []string Labels []string
MatchKeys []string MatchKeys []string
Nodes []*graph.Node Nodes []*Node
} }
type RelBatch struct { type RelBatch struct {
@@ -25,24 +23,24 @@ type RelBatch struct {
StartMatchKeys []string StartMatchKeys []string
EndLabel string EndLabel string
EndMatchKeys []string EndMatchKeys []string
Rels []*graph.Relationship Rels []*Relationship
} }
type BatchSubgraph struct { type BatchSubgraph struct {
nodes map[string][]*graph.Node nodes map[string][]*Node
rels map[string][]*graph.Relationship rels map[string][]*Relationship
matchProvider graph.MatchKeysProvider matchProvider MatchKeysProvider
} }
func NewBatchSubgraph(matchProvider graph.MatchKeysProvider) *BatchSubgraph { func NewBatchSubgraph(matchProvider MatchKeysProvider) *BatchSubgraph {
return &BatchSubgraph{ return &BatchSubgraph{
nodes: make(map[string][]*graph.Node), nodes: make(map[string][]*Node),
rels: make(map[string][]*graph.Relationship), rels: make(map[string][]*Relationship),
matchProvider: matchProvider, matchProvider: matchProvider,
} }
} }
func (s *BatchSubgraph) AddNode(node *graph.Node) error { func (s *BatchSubgraph) AddNode(node *Node) error {
// Verify that the node has defined match property values. // Verify that the node has defined match property values.
matchLabel, _, err := node.MatchProps(s.matchProvider) matchLabel, _, err := node.MatchProps(s.matchProvider)
@@ -54,16 +52,16 @@ func (s *BatchSubgraph) AddNode(node *graph.Node) error {
batchKey := createNodeBatchKey(matchLabel, node.Labels.ToArray()) batchKey := createNodeBatchKey(matchLabel, node.Labels.ToArray())
if _, exists := s.nodes[batchKey]; !exists { if _, exists := s.nodes[batchKey]; !exists {
s.nodes[batchKey] = []*graph.Node{} s.nodes[batchKey] = []*Node{}
} }
// Add the node to the subgraph. // Add the node to the sub
s.nodes[batchKey] = append(s.nodes[batchKey], node) s.nodes[batchKey] = append(s.nodes[batchKey], node)
return nil return nil
} }
func (s *BatchSubgraph) AddRel(rel *graph.Relationship) error { func (s *BatchSubgraph) AddRel(rel *Relationship) error {
// Verify that the start node has defined match property values. // Verify that the start node has defined match property values.
startLabel, _, err := rel.Start.MatchProps(s.matchProvider) startLabel, _, err := rel.Start.MatchProps(s.matchProvider)
@@ -81,10 +79,10 @@ func (s *BatchSubgraph) AddRel(rel *graph.Relationship) error {
batchKey := createRelBatchKey(rel.Type, startLabel, endLabel) batchKey := createRelBatchKey(rel.Type, startLabel, endLabel)
if _, exists := s.rels[batchKey]; !exists { if _, exists := s.rels[batchKey]; !exists {
s.rels[batchKey] = []*graph.Relationship{} s.rels[batchKey] = []*Relationship{}
} }
// Add the relationship to the subgraph. // Add the relationship to the sub
s.rels[batchKey] = append(s.rels[batchKey], rel) s.rels[batchKey] = append(s.rels[batchKey], rel)
return nil return nil
@@ -287,10 +285,10 @@ func MergeNodes(
tx neo4j.ManagedTransaction, tx neo4j.ManagedTransaction,
batch NodeBatch, batch NodeBatch,
) (*neo4j.ResultSummary, error) { ) (*neo4j.ResultSummary, error) {
cypherLabels := cypher.ToCypherLabels(batch.Labels) cypherLabels := ToCypherLabels(batch.Labels)
cypherProps := cypher.ToCypherProps(batch.MatchKeys, "node.") cypherProps := ToCypherProps(batch.MatchKeys, "node.")
serializedNodes := []*graph.SerializedNode{} serializedNodes := []*SerializedNode{}
for _, node := range batch.Nodes { for _, node := range batch.Nodes {
serializedNodes = append(serializedNodes, node.Serialize()) serializedNodes = append(serializedNodes, node.Serialize())
} }
@@ -326,13 +324,13 @@ func MergeRels(
tx neo4j.ManagedTransaction, tx neo4j.ManagedTransaction,
batch RelBatch, batch RelBatch,
) (*neo4j.ResultSummary, error) { ) (*neo4j.ResultSummary, error) {
cypherType := cypher.ToCypherLabel(batch.Type) cypherType := ToCypherLabel(batch.Type)
startCypherLabel := cypher.ToCypherLabel(batch.StartLabel) startCypherLabel := ToCypherLabel(batch.StartLabel)
endCypherLabel := cypher.ToCypherLabel(batch.EndLabel) endCypherLabel := ToCypherLabel(batch.EndLabel)
startCypherProps := cypher.ToCypherProps(batch.StartMatchKeys, "rel.start.") startCypherProps := ToCypherProps(batch.StartMatchKeys, "rel.start.")
endCypherProps := cypher.ToCypherProps(batch.EndMatchKeys, "rel.end.") endCypherProps := ToCypherProps(batch.EndMatchKeys, "rel.end.")
serializedRels := []*graph.SerializedRel{} serializedRels := []*SerializedRel{}
for _, rel := range batch.Rels { for _, rel := range batch.Rels {
serializedRels = append(serializedRels, rel.Serialize()) serializedRels = append(serializedRels, rel.Serialize())
} }

View File

@@ -1,7 +1,6 @@
package graphstore package heartwood
import ( import (
"git.wisehodl.dev/jay/go-heartwood/graph"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"testing" "testing"
) )
@@ -41,21 +40,21 @@ func TestRelBatchKey(t *testing.T) {
} }
func TestBatchSubgraphAddNode(t *testing.T) { func TestBatchSubgraphAddNode(t *testing.T) {
matchKeys := graph.NewSimpleMatchKeys() matchKeys := NewSimpleMatchKeys()
subgraph := NewBatchSubgraph(matchKeys) subgraph := NewBatchSubgraph(matchKeys)
node := graph.NewEventNode("abc123") node := NewEventNode("abc123")
err := subgraph.AddNode(node) err := subgraph.AddNode(node)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 1, subgraph.NodeCount()) assert.Equal(t, 1, subgraph.NodeCount())
assert.Equal(t, []*graph.Node{node}, subgraph.nodes["Event:Event"]) assert.Equal(t, []*Node{node}, subgraph.nodes["Event:Event"])
} }
func TestBatchSubgraphAddNodeInvalid(t *testing.T) { func TestBatchSubgraphAddNodeInvalid(t *testing.T) {
matchKeys := graph.NewSimpleMatchKeys() matchKeys := NewSimpleMatchKeys()
subgraph := NewBatchSubgraph(matchKeys) subgraph := NewBatchSubgraph(matchKeys)
node := graph.NewNode("Event", graph.Properties{}) node := NewNode("Event", Properties{})
err := subgraph.AddNode(node) err := subgraph.AddNode(node)
@@ -64,24 +63,24 @@ func TestBatchSubgraphAddNodeInvalid(t *testing.T) {
} }
func TestBatchSubgraphAddRel(t *testing.T) { func TestBatchSubgraphAddRel(t *testing.T) {
matchKeys := graph.NewSimpleMatchKeys() matchKeys := NewSimpleMatchKeys()
subgraph := NewBatchSubgraph(matchKeys) subgraph := NewBatchSubgraph(matchKeys)
userNode := graph.NewUserNode("pubkey1") userNode := NewUserNode("pubkey1")
eventNode := graph.NewEventNode("abc123") eventNode := NewEventNode("abc123")
rel := graph.NewSignedRel(userNode, eventNode, nil) rel := NewSignedRel(userNode, eventNode, nil)
err := subgraph.AddRel(rel) err := subgraph.AddRel(rel)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 1, subgraph.RelCount()) assert.Equal(t, 1, subgraph.RelCount())
assert.Equal(t, []*graph.Relationship{rel}, subgraph.rels["SIGNED,User,Event"]) assert.Equal(t, []*Relationship{rel}, subgraph.rels["SIGNED,User,Event"])
} }
func TestNodeBatches(t *testing.T) { func TestNodeBatches(t *testing.T) {
matchKeys := graph.NewSimpleMatchKeys() matchKeys := NewSimpleMatchKeys()
subgraph := NewBatchSubgraph(matchKeys) subgraph := NewBatchSubgraph(matchKeys)
node := graph.NewEventNode("abc123") node := NewEventNode("abc123")
subgraph.AddNode(node) subgraph.AddNode(node)
batches, err := subgraph.NodeBatches() batches, err := subgraph.NodeBatches()
@@ -91,15 +90,15 @@ func TestNodeBatches(t *testing.T) {
assert.Equal(t, "Event", batches[0].MatchLabel) assert.Equal(t, "Event", batches[0].MatchLabel)
assert.ElementsMatch(t, []string{"Event"}, batches[0].Labels) assert.ElementsMatch(t, []string{"Event"}, batches[0].Labels)
assert.ElementsMatch(t, []string{"id"}, batches[0].MatchKeys) assert.ElementsMatch(t, []string{"id"}, batches[0].MatchKeys)
assert.Equal(t, []*graph.Node{node}, batches[0].Nodes) assert.Equal(t, []*Node{node}, batches[0].Nodes)
} }
func TestRelBatches(t *testing.T) { func TestRelBatches(t *testing.T) {
matchKeys := graph.NewSimpleMatchKeys() matchKeys := NewSimpleMatchKeys()
subgraph := NewBatchSubgraph(matchKeys) subgraph := NewBatchSubgraph(matchKeys)
userNode := graph.NewUserNode("pubkey1") userNode := NewUserNode("pubkey1")
eventNode := graph.NewEventNode("abc123") eventNode := NewEventNode("abc123")
rel := graph.NewSignedRel(userNode, eventNode, nil) rel := NewSignedRel(userNode, eventNode, nil)
subgraph.AddRel(rel) subgraph.AddRel(rel)
batches, err := subgraph.RelBatches() batches, err := subgraph.RelBatches()
@@ -111,5 +110,5 @@ func TestRelBatches(t *testing.T) {
assert.ElementsMatch(t, []string{"pubkey"}, batches[0].StartMatchKeys) assert.ElementsMatch(t, []string{"pubkey"}, batches[0].StartMatchKeys)
assert.Equal(t, "Event", batches[0].EndLabel) assert.Equal(t, "Event", batches[0].EndLabel)
assert.ElementsMatch(t, []string{"id"}, batches[0].EndMatchKeys) assert.ElementsMatch(t, []string{"id"}, batches[0].EndMatchKeys)
assert.Equal(t, []*graph.Relationship{rel}, batches[0].Rels) assert.Equal(t, []*Relationship{rel}, batches[0].Rels)
} }

80
boltdb.go Normal file
View File

@@ -0,0 +1,80 @@
package heartwood
import (
"github.com/boltdb/bolt"
)
// Interface
type BoltDB interface {
Setup() error
BatchCheckEventsExist(eventIDs []string) map[string]bool
BatchWriteEvents(events []EventBlob) error
}
func NewKVDB(boltdb *bolt.DB) BoltDB {
return &boltDB{db: boltdb}
}
type boltDB struct {
db *bolt.DB
}
func (b *boltDB) Setup() error {
return SetupBoltDB(b.db)
}
func (b *boltDB) BatchCheckEventsExist(eventIDs []string) map[string]bool {
return BatchCheckEventsExist(b.db, eventIDs)
}
func (b *boltDB) BatchWriteEvents(events []EventBlob) error {
return BatchWriteEvents(b.db, events)
}
func SetupBoltDB(boltdb *bolt.DB) error {
return boltdb.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(BucketName))
return err
})
}
// Functions
const BucketName string = "events"
type EventBlob struct {
ID string
JSON string
}
func BatchCheckEventsExist(boltdb *bolt.DB, eventIDs []string) map[string]bool {
existsMap := make(map[string]bool)
boltdb.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
if bucket == nil {
return nil
}
for _, id := range eventIDs {
existsMap[id] = bucket.Get([]byte(id)) != nil
}
return nil
})
return existsMap
}
func BatchWriteEvents(boltdb *bolt.DB, events []EventBlob) error {
return boltdb.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
for _, event := range events {
if err := bucket.Put(
[]byte(event.ID), []byte(event.JSON),
); err != nil {
return err
}
}
return nil
})
}

View File

@@ -1,4 +1,4 @@
package cypher package heartwood
import ( import (
"fmt" "fmt"

View File

@@ -1,4 +1,4 @@
package cypher package heartwood
import ( import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"

View File

@@ -1,7 +1,6 @@
package heartwood package heartwood
import ( import (
"git.wisehodl.dev/jay/go-heartwood/graph"
roots "git.wisehodl.dev/jay/go-roots/events" roots "git.wisehodl.dev/jay/go-roots/events"
) )
@@ -39,10 +38,10 @@ func ExpandTaggedEvents(e roots.Event, s *EventSubgraph) {
continue continue
} }
referencedEvent := graph.NewEventNode(value) referencedEvent := NewEventNode(value)
s.AddNode(referencedEvent) s.AddNode(referencedEvent)
s.AddRel(graph.NewReferencesEventRel(tagNode, referencedEvent, nil)) s.AddRel(NewReferencesEventRel(tagNode, referencedEvent, nil))
} }
} }
@@ -64,16 +63,16 @@ func ExpandTaggedUsers(e roots.Event, s *EventSubgraph) {
continue continue
} }
referencedEvent := graph.NewUserNode(value) referencedEvent := NewUserNode(value)
s.AddNode(referencedEvent) s.AddNode(referencedEvent)
s.AddRel(graph.NewReferencesUserRel(tagNode, referencedEvent, nil)) s.AddRel(NewReferencesUserRel(tagNode, referencedEvent, nil))
} }
} }
// Helpers // Helpers
func findTagNode(nodes []*graph.Node, name, value string) *graph.Node { func findTagNode(nodes []*Node, name, value string) *Node {
for _, node := range nodes { for _, node := range nodes {
if node.Props["name"] == name && node.Props["value"] == value { if node.Props["name"] == name && node.Props["value"] == value {
return node return node

View File

@@ -1,7 +1,4 @@
// This module defines types and functions for working with Neo4j graph package heartwood
// entities.
package graph
import ( import (
"fmt" "fmt"

View File

@@ -1,4 +1,4 @@
package graph package heartwood
import ( import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"

View File

@@ -1,23 +0,0 @@
package graphstore
import (
"context"
"github.com/neo4j/neo4j-go-driver/v6/neo4j"
)
// ConnectNeo4j creates a new Neo4j driver and verifies its connectivity.
func ConnectNeo4j(ctx context.Context, uri, user, password string) (*neo4j.Driver, error) {
driver, err := neo4j.NewDriver(
uri,
neo4j.BasicAuth(user, password, ""))
if err != nil {
return nil, err
}
err = driver.VerifyConnectivity(ctx)
if err != nil {
return nil, err
}
return &driver, nil
}

View File

@@ -1,42 +0,0 @@
package graphstore
import (
"context"
"github.com/neo4j/neo4j-go-driver/v6/neo4j"
)
// SetNeo4jSchema ensures that the necessary indexes and constraints exist in
// the database
func SetNeo4jSchema(ctx context.Context, driver neo4j.Driver) error {
schemaQueries := []string{
`CREATE CONSTRAINT user_pubkey IF NOT EXISTS
FOR (n:User) REQUIRE n.pubkey IS UNIQUE`,
`CREATE INDEX user_pubkey IF NOT EXISTS
FOR (n:User) ON (n.pubkey)`,
`CREATE INDEX event_id IF NOT EXISTS
FOR (n:Event) ON (n.id)`,
`CREATE INDEX event_kind IF NOT EXISTS
FOR (n:Event) ON (n.kind)`,
`CREATE INDEX tag_name_value IF NOT EXISTS
FOR (n:Tag) ON (n.name, n.value)`,
}
// Create indexes and constraints
for _, query := range schemaQueries {
_, err := neo4j.ExecuteQuery(ctx, driver,
query,
nil,
neo4j.EagerResultTransformer,
neo4j.ExecuteQueryWithDatabase("neo4j"))
if err != nil {
return err
}
}
return nil
}

42
neo4j.go Normal file
View File

@@ -0,0 +1,42 @@
package heartwood
import (
"context"
"github.com/neo4j/neo4j-go-driver/v6/neo4j"
)
// Interface
type GraphDB interface {
MergeSubgraph(ctx context.Context, subgraph *BatchSubgraph) ([]neo4j.ResultSummary, error)
}
func NewGraphDriver(driver neo4j.Driver) GraphDB {
return &graphdb{driver: driver}
}
type graphdb struct {
driver neo4j.Driver
}
func (n *graphdb) MergeSubgraph(ctx context.Context, subgraph *BatchSubgraph) ([]neo4j.ResultSummary, error) {
return MergeSubgraph(ctx, n.driver, subgraph)
}
// Functions
func ConnectNeo4j(ctx context.Context, uri, user, password string) (neo4j.Driver, error) {
driver, err := neo4j.NewDriver(
uri,
neo4j.BasicAuth(user, password, ""))
if err != nil {
return nil, err
}
err = driver.VerifyConnectivity(ctx)
if err != nil {
return nil, err
}
return driver, nil
}

View File

@@ -1,10 +1,9 @@
// This module provides methods for creating nodes and relationships according package heartwood
// to a defined schema.
package graph
import ( import (
"context"
"fmt" "fmt"
"github.com/neo4j/neo4j-go-driver/v6/neo4j"
) )
// ======================================== // ========================================
@@ -99,3 +98,43 @@ func NewRelationshipWithValidation(
return NewRelationship(rtype, start, end, props) return NewRelationship(rtype, start, end, props)
} }
// ========================================
// Schema Indexes and Constraints
// ========================================
// SetNeo4jSchema ensures that the necessary indexes and constraints exist in
// the database
func SetNeo4jSchema(ctx context.Context, driver neo4j.Driver) error {
schemaQueries := []string{
`CREATE CONSTRAINT user_pubkey IF NOT EXISTS
FOR (n:User) REQUIRE n.pubkey IS UNIQUE`,
`CREATE INDEX user_pubkey IF NOT EXISTS
FOR (n:User) ON (n.pubkey)`,
`CREATE INDEX event_id IF NOT EXISTS
FOR (n:Event) ON (n.id)`,
`CREATE INDEX event_kind IF NOT EXISTS
FOR (n:Event) ON (n.kind)`,
`CREATE INDEX tag_name_value IF NOT EXISTS
FOR (n:Tag) ON (n.name, n.value)`,
}
// Create indexes and constraints
for _, query := range schemaQueries {
_, err := neo4j.ExecuteQuery(ctx, driver,
query,
nil,
neo4j.EagerResultTransformer,
neo4j.ExecuteQueryWithDatabase("neo4j"))
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,4 +1,4 @@
package graph package heartwood
import ( import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"

View File

@@ -1,4 +1,4 @@
package graph package heartwood
// Sets // Sets

View File

@@ -1,42 +1,41 @@
package heartwood package heartwood
import ( import (
"git.wisehodl.dev/jay/go-heartwood/graph"
roots "git.wisehodl.dev/jay/go-roots/events" roots "git.wisehodl.dev/jay/go-roots/events"
) )
// Event subgraph struct // Event subgraph struct
type EventSubgraph struct { type EventSubgraph struct {
nodes []*graph.Node nodes []*Node
rels []*graph.Relationship rels []*Relationship
} }
func NewEventSubgraph() *EventSubgraph { func NewEventSubgraph() *EventSubgraph {
return &EventSubgraph{ return &EventSubgraph{
nodes: []*graph.Node{}, nodes: []*Node{},
rels: []*graph.Relationship{}, rels: []*Relationship{},
} }
} }
func (s *EventSubgraph) AddNode(node *graph.Node) { func (s *EventSubgraph) AddNode(node *Node) {
s.nodes = append(s.nodes, node) s.nodes = append(s.nodes, node)
} }
func (s *EventSubgraph) AddRel(rel *graph.Relationship) { func (s *EventSubgraph) AddRel(rel *Relationship) {
s.rels = append(s.rels, rel) s.rels = append(s.rels, rel)
} }
func (s *EventSubgraph) Nodes() []*graph.Node { func (s *EventSubgraph) Nodes() []*Node {
return s.nodes return s.nodes
} }
func (s *EventSubgraph) Rels() []*graph.Relationship { func (s *EventSubgraph) Rels() []*Relationship {
return s.rels return s.rels
} }
func (s *EventSubgraph) NodesByLabel(label string) []*graph.Node { func (s *EventSubgraph) NodesByLabel(label string) []*Node {
nodes := []*graph.Node{} nodes := []*Node{}
for _, node := range s.nodes { for _, node := range s.nodes {
if node.Labels.Contains(label) { if node.Labels.Contains(label) {
nodes = append(nodes, node) nodes = append(nodes, node)
@@ -90,37 +89,37 @@ func EventToSubgraph(e roots.Event, p ExpanderPipeline) *EventSubgraph {
return s return s
} }
func newEventNode(eventID string, createdAt int, kind int, content string) *graph.Node { func newEventNode(eventID string, createdAt int, kind int, content string) *Node {
eventNode := graph.NewEventNode(eventID) eventNode := NewEventNode(eventID)
eventNode.Props["created_at"] = createdAt eventNode.Props["created_at"] = createdAt
eventNode.Props["kind"] = kind eventNode.Props["kind"] = kind
eventNode.Props["content"] = content eventNode.Props["content"] = content
return eventNode return eventNode
} }
func newUserNode(pubkey string) *graph.Node { func newUserNode(pubkey string) *Node {
return graph.NewUserNode(pubkey) return NewUserNode(pubkey)
} }
func newSignedRel(user, event *graph.Node) *graph.Relationship { func newSignedRel(user, event *Node) *Relationship {
return graph.NewSignedRel(user, event, nil) return NewSignedRel(user, event, nil)
} }
func newTagNodes(tags []roots.Tag) []*graph.Node { func newTagNodes(tags []roots.Tag) []*Node {
nodes := []*graph.Node{} nodes := []*Node{}
for _, tag := range tags { for _, tag := range tags {
if !isValidTag(tag) { if !isValidTag(tag) {
continue continue
} }
nodes = append(nodes, graph.NewTagNode(tag[0], tag[1])) nodes = append(nodes, NewTagNode(tag[0], tag[1]))
} }
return nodes return nodes
} }
func newTagRels(event *graph.Node, tags []*graph.Node) []*graph.Relationship { func newTagRels(event *Node, tags []*Node) []*Relationship {
rels := []*graph.Relationship{} rels := []*Relationship{}
for _, tag := range tags { for _, tag := range tags {
rels = append(rels, graph.NewTaggedRel(event, tag, nil)) rels = append(rels, NewTaggedRel(event, tag, nil))
} }
return rels return rels
} }

View File

@@ -2,7 +2,6 @@ package heartwood
import ( import (
"fmt" "fmt"
"git.wisehodl.dev/jay/go-heartwood/graph"
roots "git.wisehodl.dev/jay/go-roots/events" roots "git.wisehodl.dev/jay/go-roots/events"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"reflect" "reflect"
@@ -22,21 +21,21 @@ var static = roots.Event{
Content: "hello", Content: "hello",
} }
func newFullEventNode(id string, createdAt, kind int, content string) *graph.Node { func newFullEventNode(id string, createdAt, kind int, content string) *Node {
n := graph.NewEventNode(id) n := NewEventNode(id)
n.Props["created_at"] = createdAt n.Props["created_at"] = createdAt
n.Props["kind"] = kind n.Props["kind"] = kind
n.Props["content"] = content n.Props["content"] = content
return n return n
} }
func baseSubgraph(eventID, pubkey string) (*EventSubgraph, *graph.Node, *graph.Node) { func baseSubgraph(eventID, pubkey string) (*EventSubgraph, *Node, *Node) {
s := NewEventSubgraph() s := NewEventSubgraph()
eventNode := newFullEventNode(eventID, static.CreatedAt, static.Kind, static.Content) eventNode := newFullEventNode(eventID, static.CreatedAt, static.Kind, static.Content)
userNode := graph.NewUserNode(pubkey) userNode := NewUserNode(pubkey)
s.AddNode(eventNode) s.AddNode(eventNode)
s.AddNode(userNode) s.AddNode(userNode)
s.AddRel(graph.NewSignedRel(userNode, eventNode, nil)) s.AddRel(NewSignedRel(userNode, eventNode, nil))
return s, eventNode, userNode return s, eventNode, userNode
} }
@@ -66,9 +65,9 @@ func TestEventToSubgraph(t *testing.T) {
}, },
expected: func() *EventSubgraph { expected: func() *EventSubgraph {
s, eventNode, _ := baseSubgraph(ids["a"], ids["b"]) s, eventNode, _ := baseSubgraph(ids["a"], ids["b"])
tagNode := graph.NewTagNode("t", "bitcoin") tagNode := NewTagNode("t", "bitcoin")
s.AddNode(tagNode) s.AddNode(tagNode)
s.AddRel(graph.NewTaggedRel(eventNode, tagNode, nil)) s.AddRel(NewTaggedRel(eventNode, tagNode, nil))
return s return s
}(), }(),
}, },
@@ -93,12 +92,12 @@ func TestEventToSubgraph(t *testing.T) {
}, },
expected: func() *EventSubgraph { expected: func() *EventSubgraph {
s, eventNode, _ := baseSubgraph(ids["a"], ids["b"]) s, eventNode, _ := baseSubgraph(ids["a"], ids["b"])
tagNode := graph.NewTagNode("e", ids["c"]) tagNode := NewTagNode("e", ids["c"])
referencedEvent := graph.NewEventNode(ids["c"]) referencedEvent := NewEventNode(ids["c"])
s.AddNode(tagNode) s.AddNode(tagNode)
s.AddNode(referencedEvent) s.AddNode(referencedEvent)
s.AddRel(graph.NewTaggedRel(eventNode, tagNode, nil)) s.AddRel(NewTaggedRel(eventNode, tagNode, nil))
s.AddRel(graph.NewReferencesEventRel(tagNode, referencedEvent, nil)) s.AddRel(NewReferencesEventRel(tagNode, referencedEvent, nil))
return s return s
}(), }(),
}, },
@@ -111,9 +110,9 @@ func TestEventToSubgraph(t *testing.T) {
}, },
expected: func() *EventSubgraph { expected: func() *EventSubgraph {
s, eventNode, _ := baseSubgraph(ids["a"], ids["b"]) s, eventNode, _ := baseSubgraph(ids["a"], ids["b"])
tagNode := graph.NewTagNode("e", "notvalid") tagNode := NewTagNode("e", "notvalid")
s.AddNode(tagNode) s.AddNode(tagNode)
s.AddRel(graph.NewTaggedRel(eventNode, tagNode, nil)) s.AddRel(NewTaggedRel(eventNode, tagNode, nil))
return s return s
}(), }(),
}, },
@@ -126,12 +125,12 @@ func TestEventToSubgraph(t *testing.T) {
}, },
expected: func() *EventSubgraph { expected: func() *EventSubgraph {
s, eventNode, _ := baseSubgraph(ids["a"], ids["b"]) s, eventNode, _ := baseSubgraph(ids["a"], ids["b"])
tagNode := graph.NewTagNode("p", ids["d"]) tagNode := NewTagNode("p", ids["d"])
referencedUser := graph.NewUserNode(ids["d"]) referencedUser := NewUserNode(ids["d"])
s.AddNode(tagNode) s.AddNode(tagNode)
s.AddNode(referencedUser) s.AddNode(referencedUser)
s.AddRel(graph.NewTaggedRel(eventNode, tagNode, nil)) s.AddRel(NewTaggedRel(eventNode, tagNode, nil))
s.AddRel(graph.NewReferencesUserRel(tagNode, referencedUser, nil)) s.AddRel(NewReferencesUserRel(tagNode, referencedUser, nil))
return s return s
}(), }(),
}, },
@@ -144,9 +143,9 @@ func TestEventToSubgraph(t *testing.T) {
}, },
expected: func() *EventSubgraph { expected: func() *EventSubgraph {
s, eventNode, _ := baseSubgraph(ids["a"], ids["b"]) s, eventNode, _ := baseSubgraph(ids["a"], ids["b"])
tagNode := graph.NewTagNode("p", "notvalid") tagNode := NewTagNode("p", "notvalid")
s.AddNode(tagNode) s.AddNode(tagNode)
s.AddRel(graph.NewTaggedRel(eventNode, tagNode, nil)) s.AddRel(NewTaggedRel(eventNode, tagNode, nil))
return s return s
}(), }(),
}, },
@@ -164,7 +163,7 @@ func TestEventToSubgraph(t *testing.T) {
// helpers // helpers
func nodesEqual(expected, got *graph.Node) error { func nodesEqual(expected, got *Node) error {
// Compare label counts // Compare label counts
if expected.Labels.Length() != got.Labels.Length() { if expected.Labels.Length() != got.Labels.Length() {
return fmt.Errorf( return fmt.Errorf(
@@ -187,7 +186,7 @@ func nodesEqual(expected, got *graph.Node) error {
return nil return nil
} }
func relsEqual(expected, got *graph.Relationship) error { func relsEqual(expected, got *Relationship) error {
// Compare type // Compare type
if expected.Type != got.Type { if expected.Type != got.Type {
return fmt.Errorf("type: expected %q, got %q", expected.Type, got.Type) return fmt.Errorf("type: expected %q, got %q", expected.Type, got.Type)
@@ -209,7 +208,7 @@ func relsEqual(expected, got *graph.Relationship) error {
return nil return nil
} }
func propsEqual(expected, got graph.Properties) error { func propsEqual(expected, got Properties) error {
if len(expected) != len(got) { if len(expected) != len(got) {
return fmt.Errorf( return fmt.Errorf(
"number of props does not match. expected %d, got %d", "number of props does not match. expected %d, got %d",
@@ -231,10 +230,10 @@ func propsEqual(expected, got graph.Properties) error {
func assertSubgraphsEqual(t *testing.T, expected, got *EventSubgraph) { func assertSubgraphsEqual(t *testing.T, expected, got *EventSubgraph) {
t.Helper() t.Helper()
gotNodes := make([]*graph.Node, len(got.Nodes())) gotNodes := make([]*Node, len(got.Nodes()))
copy(gotNodes, got.Nodes()) copy(gotNodes, got.Nodes())
gotRels := make([]*graph.Relationship, len(got.Rels())) gotRels := make([]*Relationship, len(got.Rels()))
copy(gotRels, got.Rels()) copy(gotRels, got.Rels())
for _, expectedNode := range expected.Nodes() { for _, expectedNode := range expected.Nodes() {

224
write.go
View File

@@ -1,20 +1,25 @@
package heartwood package heartwood
import ( import (
"context"
"encoding/json"
"fmt" "fmt"
roots "git.wisehodl.dev/jay/go-roots/events" roots "git.wisehodl.dev/jay/go-roots/events"
"github.com/boltdb/bolt"
"github.com/neo4j/neo4j-go-driver/v6/neo4j" "github.com/neo4j/neo4j-go-driver/v6/neo4j"
"sync" "sync"
// "git.wisehodl.dev/jay/go-heartwood/graph"
"time" "time"
) )
type WriteOptions struct {
Expanders ExpanderPipeline
KVReadBatchSize int
}
type EventFollower struct { type EventFollower struct {
ID string ID string
JSON string JSON string
Event roots.Event Event roots.Event
Subgraph EventSubgraph Subgraph *EventSubgraph
Error error Error error
} }
@@ -34,11 +39,18 @@ type WriteReport struct {
func WriteEvents( func WriteEvents(
events []string, events []string,
driver *neo4j.Driver, boltdb *bolt.DB, graphdb GraphDB, boltdb BoltDB,
opts *WriteOptions,
) (WriteReport, error) { ) (WriteReport, error) {
start := time.Now() start := time.Now()
err := setupBoltDB(boltdb) if opts == nil {
opts = &WriteOptions{}
}
setDefaultWriteOptions(opts)
err := boltdb.Setup()
if err != nil { if err != nil {
return WriteReport{}, fmt.Errorf("error setting up bolt db: %w", err) return WriteReport{}, fmt.Errorf("error setting up bolt db: %w", err)
} }
@@ -81,7 +93,10 @@ func WriteEvents(
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
enforcePolicyRules(driver, boltdb, parsedChan, queuedChan, skippedChan) enforcePolicyRules(
graphdb, boltdb,
opts.KVReadBatchSize,
parsedChan, queuedChan, skippedChan)
}() }()
// Collect Skipped Events // Collect Skipped Events
@@ -99,7 +114,7 @@ func WriteEvents(
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
convertEventsToSubgraphs(queuedChan, convertedChan) convertEventsToSubgraphs(opts.Expanders, queuedChan, convertedChan)
}() }()
// Write Events To Databases // Write Events To Databases
@@ -109,7 +124,7 @@ func WriteEvents(
go func() { go func() {
defer wg.Done() defer wg.Done()
writeEventsToDatabases( writeEventsToDatabases(
driver, boltdb, graphdb, boltdb,
convertedChan, writeResultChan) convertedChan, writeResultChan)
}() }()
@@ -139,20 +154,102 @@ func WriteEvents(
}, writeResult.Error }, writeResult.Error
} }
func setupBoltDB(boltdb *bolt.DB) error func setDefaultWriteOptions(opts *WriteOptions) {
if opts.Expanders == nil {
opts.Expanders = NewExpanderPipeline(DefaultExpanders()...)
}
if opts.KVReadBatchSize == 0 {
opts.KVReadBatchSize = 100
}
}
func createEventFollowers(jsonChan chan string, eventChan chan EventFollower) func createEventFollowers(jsonChan chan string, eventChan chan EventFollower) {
for json := range jsonChan {
eventChan <- EventFollower{JSON: json}
}
close(eventChan)
}
func parseEventJSON(inChan, parsedChan, invalidChan chan EventFollower) func parseEventJSON(inChan, parsedChan, invalidChan chan EventFollower) {
for follower := range inChan {
var event roots.Event
jsonBytes := []byte(follower.JSON)
err := json.Unmarshal(jsonBytes, &event)
if err != nil {
follower.Error = err
invalidChan <- follower
continue
}
follower.ID = event.ID
follower.Event = event
parsedChan <- follower
}
close(parsedChan)
close(invalidChan)
}
func enforcePolicyRules( func enforcePolicyRules(
driver *neo4j.Driver, boltdb *bolt.DB, graphdb GraphDB, boltdb BoltDB,
inChan, queuedChan, skippedChan chan EventFollower) batchSize int,
inChan, queuedChan, skippedChan chan EventFollower,
) {
batch := []EventFollower{}
func convertEventsToSubgraphs(inChan, convertedChan chan EventFollower) for follower := range inChan {
batch = append(batch, follower)
if len(batch) >= batchSize {
processPolicyRulesBatch(boltdb, batch, queuedChan, skippedChan)
batch = []EventFollower{}
}
}
if len(batch) > 0 {
processPolicyRulesBatch(boltdb, batch, queuedChan, skippedChan)
}
close(queuedChan)
close(skippedChan)
}
func processPolicyRulesBatch(
boltdb BoltDB,
batch []EventFollower,
queuedChan, skippedChan chan EventFollower,
) {
eventIDs := []string{}
for _, follower := range batch {
eventIDs = append(eventIDs, follower.ID)
}
existsMap := boltdb.BatchCheckEventsExist(eventIDs)
for _, follower := range batch {
if existsMap[follower.ID] {
skippedChan <- follower
} else {
queuedChan <- follower
}
}
}
func convertEventsToSubgraphs(
expanders ExpanderPipeline,
inChan, convertedChan chan EventFollower,
) {
for follower := range inChan {
subgraph := EventToSubgraph(follower.Event, expanders)
follower.Subgraph = subgraph
convertedChan <- follower
}
close(convertedChan)
}
func writeEventsToDatabases( func writeEventsToDatabases(
driver *neo4j.Driver, boltdb *bolt.DB, graphdb GraphDB, boltdb BoltDB,
inChan chan EventFollower, inChan chan EventFollower,
resultChan chan WriteResult, resultChan chan WriteResult,
) { ) {
@@ -171,12 +268,12 @@ func writeEventsToDatabases(
defer wg.Done() defer wg.Done()
writeEventsToKVStore( writeEventsToKVStore(
boltdb, boltdb,
kvEventChan, kvErrorChan) kvEventChan, kvWriteDone, kvErrorChan)
}() }()
go func() { go func() {
defer wg.Done() defer wg.Done()
writeEventsToGraphStore( writeEventsToGraphDriver(
driver, graphdb,
graphEventChan, kvWriteDone, graphResultChan) graphEventChan, kvWriteDone, graphResultChan)
}() }()
@@ -191,37 +288,86 @@ func writeEventsToDatabases(
wg.Wait() wg.Wait()
kvError := <-kvErrorChan kvError := <-kvErrorChan
if kvError != nil {
close(kvWriteDone) // signal abort
resultChan <- WriteResult{Error: kvError}
return
}
// Signal graph writer to proceed
kvWriteDone <- struct{}{}
close(kvWriteDone)
graphResult := <-graphResultChan graphResult := <-graphResultChan
if graphResult.Error != nil {
resultChan <- WriteResult{Error: graphResult.Error} var finalErr error
return if kvError != nil && graphResult.Error != nil {
finalErr = fmt.Errorf("kvstore: %w; graphstore: %v", kvError, graphResult.Error)
} else if kvError != nil {
finalErr = fmt.Errorf("kvstore: %w", kvError)
} else if graphResult.Error != nil {
finalErr = fmt.Errorf("graphstore: %w", graphResult.Error)
} }
resultChan <- graphResult resultChan <- WriteResult{
ResultSummaries: graphResult.ResultSummaries,
Error: finalErr,
}
} }
func writeEventsToKVStore( func writeEventsToKVStore(
boltdb *bolt.DB, boltdb BoltDB,
inChan chan EventFollower, inChan chan EventFollower,
done chan struct{},
resultChan chan error, resultChan chan error,
) ) {
events := []EventBlob{}
func writeEventsToGraphStore( for follower := range inChan {
driver *neo4j.Driver, events = append(events,
EventBlob{ID: follower.ID, JSON: follower.JSON})
}
err := boltdb.BatchWriteEvents(events)
if err != nil {
close(done)
} else {
done <- struct{}{}
close(done)
}
resultChan <- err
close(resultChan)
}
func writeEventsToGraphDriver(
graphdb GraphDB,
inChan chan EventFollower, inChan chan EventFollower,
start chan struct{}, start chan struct{},
resultChan chan WriteResult, resultChan chan WriteResult,
) ) {
matchKeys := NewSimpleMatchKeys()
batch := NewBatchSubgraph(matchKeys)
func collectEvents(inChan chan EventFollower, resultChan chan []EventFollower) for follower := range inChan {
for _, node := range follower.Subgraph.Nodes() {
batch.AddNode(node)
}
for _, rel := range follower.Subgraph.Rels() {
batch.AddRel(rel)
}
}
_, ok := <-start
if !ok {
resultChan <- WriteResult{Error: fmt.Errorf("kv write failed, aborting graph write")}
close(resultChan)
return
}
summaries, err := graphdb.MergeSubgraph(context.Background(), batch)
resultChan <- WriteResult{
ResultSummaries: summaries,
Error: err,
}
close(resultChan)
}
func collectEvents(inChan chan EventFollower, resultChan chan []EventFollower) {
collected := []EventFollower{}
for follower := range inChan {
collected = append(collected, follower)
}
resultChan <- collected
close(resultChan)
}