Initial commit
This commit is contained in:
		
							
								
								
									
										57
									
								
								mdb/pfile/alloclist.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								mdb/pfile/alloclist.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| package pfile | ||||
|  | ||||
| import "slices" | ||||
|  | ||||
| type allocList map[[2]uint64][]uint64 | ||||
|  | ||||
| func newAllocList() *allocList { | ||||
| 	al := allocList(map[[2]uint64][]uint64{}) | ||||
| 	return &al | ||||
| } | ||||
|  | ||||
| func (al allocList) Create(collectionID, itemID, page uint64) { | ||||
| 	key := al.key(collectionID, itemID) | ||||
| 	al[key] = []uint64{page} | ||||
| } | ||||
|  | ||||
| // Push is used to add pages to the storage when loading. It will append | ||||
| // pages to the appropriate list, or return false if the list isn't found. | ||||
| func (al allocList) Push(collectionID, itemID, page uint64) bool { | ||||
| 	key := al.key(collectionID, itemID) | ||||
| 	if _, ok := al[key]; !ok { | ||||
| 		return false | ||||
| 	} | ||||
| 	al[key] = append(al[key], page) | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func (al allocList) Store(collectionID, itemID uint64, pages []uint64) { | ||||
| 	key := al.key(collectionID, itemID) | ||||
| 	al[key] = slices.Clone(pages) | ||||
| } | ||||
|  | ||||
| func (al allocList) Remove(collectionID, itemID uint64) []uint64 { | ||||
| 	key := al.key(collectionID, itemID) | ||||
| 	pages := al[key] | ||||
| 	delete(al, key) | ||||
| 	return pages | ||||
| } | ||||
|  | ||||
| func (al allocList) Iterate( | ||||
| 	each func(collectionID, itemID uint64, pages []uint64) error, | ||||
| ) error { | ||||
| 	for key, pages := range al { | ||||
| 		if err := each(key[0], key[1], pages); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (al allocList) Len() int { | ||||
| 	return len(al) | ||||
| } | ||||
|  | ||||
| func (al allocList) key(collectionID, itemID uint64) [2]uint64 { | ||||
| 	return [2]uint64{collectionID, itemID} | ||||
| } | ||||
							
								
								
									
										172
									
								
								mdb/pfile/alloclist_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								mdb/pfile/alloclist_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | ||||
| package pfile | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func (al allocList) Assert(t *testing.T, state map[[2]uint64][]uint64) { | ||||
| 	t.Helper() | ||||
|  | ||||
| 	if len(al) != len(state) { | ||||
| 		t.Fatalf("Expected %d items, but found %d.", len(state), len(al)) | ||||
| 	} | ||||
|  | ||||
| 	for key, expected := range state { | ||||
| 		val, ok := al[key] | ||||
| 		if !ok { | ||||
| 			t.Fatalf("Expected to find key %v.", key) | ||||
| 		} | ||||
| 		if !reflect.DeepEqual(val, expected) { | ||||
| 			t.Fatalf("For %v, expected %v but got %v.", key, expected, val) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (al *allocList) With(collectionID, itemID uint64, pages ...uint64) *allocList { | ||||
| 	al.Store(collectionID, itemID, pages) | ||||
| 	return al | ||||
| } | ||||
|  | ||||
| func (al *allocList) Equals(rhs *allocList) bool { | ||||
|  | ||||
| 	if len(*rhs) != len(*al) { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	for key, val := range *rhs { | ||||
| 		actual := (*al)[key] | ||||
| 		if !reflect.DeepEqual(val, actual) { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func TestAllocList(t *testing.T) { | ||||
| 	const ( | ||||
| 		CREATE = "CREATE" | ||||
| 		PUSH   = "PUSH" | ||||
| 		STORE  = "STORE" | ||||
| 		REMOVE = "REMOVE" | ||||
| 	) | ||||
|  | ||||
| 	type TestCase struct { | ||||
| 		Name        string | ||||
| 		Action      string | ||||
| 		Key         [2]uint64 | ||||
| 		Page        uint64 | ||||
| 		Pages       []uint64 // For STORE command. | ||||
| 		Expected    *allocList | ||||
| 		ExpectedLen int | ||||
| 	} | ||||
|  | ||||
| 	testCases := []TestCase{{ | ||||
| 		Name:        "Create something", | ||||
| 		Action:      CREATE, | ||||
| 		Key:         [2]uint64{1, 1}, | ||||
| 		Page:        1, | ||||
| 		Expected:    newAllocList().With(1, 1, 1), | ||||
| 		ExpectedLen: 1, | ||||
| 	}, { | ||||
| 		Name:        "Push onto something", | ||||
| 		Action:      PUSH, | ||||
| 		Key:         [2]uint64{1, 1}, | ||||
| 		Page:        2, | ||||
| 		Expected:    newAllocList().With(1, 1, 1, 2), | ||||
| 		ExpectedLen: 1, | ||||
| 	}, { | ||||
| 		Name:        "Push onto something again", | ||||
| 		Action:      PUSH, | ||||
| 		Key:         [2]uint64{1, 1}, | ||||
| 		Page:        3, | ||||
| 		Expected:    newAllocList().With(1, 1, 1, 2, 3), | ||||
| 		ExpectedLen: 1, | ||||
| 	}, { | ||||
| 		Name:        "Store something", | ||||
| 		Action:      STORE, | ||||
| 		Key:         [2]uint64{2, 2}, | ||||
| 		Pages:       []uint64{4, 5, 6}, | ||||
| 		Expected:    newAllocList().With(1, 1, 1, 2, 3).With(2, 2, 4, 5, 6), | ||||
| 		ExpectedLen: 2, | ||||
| 	}, { | ||||
| 		Name:        "Remove something", | ||||
| 		Action:      REMOVE, | ||||
| 		Key:         [2]uint64{1, 1}, | ||||
| 		Expected:    newAllocList().With(2, 2, 4, 5, 6), | ||||
| 		ExpectedLen: 1, | ||||
| 	}} | ||||
|  | ||||
| 	al := newAllocList() | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		switch tc.Action { | ||||
| 		case CREATE: | ||||
| 			al.Create(tc.Key[0], tc.Key[1], tc.Page) | ||||
|  | ||||
| 		case PUSH: | ||||
| 			al.Push(tc.Key[0], tc.Key[1], tc.Page) | ||||
|  | ||||
| 		case STORE: | ||||
| 			al.Store(tc.Key[0], tc.Key[1], tc.Pages) | ||||
|  | ||||
| 		case REMOVE: | ||||
| 			al.Remove(tc.Key[0], tc.Key[1]) | ||||
|  | ||||
| 		default: | ||||
| 			t.Fatalf("Unknown action: %s", tc.Action) | ||||
| 		} | ||||
|  | ||||
| 		if !al.Equals(tc.Expected) { | ||||
| 			t.Fatal(tc.Name, al, tc.Expected) | ||||
| 		} | ||||
|  | ||||
| 		if al.Len() != tc.ExpectedLen { | ||||
| 			t.Fatal(tc.Name, al.Len(), tc.ExpectedLen) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestAllocListIterate_eachError(t *testing.T) { | ||||
| 	al := newAllocList().With(1, 1, 2, 3, 4, 5) | ||||
| 	myErr := errors.New("xxx") | ||||
| 	err := al.Iterate(func(collectionID, itemID uint64, pageIDs []uint64) error { | ||||
| 		return myErr | ||||
| 	}) | ||||
| 	if err != myErr { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestAllocListIterate(t *testing.T) { | ||||
| 	al := newAllocList().With(1, 1, 2, 3, 4, 5).With(2, 2, 6, 7) | ||||
| 	expected := map[uint64][]uint64{ | ||||
| 		1: {2, 3, 4, 5}, | ||||
| 		2: {6, 7}, | ||||
| 	} | ||||
|  | ||||
| 	err := al.Iterate(func(collectionID, itemID uint64, pageIDs []uint64) error { | ||||
| 		e, ok := expected[collectionID] | ||||
| 		if !ok { | ||||
| 			t.Fatalf("Not found: %d", collectionID) | ||||
| 		} | ||||
| 		if !reflect.DeepEqual(e, pageIDs) { | ||||
| 			t.Fatalf("%v != %v", pageIDs, e) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestAllocListPushNoHead(t *testing.T) { | ||||
| 	al := newAllocList().With(1, 1, 2, 3, 4, 5).With(2, 2, 6, 7) | ||||
| 	if !al.Push(1, 1, 8) { | ||||
| 		t.Fatal("Failed to push onto head page") | ||||
| 	} | ||||
| 	if al.Push(1, 2, 9) { | ||||
| 		t.Fatal("Pushed with no head.") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										58
									
								
								mdb/pfile/change_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								mdb/pfile/change_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| package pfile | ||||
|  | ||||
| import ( | ||||
| 	crand "crypto/rand" | ||||
| 	"git.crumpington.com/public/jldb/mdb/change" | ||||
| 	"math/rand" | ||||
| ) | ||||
|  | ||||
| func randomChangeList() (changes []change.Change) { | ||||
| 	count := 1 + rand.Intn(8) | ||||
| 	for i := 0; i < count; i++ { | ||||
| 		change := change.Change{ | ||||
| 			CollectionID: 1 + uint64(rand.Int63n(10)), | ||||
| 			ItemID:       1 + uint64(rand.Int63n(10)), | ||||
| 		} | ||||
|  | ||||
| 		if rand.Float32() < 0.95 { | ||||
| 			change.Data = randBytes(1 + rand.Intn(pageDataSize*4)) | ||||
| 			change.Store = true | ||||
| 		} | ||||
|  | ||||
| 		changes = append(changes, change) | ||||
| 	} | ||||
|  | ||||
| 	return changes | ||||
| } | ||||
|  | ||||
| type changeListBuilder []change.Change | ||||
|  | ||||
| func (b *changeListBuilder) Clear() *changeListBuilder { | ||||
| 	*b = (*b)[:0] | ||||
| 	return b | ||||
| } | ||||
|  | ||||
| func (b *changeListBuilder) Store(cID, iID, dataSize uint64) *changeListBuilder { | ||||
| 	data := make([]byte, dataSize) | ||||
| 	crand.Read(data) | ||||
| 	*b = append(*b, change.Change{ | ||||
| 		CollectionID: cID, | ||||
| 		ItemID:       iID, | ||||
| 		Store:        true, | ||||
| 		Data:         data, | ||||
| 	}) | ||||
| 	return b | ||||
| } | ||||
|  | ||||
| func (b *changeListBuilder) Delete(cID, iID uint64) *changeListBuilder { | ||||
| 	*b = append(*b, change.Change{ | ||||
| 		CollectionID: cID, | ||||
| 		ItemID:       iID, | ||||
| 		Store:        false, | ||||
| 	}) | ||||
| 	return b | ||||
| } | ||||
|  | ||||
| func (b *changeListBuilder) Build() []change.Change { | ||||
| 	return *b | ||||
| } | ||||
							
								
								
									
										67
									
								
								mdb/pfile/freelist.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								mdb/pfile/freelist.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| package pfile | ||||
|  | ||||
| import "container/heap" | ||||
|  | ||||
| // ---------------------------------------------------------------------------- | ||||
| // The intHeap is used to store the free list. | ||||
| // ---------------------------------------------------------------------------- | ||||
|  | ||||
| type intHeap []uint64 | ||||
|  | ||||
| func (h intHeap) Len() int           { return len(h) } | ||||
| func (h intHeap) Less(i, j int) bool { return h[i] < h[j] } | ||||
| func (h intHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] } | ||||
|  | ||||
| func (h *intHeap) Push(x any) { | ||||
| 	// Push and Pop use pointer receivers because they modify the slice's length, | ||||
| 	// not just its contents. | ||||
| 	*h = append(*h, x.(uint64)) | ||||
| } | ||||
|  | ||||
| func (h *intHeap) Pop() any { | ||||
| 	old := *h | ||||
| 	n := len(old) | ||||
| 	x := old[n-1] | ||||
| 	*h = old[0 : n-1] | ||||
| 	return x | ||||
| } | ||||
|  | ||||
| // ---------------------------------------------------------------------------- | ||||
| // Free list | ||||
| // ---------------------------------------------------------------------------- | ||||
|  | ||||
| type freeList struct { | ||||
| 	h        intHeap | ||||
| 	nextPage uint64 | ||||
| } | ||||
|  | ||||
| // newFreeList creates a new free list that will return available pages from | ||||
| // smallest to largest. If there are no available pages, it will return new | ||||
| // pages starting from nextPage. | ||||
| func newFreeList(pageCount uint64) *freeList { | ||||
| 	return &freeList{ | ||||
| 		h:        []uint64{}, | ||||
| 		nextPage: pageCount, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (f *freeList) Push(pages ...uint64) { | ||||
| 	for _, page := range pages { | ||||
| 		heap.Push(&f.h, page) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (f *freeList) Pop(count int, out []uint64) []uint64 { | ||||
| 	out = out[:0] | ||||
|  | ||||
| 	for len(out) < count && len(f.h) > 0 { | ||||
| 		out = append(out, heap.Pop(&f.h).(uint64)) | ||||
| 	} | ||||
|  | ||||
| 	for len(out) < count { | ||||
| 		out = append(out, f.nextPage) | ||||
| 		f.nextPage++ | ||||
| 	} | ||||
|  | ||||
| 	return out | ||||
| } | ||||
							
								
								
									
										90
									
								
								mdb/pfile/freelist_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								mdb/pfile/freelist_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| package pfile | ||||
|  | ||||
| import ( | ||||
| 	"math/rand" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func (fl *freeList) Assert(t *testing.T, pageIDs ...uint64) { | ||||
| 	t.Helper() | ||||
|  | ||||
| 	if len(fl.h) != len(pageIDs) { | ||||
| 		t.Fatalf("FreeList: Expected %d pages but got %d.\n%v != %v", | ||||
| 			len(pageIDs), len(fl.h), fl.h, pageIDs) | ||||
| 	} | ||||
|  | ||||
| 	containsPageID := func(pageID uint64) bool { | ||||
| 		for _, v := range fl.h { | ||||
| 			if v == pageID { | ||||
| 				return true | ||||
| 			} | ||||
| 		} | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	for _, pageID := range pageIDs { | ||||
| 		if !containsPageID(pageID) { | ||||
| 			t.Fatalf("Page not free: %d", pageID) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestFreeList(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	p0 := uint64(1 + rand.Int63()) | ||||
|  | ||||
| 	type TestCase struct { | ||||
| 		Name     string | ||||
| 		Put      []uint64 | ||||
| 		Alloc    int | ||||
| 		Expected []uint64 | ||||
| 	} | ||||
|  | ||||
| 	testCases := []TestCase{ | ||||
| 		{ | ||||
| 			Name:     "Alloc first page", | ||||
| 			Put:      []uint64{}, | ||||
| 			Alloc:    1, | ||||
| 			Expected: []uint64{p0}, | ||||
| 		}, { | ||||
| 			Name:     "Alloc second page", | ||||
| 			Put:      []uint64{}, | ||||
| 			Alloc:    1, | ||||
| 			Expected: []uint64{p0 + 1}, | ||||
| 		}, { | ||||
| 			Name:     "Put second page", | ||||
| 			Put:      []uint64{p0 + 1}, | ||||
| 			Alloc:    0, | ||||
| 			Expected: []uint64{}, | ||||
| 		}, { | ||||
| 			Name:     "Alloc 2 pages", | ||||
| 			Put:      []uint64{}, | ||||
| 			Alloc:    2, | ||||
| 			Expected: []uint64{p0 + 1, p0 + 2}, | ||||
| 		}, { | ||||
| 			Name:     "Put back and alloc pages", | ||||
| 			Put:      []uint64{p0}, | ||||
| 			Alloc:    3, | ||||
| 			Expected: []uint64{p0, p0 + 3, p0 + 4}, | ||||
| 		}, { | ||||
| 			Name:     "Put back large and alloc", | ||||
| 			Put:      []uint64{p0, p0 + 2, p0 + 4, p0 + 442}, | ||||
| 			Alloc:    4, | ||||
| 			Expected: []uint64{p0, p0 + 2, p0 + 4, p0 + 442}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	fl := newFreeList(p0) | ||||
|  | ||||
| 	var pages []uint64 | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		fl.Push(tc.Put...) | ||||
| 		pages = fl.Pop(tc.Alloc, pages) | ||||
| 		if !reflect.DeepEqual(pages, tc.Expected) { | ||||
| 			t.Fatal(tc.Name, pages, tc.Expected) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										1
									
								
								mdb/pfile/header.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								mdb/pfile/header.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| package pfile | ||||
							
								
								
									
										105
									
								
								mdb/pfile/index.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								mdb/pfile/index.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| package pfile | ||||
|  | ||||
| import ( | ||||
| 	"git.crumpington.com/public/jldb/lib/errs" | ||||
| 	"git.crumpington.com/public/jldb/mdb/change" | ||||
| ) | ||||
|  | ||||
| type Index struct { | ||||
| 	fList *freeList | ||||
| 	aList allocList | ||||
| 	seen  map[[2]uint64]struct{} | ||||
| 	mask  []bool | ||||
| } | ||||
|  | ||||
| func NewIndex(f *File) (*Index, error) { | ||||
| 	idx := &Index{ | ||||
| 		fList: newFreeList(0), | ||||
| 		aList: *newAllocList(), | ||||
| 		seen:  map[[2]uint64]struct{}{}, | ||||
| 		mask:  []bool{}, | ||||
| 	} | ||||
|  | ||||
| 	err := f.iterate(func(pageID uint64, page dataPage) error { | ||||
| 		header := page.Header() | ||||
| 		switch header.PageType { | ||||
| 		case pageTypeHead: | ||||
| 			idx.aList.Create(header.CollectionID, header.ItemID, pageID) | ||||
| 		case pageTypeData: | ||||
| 			if !idx.aList.Push(header.CollectionID, header.ItemID, pageID) { | ||||
| 				return errs.Corrupt.WithMsg("encountered data page with no corresponding head page") | ||||
| 			} | ||||
| 		case pageTypeFree: | ||||
| 			idx.fList.Push(pageID) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
|  | ||||
| 	return idx, err | ||||
| } | ||||
|  | ||||
| func (idx *Index) StageChanges(changes []change.Change) { | ||||
| 	clear(idx.seen) | ||||
| 	if cap(idx.mask) < len(changes) { | ||||
| 		idx.mask = make([]bool, len(changes)) | ||||
| 	} | ||||
| 	idx.mask = idx.mask[:len(changes)] | ||||
|  | ||||
| 	for i := len(changes) - 1; i >= 0; i-- { | ||||
| 		key := [2]uint64{changes[i].CollectionID, changes[i].ItemID} | ||||
| 		if _, ok := idx.seen[key]; ok { | ||||
| 			idx.mask[i] = false | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		idx.seen[key] = struct{}{} | ||||
| 		idx.mask[i] = true | ||||
| 	} | ||||
|  | ||||
| 	for i, active := range idx.mask { | ||||
| 		if !active { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if changes[i].Store { | ||||
| 			count := idx.getPageCountForData(len(changes[i].Data)) | ||||
| 			changes[i].WritePageIDs = idx.fList.Pop(count, changes[i].WritePageIDs) | ||||
| 		} | ||||
|  | ||||
| 		if pages := idx.aList.Remove(changes[i].CollectionID, changes[i].ItemID); pages != nil { | ||||
| 			changes[i].ClearPageIDs = pages | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (idx *Index) UnstageChanges(changes []change.Change) { | ||||
| 	for i := range changes { | ||||
| 		if len(changes[i].WritePageIDs) > 0 { | ||||
| 			idx.fList.Push(changes[i].WritePageIDs...) | ||||
| 			changes[i].WritePageIDs = changes[i].WritePageIDs[:0] | ||||
| 		} | ||||
| 		if len(changes[i].ClearPageIDs) > 0 { | ||||
| 			idx.aList.Store(changes[i].CollectionID, changes[i].ItemID, changes[i].ClearPageIDs) | ||||
| 			changes[i].ClearPageIDs = changes[i].ClearPageIDs[:0] | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (idx *Index) ApplyChanges(changes []change.Change) { | ||||
| 	for i := range changes { | ||||
| 		if len(changes[i].WritePageIDs) > 0 { | ||||
| 			idx.aList.Store(changes[i].CollectionID, changes[i].ItemID, changes[i].WritePageIDs) | ||||
| 		} | ||||
| 		if len(changes[i].ClearPageIDs) > 0 { | ||||
| 			idx.fList.Push(changes[i].ClearPageIDs...) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (idx *Index) getPageCountForData(dataSize int) int { | ||||
| 	count := dataSize / pageDataSize | ||||
| 	if dataSize%pageDataSize != 0 { | ||||
| 		count++ | ||||
| 	} | ||||
| 	return count | ||||
| } | ||||
							
								
								
									
										139
									
								
								mdb/pfile/index_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								mdb/pfile/index_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| package pfile | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| type IndexState struct { | ||||
| 	FreeList  []uint64 | ||||
| 	AllocList map[[2]uint64][]uint64 | ||||
| } | ||||
|  | ||||
| func (idx *Index) Assert(t *testing.T, state IndexState) { | ||||
| 	t.Helper() | ||||
|  | ||||
| 	idx.fList.Assert(t, state.FreeList...) | ||||
| 	idx.aList.Assert(t, state.AllocList) | ||||
| } | ||||
|  | ||||
| func TestIndex(t *testing.T) { | ||||
| 	pf, idx := newForTesting(t) | ||||
| 	defer pf.Close() | ||||
|  | ||||
| 	idx.Assert(t, IndexState{ | ||||
| 		FreeList:  []uint64{}, | ||||
| 		AllocList: map[[2]uint64][]uint64{}, | ||||
| 	}) | ||||
|  | ||||
| 	p0 := uint64(0) | ||||
|  | ||||
| 	l := (&changeListBuilder{}). | ||||
| 		Store(1, 1, pageDataSize+1). | ||||
| 		Build() | ||||
|  | ||||
| 	idx.StageChanges(l) | ||||
|  | ||||
| 	idx.Assert(t, IndexState{ | ||||
| 		FreeList:  []uint64{}, | ||||
| 		AllocList: map[[2]uint64][]uint64{}, | ||||
| 	}) | ||||
|  | ||||
| 	// Unstage a change: free-list gets pages back. | ||||
| 	idx.UnstageChanges(l) | ||||
|  | ||||
| 	idx.Assert(t, IndexState{ | ||||
| 		FreeList:  []uint64{p0, p0 + 1}, | ||||
| 		AllocList: map[[2]uint64][]uint64{}, | ||||
| 	}) | ||||
|  | ||||
| 	// Stage a change: free-list entries are used again. | ||||
| 	l = (*changeListBuilder)(&l). | ||||
| 		Clear(). | ||||
| 		Store(1, 1, pageDataSize+1). | ||||
| 		Store(2, 2, pageDataSize-1). | ||||
| 		Store(3, 3, pageDataSize). | ||||
| 		Build() | ||||
|  | ||||
| 	idx.StageChanges(l) | ||||
|  | ||||
| 	idx.Assert(t, IndexState{ | ||||
| 		FreeList:  []uint64{}, | ||||
| 		AllocList: map[[2]uint64][]uint64{}, | ||||
| 	}) | ||||
|  | ||||
| 	// Apply changes: alloc-list is updated. | ||||
| 	idx.ApplyChanges(l) | ||||
|  | ||||
| 	idx.Assert(t, IndexState{ | ||||
| 		FreeList: []uint64{}, | ||||
| 		AllocList: map[[2]uint64][]uint64{ | ||||
| 			{1, 1}: {p0, p0 + 1}, | ||||
| 			{2, 2}: {p0 + 2}, | ||||
| 			{3, 3}: {p0 + 3}, | ||||
| 		}, | ||||
| 	}) | ||||
|  | ||||
| 	// Clear some things. | ||||
| 	l = (*changeListBuilder)(&l). | ||||
| 		Clear(). | ||||
| 		Store(1, 1, pageDataSize). | ||||
| 		Delete(2, 2). | ||||
| 		Build() | ||||
|  | ||||
| 	idx.StageChanges(l) | ||||
|  | ||||
| 	idx.Assert(t, IndexState{ | ||||
| 		FreeList: []uint64{}, | ||||
| 		AllocList: map[[2]uint64][]uint64{ | ||||
| 			{3, 3}: {p0 + 3}, | ||||
| 		}, | ||||
| 	}) | ||||
|  | ||||
| 	// Ustaging will push the staged page p0+4 into the free list. | ||||
| 	idx.UnstageChanges(l) | ||||
|  | ||||
| 	idx.Assert(t, IndexState{ | ||||
| 		FreeList: []uint64{p0 + 4}, | ||||
| 		AllocList: map[[2]uint64][]uint64{ | ||||
| 			{1, 1}: {p0, p0 + 1}, | ||||
| 			{2, 2}: {p0 + 2}, | ||||
| 			{3, 3}: {p0 + 3}, | ||||
| 		}, | ||||
| 	}) | ||||
|  | ||||
| 	idx.StageChanges(l) | ||||
|  | ||||
| 	idx.Assert(t, IndexState{ | ||||
| 		FreeList: []uint64{}, | ||||
| 		AllocList: map[[2]uint64][]uint64{ | ||||
| 			{3, 3}: {p0 + 3}, | ||||
| 		}, | ||||
| 	}) | ||||
|  | ||||
| 	idx.ApplyChanges(l) | ||||
|  | ||||
| 	idx.Assert(t, IndexState{ | ||||
| 		FreeList: []uint64{p0, p0 + 1, p0 + 2}, | ||||
| 		AllocList: map[[2]uint64][]uint64{ | ||||
| 			{1, 1}: {p0 + 4}, | ||||
| 			{3, 3}: {p0 + 3}, | ||||
| 		}, | ||||
| 	}) | ||||
|  | ||||
| 	// Duplicate updates. | ||||
| 	l = (*changeListBuilder)(&l). | ||||
| 		Clear(). | ||||
| 		Store(2, 2, pageDataSize). | ||||
| 		Store(3, 3, pageDataSize+1). | ||||
| 		Store(3, 3, pageDataSize). | ||||
| 		Build() | ||||
|  | ||||
| 	idx.StageChanges(l) | ||||
|  | ||||
| 	idx.Assert(t, IndexState{ | ||||
| 		FreeList: []uint64{p0 + 2}, | ||||
| 		AllocList: map[[2]uint64][]uint64{ | ||||
| 			{1, 1}: {p0 + 4}, | ||||
| 		}, | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										18
									
								
								mdb/pfile/iterate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								mdb/pfile/iterate.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| package pfile | ||||
|  | ||||
| import "bytes" | ||||
|  | ||||
| func IterateAllocated( | ||||
| 	pf *File, | ||||
| 	idx *Index, | ||||
| 	each func(collectionID, itemID uint64, data []byte) error, | ||||
| ) error { | ||||
| 	buf := &bytes.Buffer{} | ||||
| 	return idx.aList.Iterate(func(collectionID, itemID uint64, pages []uint64) error { | ||||
| 		buf.Reset() | ||||
| 		if err := pf.readData(pages[0], buf); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		return each(collectionID, itemID, buf.Bytes()) | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										64
									
								
								mdb/pfile/main_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								mdb/pfile/main_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| package pfile | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	crand "crypto/rand" | ||||
| 	"git.crumpington.com/public/jldb/lib/wal" | ||||
| 	"git.crumpington.com/public/jldb/mdb/change" | ||||
| 	"path/filepath" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func newForTesting(t *testing.T) (*File, *Index) { | ||||
| 	t.Helper() | ||||
|  | ||||
| 	tmpDir := t.TempDir() | ||||
| 	filePath := filepath.Join(tmpDir, "pagefile") | ||||
|  | ||||
| 	pf, err := Open(filePath) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	idx, err := NewIndex(pf) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	return pf, idx | ||||
| } | ||||
|  | ||||
| func randBytes(size int) []byte { | ||||
| 	buf := make([]byte, size) | ||||
| 	if _, err := crand.Read(buf); err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	return buf | ||||
| } | ||||
|  | ||||
| func changesToRec(changes []change.Change) wal.Record { | ||||
| 	buf := &bytes.Buffer{} | ||||
| 	if err := change.Write(changes, buf); err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	return wal.Record{ | ||||
| 		DataSize: int64(buf.Len()), | ||||
| 		Reader:   buf, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestChangesToRec(t *testing.T) { | ||||
| 	changes := []change.Change{ | ||||
| 		{ | ||||
| 			CollectionID: 2, | ||||
| 			ItemID:       3, | ||||
| 			Store:        true, | ||||
| 			Data:         []byte{2, 3, 4}, | ||||
| 			WritePageIDs: []uint64{0, 1}, | ||||
| 			ClearPageIDs: []uint64{2, 3}, | ||||
| 		}, | ||||
| 	} | ||||
| 	rec := changesToRec(changes) | ||||
| 	c2 := []change.Change{} | ||||
| 	c2, _ = change.Read(c2, rec.Reader) | ||||
| } | ||||
							
								
								
									
										70
									
								
								mdb/pfile/page.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								mdb/pfile/page.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| package pfile | ||||
|  | ||||
| import ( | ||||
| 	"hash/crc32" | ||||
| 	"git.crumpington.com/public/jldb/lib/errs" | ||||
| 	"unsafe" | ||||
| ) | ||||
|  | ||||
| // ---------------------------------------------------------------------------- | ||||
|  | ||||
| const ( | ||||
| 	pageSize       = 512 | ||||
| 	pageHeaderSize = 40 | ||||
| 	pageDataSize   = pageSize - pageHeaderSize | ||||
|  | ||||
| 	pageTypeFree = 0 | ||||
| 	pageTypeHead = 1 | ||||
| 	pageTypeData = 2 | ||||
| ) | ||||
|  | ||||
| var emptyPage = func() dataPage { | ||||
| 	p := newDataPage() | ||||
| 	h := p.Header() | ||||
| 	h.CRC = p.ComputeCRC() | ||||
| 	return p | ||||
| }() | ||||
|  | ||||
| // ---------------------------------------------------------------------------- | ||||
|  | ||||
| type pageHeader struct { | ||||
| 	CRC          uint32 // IEEE CRC-32 checksum. | ||||
| 	PageType     uint32 // One of the PageType* constants. | ||||
| 	CollectionID uint64  // | ||||
| 	ItemID       uint64 | ||||
| 	DataSize     uint64 | ||||
| 	NextPage     uint64 | ||||
| } | ||||
|  | ||||
| // ---------------------------------------------------------------------------- | ||||
|  | ||||
| type dataPage []byte | ||||
|  | ||||
| func newDataPage() dataPage { | ||||
| 	p := dataPage(make([]byte, pageSize)) | ||||
| 	return p | ||||
| } | ||||
|  | ||||
| func (p dataPage) Header() *pageHeader { | ||||
| 	return (*pageHeader)(unsafe.Pointer(&p[0])) | ||||
| } | ||||
|  | ||||
| func (p dataPage) ComputeCRC() uint32 { | ||||
| 	return crc32.ChecksumIEEE(p[4:]) | ||||
| } | ||||
|  | ||||
| func (p dataPage) Data() []byte { | ||||
| 	return p[pageHeaderSize:] | ||||
| } | ||||
|  | ||||
| func (p dataPage) Write(data []byte) int { | ||||
| 	return copy(p[pageHeaderSize:], data) | ||||
| } | ||||
|  | ||||
| func (p dataPage) Validate() error { | ||||
| 	header := p.Header() | ||||
| 	if header.CRC != p.ComputeCRC() { | ||||
| 		return errs.Corrupt.WithMsg("CRC mismatch on data page.") | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										103
									
								
								mdb/pfile/page_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								mdb/pfile/page_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| package pfile | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	crand "crypto/rand" | ||||
| 	"git.crumpington.com/public/jldb/lib/errs" | ||||
| 	"math/rand" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func randomPage(t *testing.T) dataPage { | ||||
| 	p := newDataPage() | ||||
| 	h := p.Header() | ||||
|  | ||||
| 	x := rand.Float32() | ||||
| 	if x > 0.66 { | ||||
| 		h.PageType = pageTypeFree | ||||
| 		h.DataSize = 0 | ||||
| 	} else if x < 0.33 { | ||||
| 		h.PageType = pageTypeHead | ||||
| 		h.DataSize = rand.Uint64() | ||||
| 	} else { | ||||
| 		h.PageType = pageTypeData | ||||
| 		h.DataSize = rand.Uint64() | ||||
| 	} | ||||
|  | ||||
| 	h.CollectionID = rand.Uint64() | ||||
| 	h.ItemID = rand.Uint64() | ||||
|  | ||||
| 	dataSize := h.DataSize | ||||
| 	if h.DataSize > pageDataSize { | ||||
| 		dataSize = pageDataSize | ||||
| 	} | ||||
|  | ||||
| 	if _, err := crand.Read(p.Data()[:dataSize]); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	h.CRC = p.ComputeCRC() | ||||
|  | ||||
| 	return p | ||||
| } | ||||
|  | ||||
| // ---------------------------------------------------------------------------- | ||||
|  | ||||
| func TestPageValidate(t *testing.T) { | ||||
| 	for i := 0; i < 100; i++ { | ||||
| 		p := randomPage(t) | ||||
|  | ||||
| 		// Should be valid initially. | ||||
| 		if err := p.Validate(); err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		for i := 0; i < pageSize; i++ { | ||||
| 			p[i]++ | ||||
| 			if err := p.Validate(); !errs.Corrupt.Is(err) { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			p[i]-- | ||||
| 		} | ||||
|  | ||||
| 		// Should be valid initially. | ||||
| 		if err := p.Validate(); err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestPageEmptyIsValid(t *testing.T) { | ||||
| 	if err := emptyPage.Validate(); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestPageWrite(t *testing.T) { | ||||
| 	for i := 0; i < 100; i++ { | ||||
| 		page := newDataPage() | ||||
| 		h := page.Header() | ||||
| 		h.PageType = pageTypeData | ||||
| 		h.CollectionID = rand.Uint64() | ||||
| 		h.ItemID = rand.Uint64() | ||||
| 		h.DataSize = uint64(1 + rand.Int63n(2*pageDataSize)) | ||||
|  | ||||
| 		data := make([]byte, h.DataSize) | ||||
| 		crand.Read(data) | ||||
|  | ||||
| 		n := page.Write(data) | ||||
| 		h.CRC = page.ComputeCRC() | ||||
|  | ||||
| 		if n > pageDataSize || n < 1 { | ||||
| 			t.Fatal(n) | ||||
| 		} | ||||
|  | ||||
| 		if !bytes.Equal(data[:n], page.Data()[:n]) { | ||||
| 			t.Fatal(data[:n], page.Data()[:n]) | ||||
| 		} | ||||
|  | ||||
| 		if err := page.Validate(); err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										307
									
								
								mdb/pfile/pagefile.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										307
									
								
								mdb/pfile/pagefile.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,307 @@ | ||||
| package pfile | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"compress/gzip" | ||||
| 	"encoding/binary" | ||||
| 	"io" | ||||
| 	"git.crumpington.com/public/jldb/lib/errs" | ||||
| 	"git.crumpington.com/public/jldb/mdb/change" | ||||
| 	"net" | ||||
| 	"os" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type File struct { | ||||
| 	lock sync.RWMutex | ||||
| 	f    *os.File | ||||
| 	page dataPage | ||||
| } | ||||
|  | ||||
| func Open(path string) (*File, error) { | ||||
| 	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0600) | ||||
| 	if err != nil { | ||||
| 		return nil, errs.IO.WithErr(err) | ||||
| 	} | ||||
|  | ||||
| 	pf := &File{f: f} | ||||
| 	pf.page = newDataPage() | ||||
|  | ||||
| 	return pf, nil | ||||
| } | ||||
|  | ||||
| func (pf *File) Close() error { | ||||
| 	pf.lock.Lock() | ||||
| 	defer pf.lock.Unlock() | ||||
|  | ||||
| 	if err := pf.f.Close(); err != nil { | ||||
| 		return errs.IO.WithErr(err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // ---------------------------------------------------------------------------- | ||||
| // Writing | ||||
| // ---------------------------------------------------------------------------- | ||||
|  | ||||
| func (pf *File) ApplyChanges(changes []change.Change) error { | ||||
| 	pf.lock.Lock() | ||||
| 	defer pf.lock.Unlock() | ||||
|  | ||||
| 	return pf.applyChanges(changes) | ||||
| } | ||||
|  | ||||
| func (pf *File) applyChanges(changes []change.Change) error { | ||||
| 	for _, change := range changes { | ||||
| 		if len(change.WritePageIDs) > 0 { | ||||
| 			if err := pf.writeChangePages(change); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		for _, id := range change.ClearPageIDs { | ||||
| 			if err := pf.writePage(emptyPage, id); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err := pf.f.Sync(); err != nil { | ||||
| 		return errs.IO.WithErr(err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (pf *File) writeChangePages(change change.Change) error { | ||||
| 	page := pf.page | ||||
|  | ||||
| 	header := page.Header() | ||||
|  | ||||
| 	header.PageType = pageTypeHead | ||||
| 	header.CollectionID = change.CollectionID | ||||
| 	header.ItemID = change.ItemID | ||||
| 	header.DataSize = uint64(len(change.Data)) | ||||
|  | ||||
| 	pageIDs := change.WritePageIDs | ||||
| 	data := change.Data | ||||
|  | ||||
| 	for len(change.Data) > 0 && len(pageIDs) > 0 { | ||||
| 		pageID := pageIDs[0] | ||||
| 		pageIDs = pageIDs[1:] | ||||
|  | ||||
| 		if len(pageIDs) > 0 { | ||||
| 			header.NextPage = pageIDs[0] | ||||
| 		} else { | ||||
| 			header.NextPage = 0 | ||||
| 		} | ||||
|  | ||||
| 		n := page.Write(data) | ||||
| 		data = data[n:] | ||||
|  | ||||
| 		page.Header().CRC = page.ComputeCRC() | ||||
|  | ||||
| 		if err := pf.writePage(page, pageID); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		// All but first page has pageTypeData. | ||||
| 		header.PageType = pageTypeData | ||||
| 	} | ||||
|  | ||||
| 	if len(pageIDs) > 0 { | ||||
| 		return errs.Unexpected.WithMsg("Too many pages provided for given data.") | ||||
| 	} | ||||
|  | ||||
| 	if len(data) > 0 { | ||||
| 		return errs.Unexpected.WithMsg("Not enough pages for given data.") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (pf *File) writePage(page dataPage, id uint64) error { | ||||
| 	if _, err := pf.f.WriteAt(page, int64(id*pageSize)); err != nil { | ||||
| 		return errs.IO.WithErr(err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // ---------------------------------------------------------------------------- | ||||
| // Reading | ||||
| // ---------------------------------------------------------------------------- | ||||
|  | ||||
| func (pf *File) iterate(each func(pageID uint64, page dataPage) error) error { | ||||
| 	pf.lock.RLock() | ||||
| 	defer pf.lock.RUnlock() | ||||
|  | ||||
| 	page := pf.page | ||||
|  | ||||
| 	fi, err := pf.f.Stat() | ||||
| 	if err != nil { | ||||
| 		return errs.IO.WithErr(err) | ||||
| 	} | ||||
|  | ||||
| 	fileSize := fi.Size() | ||||
| 	if fileSize%pageSize != 0 { | ||||
| 		return errs.Corrupt.WithMsg("File size isn't a multiple of page size.") | ||||
| 	} | ||||
|  | ||||
| 	maxPage := uint64(fileSize / pageSize) | ||||
| 	if _, err := pf.f.Seek(0, io.SeekStart); err != nil { | ||||
| 		return errs.IO.WithErr(err) | ||||
| 	} | ||||
|  | ||||
| 	r := bufio.NewReaderSize(pf.f, 1024*1024) | ||||
|  | ||||
| 	for pageID := uint64(0); pageID < maxPage; pageID++ { | ||||
| 		if _, err := r.Read(page); err != nil { | ||||
| 			return errs.IO.WithErr(err) | ||||
| 		} | ||||
|  | ||||
| 		if err := page.Validate(); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if err := each(pageID, page); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (pf *File) readData(id uint64, buf *bytes.Buffer) error { | ||||
| 	page := pf.page | ||||
|  | ||||
| 	// The head page. | ||||
| 	if err := pf.readPage(page, id); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	remaining := int(page.Header().DataSize) | ||||
|  | ||||
| 	for { | ||||
| 		data := page.Data() | ||||
| 		if len(data) > remaining { | ||||
| 			data = data[:remaining] | ||||
| 		} | ||||
|  | ||||
| 		buf.Write(data) | ||||
| 		remaining -= len(data) | ||||
|  | ||||
| 		if page.Header().NextPage == 0 { | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		if err := pf.readPage(page, page.Header().NextPage); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if remaining != 0 { | ||||
| 		return errs.Corrupt.WithMsg("Incorrect data size. %d remaining.", remaining) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (pf *File) readPage(p dataPage, id uint64) error { | ||||
| 	if _, err := pf.f.ReadAt(p, int64(id*pageSize)); err != nil { | ||||
| 		return errs.IO.WithErr(err) | ||||
| 	} | ||||
| 	return p.Validate() | ||||
| } | ||||
|  | ||||
| // ---------------------------------------------------------------------------- | ||||
| // Send / Recv | ||||
| // ---------------------------------------------------------------------------- | ||||
|  | ||||
| func (pf *File) Send(conn net.Conn, timeout time.Duration) error { | ||||
| 	pf.lock.RLock() | ||||
| 	defer pf.lock.RUnlock() | ||||
|  | ||||
| 	if _, err := pf.f.Seek(0, io.SeekStart); err != nil { | ||||
| 		return errs.IO.WithErr(err) | ||||
| 	} | ||||
|  | ||||
| 	fi, err := pf.f.Stat() | ||||
| 	if err != nil { | ||||
| 		return errs.IO.WithErr(err) | ||||
| 	} | ||||
|  | ||||
| 	remaining := fi.Size() | ||||
|  | ||||
| 	conn.SetWriteDeadline(time.Now().Add(timeout)) | ||||
| 	if err := binary.Write(conn, binary.LittleEndian, remaining); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	buf := make([]byte, 1024*1024) | ||||
| 	w, err := gzip.NewWriterLevel(conn, 3) | ||||
| 	if err != nil { | ||||
| 		return errs.Unexpected.WithErr(err) | ||||
| 	} | ||||
| 	defer w.Close() | ||||
|  | ||||
| 	for remaining > 0 { | ||||
| 		n, err := pf.f.Read(buf) | ||||
| 		if err != nil { | ||||
| 			return errs.IO.WithErr(err) | ||||
| 		} | ||||
|  | ||||
| 		conn.SetWriteDeadline(time.Now().Add(timeout)) | ||||
| 		if _, err := w.Write(buf[:n]); err != nil { | ||||
| 			return errs.IO.WithErr(err) | ||||
| 		} | ||||
|  | ||||
| 		remaining -= int64(n) | ||||
| 		w.Flush() | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func Recv(conn net.Conn, filePath string, timeout time.Duration) error { | ||||
| 	defer conn.Close() | ||||
|  | ||||
| 	f, err := os.Create(filePath) | ||||
| 	if err != nil { | ||||
| 		return errs.IO.WithErr(err) | ||||
| 	} | ||||
| 	defer f.Close() | ||||
|  | ||||
| 	remaining := uint64(0) | ||||
| 	if err := binary.Read(conn, binary.LittleEndian, &remaining); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	r, err := gzip.NewReader(conn) | ||||
| 	if err != nil { | ||||
| 		return errs.Unexpected.WithErr(err) | ||||
| 	} | ||||
| 	defer r.Close() | ||||
|  | ||||
| 	buf := make([]byte, 1024*1024) | ||||
| 	for remaining > 0 { | ||||
|  | ||||
| 		conn.SetReadDeadline(time.Now().Add(timeout)) | ||||
|  | ||||
| 		n, err := io.ReadFull(r, buf) | ||||
| 		if err != nil && n == 0 { | ||||
| 			return errs.IO.WithErr(err) | ||||
| 		} | ||||
| 		remaining -= uint64(n) | ||||
|  | ||||
| 		if _, err := f.Write(buf[:n]); err != nil { | ||||
| 			return errs.IO.WithErr(err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err := f.Sync(); err != nil { | ||||
| 		return errs.IO.WithErr(err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										94
									
								
								mdb/pfile/pagefile_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								mdb/pfile/pagefile_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| package pfile | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| type FileState struct { | ||||
| 	SeqNum uint64 | ||||
| 	Data   map[[2]uint64][]byte | ||||
| } | ||||
|  | ||||
| func (pf *File) Assert(t *testing.T, state pFileState) { | ||||
| 	t.Helper() | ||||
|  | ||||
| 	pf.lock.RLock() | ||||
| 	defer pf.lock.RUnlock() | ||||
|  | ||||
| 	idx, err := NewIndex(pf) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	data := map[[2]uint64][]byte{} | ||||
| 	err = IterateAllocated(pf, idx, func(cID, iID uint64, fileData []byte) error { | ||||
| 		data[[2]uint64{cID, iID}] = bytes.Clone(fileData) | ||||
| 		return nil | ||||
| 	}) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if len(data) != len(state.Data) { | ||||
| 		t.Fatalf("Expected %d items but got %d.", len(state.Data), len(data)) | ||||
| 	} | ||||
|  | ||||
| 	for key, expected := range state.Data { | ||||
| 		val, ok := data[key] | ||||
| 		if !ok { | ||||
| 			t.Fatalf("No data found for key %v.", key) | ||||
| 		} | ||||
| 		if !bytes.Equal(val, expected) { | ||||
| 			t.Fatalf("Incorrect data for key %v.", key) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestFileStateUpdateRandom(t *testing.T) { | ||||
| 	t.Parallel() | ||||
| 	tmpDir := t.TempDir() | ||||
| 	walDir := filepath.Join(tmpDir, "wal") | ||||
| 	pageFilePath := filepath.Join(tmpDir, "pagefile") | ||||
|  | ||||
| 	if err := os.MkdirAll(walDir, 0700); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	pf, err := Open(pageFilePath) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	idx, err := NewIndex(pf) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	state := pFileState{ | ||||
| 		Data: map[[2]uint64][]byte{}, | ||||
| 	} | ||||
|  | ||||
| 	for i := uint64(1); i < 256; i++ { | ||||
| 		changes := randomChangeList() | ||||
| 		idx.StageChanges(changes) | ||||
|  | ||||
| 		if err := pf.ApplyChanges(changes); err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		idx.ApplyChanges(changes) | ||||
|  | ||||
| 		for _, ch := range changes { | ||||
| 			if !ch.Store { | ||||
| 				delete(state.Data, [2]uint64{ch.CollectionID, ch.ItemID}) | ||||
| 			} else { | ||||
| 				state.Data[[2]uint64{ch.CollectionID, ch.ItemID}] = ch.Data | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		pf.Assert(t, state) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										57
									
								
								mdb/pfile/record_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								mdb/pfile/record_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| package pfile | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"git.crumpington.com/public/jldb/lib/wal" | ||||
| 	"git.crumpington.com/public/jldb/mdb/change" | ||||
| ) | ||||
|  | ||||
| // ---------------------------------------------------------------------------- | ||||
|  | ||||
| type pFileState struct { | ||||
| 	Data map[[2]uint64][]byte | ||||
| } | ||||
|  | ||||
| // ---------------------------------------------------------------------------- | ||||
|  | ||||
| type recBuilder struct { | ||||
| 	changes []change.Change | ||||
| 	rec     wal.Record | ||||
| } | ||||
|  | ||||
| func NewRecBuilder(seqNum, timestamp int64) *recBuilder { | ||||
| 	return &recBuilder{ | ||||
| 		rec: wal.Record{ | ||||
| 			SeqNum:      seqNum, | ||||
| 			TimestampMS: timestamp, | ||||
| 		}, | ||||
| 		changes: []change.Change{}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *recBuilder) Store(cID, iID uint64, data string) *recBuilder { | ||||
| 	b.changes = append(b.changes, change.Change{ | ||||
| 		CollectionID: cID, | ||||
| 		ItemID:       iID, | ||||
| 		Store:        true, | ||||
| 		Data:         []byte(data), | ||||
| 	}) | ||||
| 	return b | ||||
| } | ||||
|  | ||||
| func (b *recBuilder) Delete(cID, iID uint64) *recBuilder { | ||||
| 	b.changes = append(b.changes, change.Change{ | ||||
| 		CollectionID: cID, | ||||
| 		ItemID:       iID, | ||||
| 		Store:        false, | ||||
| 	}) | ||||
| 	return b | ||||
| } | ||||
|  | ||||
| func (b *recBuilder) Record() wal.Record { | ||||
| 	buf := &bytes.Buffer{} | ||||
| 	change.Write(b.changes, buf) | ||||
| 	b.rec.DataSize = int64(buf.Len()) | ||||
| 	b.rec.Reader = buf | ||||
| 	return b.rec | ||||
| } | ||||
							
								
								
									
										62
									
								
								mdb/pfile/sendrecv_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								mdb/pfile/sendrecv_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| package pfile | ||||
|  | ||||
| /* | ||||
| func TestSendRecv(t *testing.T) { | ||||
| 	tmpDir := t.TempDir() | ||||
| 	filePath1 := filepath.Join(tmpDir, "1") | ||||
| 	filePath2 := filepath.Join(tmpDir, "2") | ||||
| 	defer os.RemoveAll(tmpDir) | ||||
|  | ||||
| 	f1, err := os.Create(filePath1) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	size := rand.Int63n(1024 * 1024 * 128) | ||||
| 	buf := make([]byte, size) | ||||
| 	crand.Read(buf) | ||||
| 	if _, err := f1.Write(buf); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if err := f1.Close(); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	c1, c2 := net.Pipe() | ||||
| 	errChan := make(chan error) | ||||
|  | ||||
| 	go func() { | ||||
| 		err := Send(filePath1, c1, time.Second) | ||||
| 		if err != nil { | ||||
| 			log.Printf("Send error: %v", err) | ||||
| 		} | ||||
| 		errChan <- err | ||||
| 	}() | ||||
|  | ||||
| 	go func() { | ||||
| 		err := Recv(filePath2, c2, time.Second) | ||||
| 		if err != nil { | ||||
| 			log.Printf("Recv error: %v", err) | ||||
| 		} | ||||
| 		errChan <- err | ||||
| 	}() | ||||
|  | ||||
| 	if err := <-errChan; err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if err := <-errChan; err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	buf2, err := os.ReadFile(filePath2) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if !bytes.Equal(buf, buf2) { | ||||
| 		t.Fatal("Not equal.") | ||||
| 	} | ||||
| } | ||||
| */ | ||||
		Reference in New Issue
	
	Block a user