Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cgo_bundled.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

package lbug

//go:generate sh download_lbug.sh
//go:generate bash download_lbug.sh

/*
#cgo darwin LDFLAGS: -lc++ -L${SRCDIR}/lib/dynamic/osx -llbug -Wl,-rpath,${SRCDIR}/lib/dynamic/osx
Expand Down
104 changes: 86 additions & 18 deletions download_lbug.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ extract_archive() {
esac
}

detect_soname() {
local lib_file=$1

if command -v objdump >/dev/null 2>&1; then
objdump -p "$lib_file" 2>/dev/null | awk '$1 == "SONAME" { print $2; exit }'
elif command -v readelf >/dev/null 2>&1; then
readelf -d "$lib_file" 2>/dev/null | awk -F'[][]' '/SONAME/ { print $2; exit }'
fi
}

detect_install_name() {
local lib_file=$1

if command -v otool >/dev/null 2>&1; then
otool -D "$lib_file" 2>/dev/null | awk 'NR == 2 { print $1; exit }'
fi
}

# Function to download and extract a specific library
download_library() {
local asset=$1
Expand Down Expand Up @@ -66,26 +84,76 @@ download_library() {
fi
fi

# Find and copy library file
local lib_file=$(find . -name "$lib_pattern" | head -1)
if [ -n "$lib_file" ]; then
mkdir -p "$OLDPWD/$target_dir"
cp "$lib_file" "$OLDPWD/$target_dir/"
echo "Copied $lib_pattern to $target_dir"

# For Windows, also look for .lib if it exists
if [ "$os_type" = "windows" ]; then
local lib_import=$(find . -name "lbug_shared.lib" -o -name "lbug.lib" | head -1)
if [ -n "$lib_import" ]; then
cp "$lib_import" "$OLDPWD/$target_dir/"
echo "Copied $(basename "$lib_import") to $target_dir"
mkdir -p "$OLDPWD/$target_dir"

# Linux shared libraries typically ship as a symlink chain:
# liblbug.so -> liblbug.so.0 -> liblbug.so.0.x.y
# Preserve those names so the runtime loader can satisfy the SONAME.
if [ "$os_type" = "linux" ]; then
local found_lib=0
local lib_list="$temp_dir/lib_files.txt"
local copied_main_lib=""
find . \( -type f -o -type l \) -name "${lib_pattern}*" | sort > "$lib_list"

while IFS= read -r lib_file; do
found_lib=1
cp -a "$lib_file" "$OLDPWD/$target_dir/"
echo "Copied $(basename "$lib_file") to $target_dir"
if [ "$(basename "$lib_file")" = "$lib_pattern" ]; then
copied_main_lib="$OLDPWD/$target_dir/$lib_pattern"
fi
done < "$lib_list"

if [ "$found_lib" -eq 0 ]; then
echo "ERROR: Library file (${lib_pattern}*) not found in the extracted files"
cd "$OLDPWD"
rm -rf "$temp_dir"
exit 1
fi

if [ -n "$copied_main_lib" ]; then
local soname=$(detect_soname "$copied_main_lib")
if [ -n "$soname" ] && [ "$soname" != "$lib_pattern" ] && [ ! -e "$OLDPWD/$target_dir/$soname" ]; then
ln -s "$lib_pattern" "$OLDPWD/$target_dir/$soname"
echo "Created $soname -> $lib_pattern in $target_dir"
fi
fi
else
echo "ERROR: Library file ($lib_pattern) not found in the extracted files"
cd "$OLDPWD"
rm -rf "$temp_dir"
exit 1
# Find and copy library file
local lib_file=$(find . -name "$lib_pattern" | head -1)
if [ -n "$lib_file" ]; then
cp "$lib_file" "$OLDPWD/$target_dir/"
echo "Copied $lib_pattern to $target_dir"

if [ "$os_type" = "osx" ]; then
local copied_lib="$OLDPWD/$target_dir/$lib_pattern"
local install_name=$(detect_install_name "$copied_lib")
local install_basename=""

if [ -n "$install_name" ]; then
install_basename=$(basename "$install_name")
fi

if [ -n "$install_basename" ] && [ "$install_basename" != "$lib_pattern" ] && [ ! -e "$OLDPWD/$target_dir/$install_basename" ]; then
ln -s "$lib_pattern" "$OLDPWD/$target_dir/$install_basename"
echo "Created $install_basename -> $lib_pattern in $target_dir"
fi
fi

# For Windows, also look for .lib if it exists
if [ "$os_type" = "windows" ]; then
local lib_import=$(find . -name "lbug_shared.lib" -o -name "lbug.lib" | head -1)
if [ -n "$lib_import" ]; then
cp "$lib_import" "$OLDPWD/$target_dir/"
echo "Copied $(basename "$lib_import") to $target_dir"
fi
fi
else
echo "ERROR: Library file ($lib_pattern) not found in the extracted files"
cd "$OLDPWD"
rm -rf "$temp_dir"
exit 1
fi
fi

# Cleanup
Expand Down Expand Up @@ -175,4 +243,4 @@ else
download_library "$asset" "$target_dir" "$lib_pattern" "$os" "yes" "."
fi

echo "Done!"
echo "Done!"
225 changes: 225 additions & 0 deletions ownership_regression_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package lbug

import (
"os"
"os/exec"
"runtime"
"runtime/debug"
"sync"
"testing"

"github.com/stretchr/testify/require"
)

func TestBorrowedValueOwnershipRecursiveRelationship(t *testing.T) {
runBorrowedValueOwnershipScenario(t, "recursive_relationship")
}

func TestBorrowedValueOwnershipNestedContainers(t *testing.T) {
runBorrowedValueOwnershipScenario(t, "nested_containers")
}

func TestBorrowedValueOwnershipSubprocess(t *testing.T) {
scenario := os.Getenv("LBUG_BORROWED_VALUE_SCENARIO")
if scenario == "" {
t.Skip("subprocess helper")
}

prevGCPercent := debug.SetGCPercent(1)
defer debug.SetGCPercent(prevGCPercent)

switch scenario {
case "recursive_relationship":
runRecursiveRelationshipOwnershipStress(t)
case "nested_containers":
runNestedContainerOwnershipStress(t)
default:
t.Fatalf("unknown ownership scenario %q", scenario)
}
}

func runBorrowedValueOwnershipScenario(t *testing.T, scenario string) {
t.Helper()

cmd := exec.Command(os.Args[0], "-test.run=^TestBorrowedValueOwnershipSubprocess$")
cmd.Env = append(os.Environ(), "LBUG_BORROWED_VALUE_SCENARIO="+scenario)
output, err := cmd.CombinedOutput()
require.NoError(t, err, "scenario %s crashed:\n%s", scenario, string(output))
}

func runRecursiveRelationshipOwnershipStress(t *testing.T) {
db, conn := setupTestDatabase(t)
defer db.Close()
defer conn.Close()

createTestData(t, conn, 250)

runOwnershipStress(t, 8, 12, func() error {
return queryRecursiveRelationships(conn)
})
}

func runNestedContainerOwnershipStress(t *testing.T) {
_, conn := SetupTestDatabase(t)

runOwnershipStress(t, 8, 20, func() error {
return queryNestedContainers(conn)
})
}

func runOwnershipStress(t *testing.T, numGoroutines int, queriesPerGoroutine int, queryFn func() error) {
t.Helper()

var wg sync.WaitGroup
errChan := make(chan error, numGoroutines*queriesPerGoroutine)

for range numGoroutines {
wg.Add(1)
go func() {
defer wg.Done()

for range queriesPerGoroutine {
if err := queryFn(); err != nil {
errChan <- err
return
}
runtime.GC()
}
}()
}

wg.Wait()
close(errChan)

for err := range errChan {
require.NoError(t, err)
}
}

func queryRecursiveRelationships(conn *Connection) error {
result, err := conn.Query(`
MATCH (source:Node {id: 0})-[r:CONNECTS* ALL SHORTEST 1..3]->(target:Node {id: 4})
RETURN r, source.fqn, target.fqn
`)
if err != nil {
return err
}
defer result.Close()

rowCount := 0
for result.HasNext() {
row, err := result.Next()
if err != nil {
return err
}

value, err := row.GetValue(0)
if err != nil {
row.Close()
return err
}

recursiveRel := value.(RecursiveRelationship)
if len(recursiveRel.Relationships) != 2 {
row.Close()
return requireLengthError("recursive relationships", 2, len(recursiveRel.Relationships))
}

row.Close()
rowCount++
}

if rowCount == 0 {
return requireLengthError("recursive relationship rows", 1, 0)
}

return nil
}

func queryNestedContainers(conn *Connection) error {
result, err := conn.Query(`
MATCH (p:person)-[r:workAt]->(o:organisation)
WHERE p.ID = 5
RETURN
p,
r,
p.courseScoresPerTerm,
{
name: p.fName,
scores: p.courseScoresPerTerm,
usedNames: p.usedNames
},
o
`)
if err != nil {
return err
}
defer result.Close()

if !result.HasNext() {
return requireLengthError("nested container rows", 1, 0)
}

row, err := result.Next()
if err != nil {
return err
}
defer row.Close()

values, err := row.GetAsSlice()
if err != nil {
return err
}
if len(values) != 5 {
return requireLengthError("nested container values", 5, len(values))
}

person := values[0].(Node)
if person.Label != "person" || person.Properties["ID"] != int64(5) {
return errUnexpectedValue("person payload")
}

rel := values[1].(Relationship)
if rel.Label != "workAt" || rel.Properties["year"] != int64(2010) {
return errUnexpectedValue("relationship payload")
}

scores := values[2].([]any)
if len(scores) == 0 {
return errUnexpectedValue("scores payload")
}

profile := values[3].(map[string]any)
if profile["name"] != person.Properties["fName"] {
return errUnexpectedValue("profile payload")
}

org := values[4].(Node)
if org.Label != "organisation" {
return errUnexpectedValue("organisation payload")
}

return nil
}

func requireLengthError(name string, want int, got int) error {
return &ownershipError{name: name, message: "unexpected length", want: want, got: got}
}

func errUnexpectedValue(name string) error {
return &ownershipError{name: name, message: "unexpected value"}
}

type ownershipError struct {
name string
message string
want int
got int
}

func (e *ownershipError) Error() string {
if e.want == 0 && e.got == 0 {
return e.name + ": " + e.message
}
return e.name + ": " + e.message
}
Loading
Loading