perobs 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/perobs.rb +1 -0
- data/lib/perobs/Array.rb +66 -19
- data/lib/perobs/BTree.rb +83 -12
- data/lib/perobs/BTreeBlob.rb +1 -1
- data/lib/perobs/BTreeDB.rb +2 -2
- data/lib/perobs/BTreeNode.rb +365 -85
- data/lib/perobs/BigArray.rb +267 -0
- data/lib/perobs/BigArrayNode.rb +998 -0
- data/lib/perobs/BigHash.rb +262 -0
- data/lib/perobs/BigTree.rb +184 -0
- data/lib/perobs/BigTreeNode.rb +873 -0
- data/lib/perobs/ConsoleProgressMeter.rb +61 -0
- data/lib/perobs/DataBase.rb +4 -3
- data/lib/perobs/DynamoDB.rb +57 -15
- data/lib/perobs/EquiBlobsFile.rb +143 -51
- data/lib/perobs/FNV_Hash_1a_64.rb +54 -0
- data/lib/perobs/FlatFile.rb +363 -203
- data/lib/perobs/FlatFileBlobHeader.rb +98 -54
- data/lib/perobs/FlatFileDB.rb +42 -20
- data/lib/perobs/Hash.rb +58 -13
- data/lib/perobs/IDList.rb +144 -0
- data/lib/perobs/IDListPage.rb +107 -0
- data/lib/perobs/IDListPageFile.rb +180 -0
- data/lib/perobs/IDListPageRecord.rb +142 -0
- data/lib/perobs/Object.rb +18 -15
- data/lib/perobs/ObjectBase.rb +38 -4
- data/lib/perobs/PersistentObjectCache.rb +53 -67
- data/lib/perobs/PersistentObjectCacheLine.rb +24 -12
- data/lib/perobs/ProgressMeter.rb +97 -0
- data/lib/perobs/SpaceTree.rb +21 -12
- data/lib/perobs/SpaceTreeNode.rb +53 -61
- data/lib/perobs/Store.rb +71 -32
- data/lib/perobs/version.rb +1 -1
- data/perobs.gemspec +4 -4
- data/test/Array_spec.rb +15 -6
- data/test/BTree_spec.rb +5 -2
- data/test/BigArray_spec.rb +214 -0
- data/test/BigHash_spec.rb +144 -0
- data/test/BigTreeNode_spec.rb +153 -0
- data/test/BigTree_spec.rb +259 -0
- data/test/EquiBlobsFile_spec.rb +105 -1
- data/test/FNV_Hash_1a_64_spec.rb +59 -0
- data/test/FlatFileDB_spec.rb +63 -14
- data/test/Hash_spec.rb +1 -2
- data/test/IDList_spec.rb +77 -0
- data/test/LegacyDBs/LegacyDB.rb +151 -0
- data/test/LegacyDBs/version_3/class_map.json +1 -0
- data/test/LegacyDBs/version_3/config.json +1 -0
- data/test/LegacyDBs/version_3/database.blobs +0 -0
- data/test/LegacyDBs/version_3/database_spaces.blobs +0 -0
- data/test/LegacyDBs/version_3/index.blobs +0 -0
- data/test/LegacyDBs/version_3/version +1 -0
- data/test/LockFile_spec.rb +9 -6
- data/test/SpaceTree_spec.rb +4 -1
- data/test/Store_spec.rb +290 -199
- data/test/spec_helper.rb +9 -4
- metadata +47 -10
- data/lib/perobs/TreeDB.rb +0 -277
@@ -0,0 +1,144 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = IDList.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2018 by Chris Schlaeger <chris@taskjuggler.org>
|
6
|
+
#
|
7
|
+
# MIT License
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
+
# a copy of this software and associated documentation files (the
|
11
|
+
# "Software"), to deal in the Software without restriction, including
|
12
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
+
# the following conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
|
28
|
+
require 'perobs/IDListPageFile'
|
29
|
+
require 'perobs/IDListPageRecord'
|
30
|
+
|
31
|
+
module PEROBS
|
32
|
+
|
33
|
+
# This class stores a list of 64 bit values. Values can be added to the list
|
34
|
+
# and the presence of a certain value can be checked. It can hold up to 2^64
|
35
|
+
# values. It tries to keep values in memory but can store them in a file if
|
36
|
+
# needed. A threshold for the in-memory values can be set in the
|
37
|
+
# constructor. The stored values are grouped in pages. Each page can hold up
|
38
|
+
# to page_size entries.
|
39
|
+
class IDList
|
40
|
+
|
41
|
+
# Create a new IDList object. The data that can't be kept in memory will
|
42
|
+
# be stored in the specified directory under the given name.
|
43
|
+
# @param dir [String] Path of the directory
|
44
|
+
# @param name [String] Name of the file
|
45
|
+
# @param max_in_memory [Integer] Specifies the maximum number of values
|
46
|
+
# that will be kept in memory. If the list is larger, values will
|
47
|
+
# be cached in the specified file.
|
48
|
+
# @param page_size [Integer] The number of values per page. The default
|
49
|
+
# value is 32 which was found the best performing config in tests.
|
50
|
+
def initialize(dir, name, max_in_memory, page_size = 32)
|
51
|
+
# The page_file manages the pages that store the values.
|
52
|
+
@page_file = IDListPageFile.new(self, dir, name,
|
53
|
+
max_in_memory, page_size)
|
54
|
+
clear
|
55
|
+
end
|
56
|
+
|
57
|
+
# Insert a new value into the list.
|
58
|
+
# @param id [Integer] The value to add
|
59
|
+
def insert(id)
|
60
|
+
# Find the index of the page that should hold ID.
|
61
|
+
index = @page_records.bsearch_index { |pr| pr.max_id >= id }
|
62
|
+
# Get the corresponding IDListPageRecord object.
|
63
|
+
page = @page_records[index]
|
64
|
+
|
65
|
+
# In case the page is already full we'll have to create a new page.
|
66
|
+
# There is no guarantee that a split will yield an page with space as we
|
67
|
+
# split by ID range, not by distributing the values evenly across the
|
68
|
+
# two pages.
|
69
|
+
while page.is_full?
|
70
|
+
new_page = page.split
|
71
|
+
# Store the newly created page into the page_records list.
|
72
|
+
@page_records.insert(index + 1, new_page)
|
73
|
+
if id >= new_page.min_id
|
74
|
+
# We need to insert the ID into the newly created page. Adjust index
|
75
|
+
# and page reference accordingly.
|
76
|
+
index += 1
|
77
|
+
page = new_page
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Insert the ID into the page.
|
82
|
+
page.insert(id)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Check if a given value is already stored in the list.
|
86
|
+
# @param id [Integer] The value to check for
|
87
|
+
def include?(id)
|
88
|
+
@page_records.bsearch { |pr| pr.max_id >= id }.include?(id)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Clear the list and empty the filesystem cache file.
|
92
|
+
def clear
|
93
|
+
@page_file.clear
|
94
|
+
@page_records = [ IDListPageRecord.new(@page_file, 0, 2 ** 64) ]
|
95
|
+
end
|
96
|
+
|
97
|
+
# Erase the list including the filesystem cache file. The IDList is no
|
98
|
+
# longer usable after this call but the cache file is removed from the
|
99
|
+
# filesystem.
|
100
|
+
def erase
|
101
|
+
@page_file.erase
|
102
|
+
@page_records = nil
|
103
|
+
end
|
104
|
+
|
105
|
+
# Perform some consistency checks on the internal data structures. Raises
|
106
|
+
# a RuntimeError in case a problem is found.
|
107
|
+
def check
|
108
|
+
last_max = -1
|
109
|
+
unless (min_id = @page_records.first.min_id) == 0
|
110
|
+
raise RuntimeError, "min_id of first record (#{min_id}) " +
|
111
|
+
"must be 0."
|
112
|
+
end
|
113
|
+
|
114
|
+
@page_records.each do |pr|
|
115
|
+
unless pr.min_id == last_max + 1
|
116
|
+
raise RuntimeError, "max_id of previous record (#{last_max}) " +
|
117
|
+
"must be exactly 1 smaller than current record (#{pr.min_id})."
|
118
|
+
end
|
119
|
+
last_max = pr.max_id
|
120
|
+
pr.check
|
121
|
+
end
|
122
|
+
|
123
|
+
unless last_max == 2 ** 64
|
124
|
+
raise RuntimeError, "max_id of last records " +
|
125
|
+
"(#{@page_records.last.max_id}) must be #{2 ** 64})."
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def to_a
|
130
|
+
a = []
|
131
|
+
@page_records.each { |pr| a += pr.values }
|
132
|
+
a
|
133
|
+
end
|
134
|
+
|
135
|
+
# Print a human readable form of the tree that stores the list. This is
|
136
|
+
# only meant for debugging purposes and does not scale for larger trees.
|
137
|
+
def to_s
|
138
|
+
"\n" + @root.to_s
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = IDListPage.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2018 by Chris Schlaeger <chris@taskjuggler.org>
|
6
|
+
#
|
7
|
+
# MIT License
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
+
# a copy of this software and associated documentation files (the
|
11
|
+
# "Software"), to deal in the Software without restriction, including
|
12
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
+
# the following conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
|
28
|
+
module PEROBS
|
29
|
+
|
30
|
+
class IDListPage
|
31
|
+
|
32
|
+
attr_reader :uid, :values
|
33
|
+
attr_accessor :record
|
34
|
+
|
35
|
+
def initialize(page_file, record, uid, values = [])
|
36
|
+
@page_file = page_file
|
37
|
+
@record = record
|
38
|
+
@uid = uid
|
39
|
+
@values = values
|
40
|
+
@record.page_entries = @values.length
|
41
|
+
end
|
42
|
+
|
43
|
+
def IDListPage::load(page_file, uid, ref)
|
44
|
+
page_file.load(uid, ref)
|
45
|
+
end
|
46
|
+
|
47
|
+
def is_full?
|
48
|
+
@values.length >= @page_file.page_size
|
49
|
+
end
|
50
|
+
|
51
|
+
def length
|
52
|
+
@values.length
|
53
|
+
end
|
54
|
+
|
55
|
+
def save
|
56
|
+
@page_file.save_page(self)
|
57
|
+
end
|
58
|
+
|
59
|
+
def insert(id)
|
60
|
+
if is_full?
|
61
|
+
raise ArgumentError, "IDListPage is already full"
|
62
|
+
end
|
63
|
+
index = @values.bsearch_index { |v| v >= id } || @values.length
|
64
|
+
|
65
|
+
# If the value isn't stored already, insert it.
|
66
|
+
if @values[index] != id
|
67
|
+
@values.insert(index, id)
|
68
|
+
@record.page_entries = @values.length
|
69
|
+
@page_file.mark_page_as_modified(self)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def include?(id)
|
74
|
+
!(v = @values.bsearch { |v| v >= id }).nil? && v == id
|
75
|
+
end
|
76
|
+
|
77
|
+
def delete(max_id)
|
78
|
+
a = []
|
79
|
+
@values.delete_if { |v| v > max_id ? a << v : false }
|
80
|
+
|
81
|
+
unless a.empty?
|
82
|
+
@record.page_entries = @values.length
|
83
|
+
@page_file.mark_page_as_modified(self)
|
84
|
+
end
|
85
|
+
|
86
|
+
a
|
87
|
+
end
|
88
|
+
|
89
|
+
def check
|
90
|
+
last_value = nil
|
91
|
+
@values.each_with_index do |v, i|
|
92
|
+
if last_value && last_value >= v
|
93
|
+
raise RuntimeError, "The values #{last_value} and #{v} must be " +
|
94
|
+
"strictly ascending: #{@values.inspect}"
|
95
|
+
end
|
96
|
+
last_value = v
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_s
|
101
|
+
"[ #{@values.join(', ')} ]"
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = IDListPageFile.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2018 by Chris Schlaeger <chris@taskjuggler.org>
|
6
|
+
#
|
7
|
+
# MIT License
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
+
# a copy of this software and associated documentation files (the
|
11
|
+
# "Software"), to deal in the Software without restriction, including
|
12
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
+
# the following conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
|
28
|
+
require 'perobs/IDListPage'
|
29
|
+
require 'perobs/IDListPageRecord'
|
30
|
+
require 'perobs/Log'
|
31
|
+
require 'perobs/PersistentObjectCache'
|
32
|
+
|
33
|
+
module PEROBS
|
34
|
+
|
35
|
+
# The IDListPageFile class provides filesystem based cache for the
|
36
|
+
# IDListPage objects. The IDListRecord objects only hold the index of the
|
37
|
+
# page in this cache. This allows the pages to be garbage collected and
|
38
|
+
# swapped to the file. If accessed, the pages will be swaped in again. While
|
39
|
+
# this process is similar to the demand paging of the OS it has absolutely
|
40
|
+
# nothing to do with it.
|
41
|
+
class IDListPageFile
|
42
|
+
|
43
|
+
attr_reader :page_size, :pages
|
44
|
+
|
45
|
+
# Create a new IDListPageFile object that uses the given file in the given
|
46
|
+
# directory as cache file.
|
47
|
+
# @param list [IDList] The IDList object that caches pages here
|
48
|
+
# @param dir [String] An existing directory
|
49
|
+
# @param name [String] A file name (without path)
|
50
|
+
# @param max_in_memory [Integer] Maximum number of pages to keep in memory
|
51
|
+
# @param page_size [Integer] The number of values in each page
|
52
|
+
def initialize(list, dir, name, max_in_memory, page_size)
|
53
|
+
@list = list
|
54
|
+
@file_name = File.join(dir, name + '.cache')
|
55
|
+
@page_size = page_size
|
56
|
+
open
|
57
|
+
@pages = PersistentObjectCache.new(max_in_memory, max_in_memory / 2,
|
58
|
+
IDListPage, self)
|
59
|
+
@page_counter = 0
|
60
|
+
end
|
61
|
+
|
62
|
+
# Load the IDListPage from the cache file.
|
63
|
+
# @param page_idx [Integer] The page index in the page file
|
64
|
+
# @param record [IDListPageRecord] the corresponding IDListPageRecord
|
65
|
+
# @return [IDListPage] The loaded values
|
66
|
+
def load(page_idx, record)
|
67
|
+
# The IDListPageRecord will tell us the actual number of values stored
|
68
|
+
# in this page.
|
69
|
+
values = []
|
70
|
+
unless (entries = record.page_entries) == 0
|
71
|
+
begin
|
72
|
+
@f.seek(page_idx * @page_size * 8)
|
73
|
+
values = @f.read(entries * 8).unpack("Q#{entries}")
|
74
|
+
rescue IOError => e
|
75
|
+
PEROBS.log.fatal "Cannot read cache file #{@file_name}: #{e.message}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Create the IDListPage object with the given values.
|
80
|
+
p = IDListPage.new(self, record, page_idx, values)
|
81
|
+
@pages.insert(p, false)
|
82
|
+
|
83
|
+
p
|
84
|
+
end
|
85
|
+
|
86
|
+
# Return the number of registered pages.
|
87
|
+
def page_count
|
88
|
+
@page_counter
|
89
|
+
end
|
90
|
+
|
91
|
+
# Create a new IDListPage and register it.
|
92
|
+
# @param record [IDListPageRecord] The corresponding record.
|
93
|
+
# @param values [Array of Integer] The values stored in the page
|
94
|
+
# @return [IDListPage]
|
95
|
+
def new_page(record, values = [])
|
96
|
+
idx = @page_counter
|
97
|
+
@page_counter += 1
|
98
|
+
mark_page_as_modified(IDListPage.new(self, record, idx, values))
|
99
|
+
idx
|
100
|
+
end
|
101
|
+
|
102
|
+
# Return the IDListPage object with the given index.
|
103
|
+
# @param record [IDListPageRecord] the corresponding IDListPageRecord
|
104
|
+
# @return [IDListPage] The page corresponding to the index.
|
105
|
+
def page(record)
|
106
|
+
p = @pages.get(record.page_idx, record) || load(record.page_idx, record)
|
107
|
+
unless p.uid == record.page_idx
|
108
|
+
raise RuntimeError, "Page reference mismatch. Record " +
|
109
|
+
"#{record.page_idx} points to page #{p.uid}"
|
110
|
+
end
|
111
|
+
|
112
|
+
p
|
113
|
+
end
|
114
|
+
|
115
|
+
# Mark a page as modified. This means it has to be written into the cache
|
116
|
+
# before it is removed from memory.
|
117
|
+
# @param p [IDListPage] page reference
|
118
|
+
def mark_page_as_modified(p)
|
119
|
+
@pages.insert(p)
|
120
|
+
@pages.flush
|
121
|
+
end
|
122
|
+
|
123
|
+
# Clear all pages, erase the cache and re-open it again.
|
124
|
+
def clear
|
125
|
+
@pages.clear
|
126
|
+
@page_counter = 0
|
127
|
+
begin
|
128
|
+
@f.truncate(0)
|
129
|
+
rescue IOError => e
|
130
|
+
raise RuntimeError, "Cannote truncate cache file #{@file_name}: " +
|
131
|
+
e.message
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Discard all pages and erase the cache file.
|
136
|
+
def erase
|
137
|
+
@pages.clear
|
138
|
+
@page_counter = 0
|
139
|
+
close
|
140
|
+
end
|
141
|
+
|
142
|
+
# Save the given IDListPage into the cache file.
|
143
|
+
# @param p [IDListPage] page to store
|
144
|
+
def save_page(p)
|
145
|
+
if p.record.page_entries != p.values.length
|
146
|
+
raise RuntimeError, "page_entries mismatch for node #{p.uid}"
|
147
|
+
end
|
148
|
+
begin
|
149
|
+
@f.seek(p.uid * @page_size * 8)
|
150
|
+
@f.write(p.values.pack('Q*'))
|
151
|
+
rescue IOError => e
|
152
|
+
PEROBS.log.fatal "Cannot write cache file #{@file_name}: #{e.message}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def open
|
159
|
+
begin
|
160
|
+
# Create a new file by writing a new header.
|
161
|
+
@f = File.open(@file_name, 'wb+')
|
162
|
+
rescue IOError => e
|
163
|
+
PEROBS.log.fatal "Cannot open cache file #{@file_name}: #{e.message}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def close
|
168
|
+
begin
|
169
|
+
@f.close
|
170
|
+
File.delete(@file_name) if File.exist?(@file_name)
|
171
|
+
rescue IOError => e
|
172
|
+
PEROBS.log.fatal "Cannot erase cache file #{@file_name}: #{e.message}"
|
173
|
+
end
|
174
|
+
@f = nil
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = IDListPageRecord.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2018 by Chris Schlaeger <chris@taskjuggler.org>
|
6
|
+
#
|
7
|
+
# MIT License
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
+
# a copy of this software and associated documentation files (the
|
11
|
+
# "Software"), to deal in the Software without restriction, including
|
12
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
+
# the following conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
|
28
|
+
module PEROBS
|
29
|
+
|
30
|
+
# The IDListPageRecord class models the elements of the IDList. Each page
|
31
|
+
# holds up to a certain number of IDs that can be cached into a file if
|
32
|
+
# needed. Each page holds IDs within a given interval. The cache is managed
|
33
|
+
# by the IDListPageFile object.
|
34
|
+
class IDListPageRecord
|
35
|
+
|
36
|
+
attr_reader :min_id, :max_id, :page_idx
|
37
|
+
attr_accessor :page_entries
|
38
|
+
|
39
|
+
# Create a new IDListPageRecord object.
|
40
|
+
# @param page_file [IDListPageFile] The page file that manages the cache.
|
41
|
+
# @param min_id [Integer] The smallest ID that can be stored in this page
|
42
|
+
# @param max_id [Integer] the largest ID that can be stored in this page
|
43
|
+
# @param values [Array] An array of IDs to be stored in this page
|
44
|
+
def initialize(page_file, min_id, max_id, values = [])
|
45
|
+
@page_file = page_file
|
46
|
+
@min_id = min_id
|
47
|
+
@max_id = max_id
|
48
|
+
@page_entries = 0
|
49
|
+
@page_idx = @page_file.new_page(self, values)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Check if the given ID is included in this page.
|
53
|
+
# @param id [Integer]
|
54
|
+
# @return [True of False] Return true if found, false otherwise.
|
55
|
+
def include?(id)
|
56
|
+
return false if id < @min_id || @max_id < id
|
57
|
+
|
58
|
+
page.include?(id)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Check if the page is full and can't store any more IDs.
|
62
|
+
# @return [True or False]
|
63
|
+
def is_full?
|
64
|
+
page.is_full?
|
65
|
+
end
|
66
|
+
|
67
|
+
# Insert an ID into the page.
|
68
|
+
# @param ID [Integer] The ID to store
|
69
|
+
def insert(id)
|
70
|
+
unless @min_id <= id && id <= @max_id
|
71
|
+
raise ArgumentError, "IDs for this page must be between #{@min_id} " +
|
72
|
+
"and #{@max_id}. #{id} is outside this range."
|
73
|
+
end
|
74
|
+
|
75
|
+
page.insert(id)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Split the current page. This split is done by splitting the ID range in
|
79
|
+
# half. This page will keep the first half, the newly created page will
|
80
|
+
# get the second half. This may not actually yield an empty page as all
|
81
|
+
# values could remain with one of the pages. In this case further splits
|
82
|
+
# need to be issued by the caller.
|
83
|
+
# @return [IDListPageRecord] A new IDListPageRecord object.
|
84
|
+
def split
|
85
|
+
# Determine the new max_id for the old page.
|
86
|
+
max_id = @min_id + (@max_id - @min_id) / 2
|
87
|
+
# Create a new page that stores the upper half of the ID range. Remove
|
88
|
+
# all IDs from this page that now belong into the new page and transfer
|
89
|
+
# them.
|
90
|
+
new_page_record = IDListPageRecord.new(@page_file, max_id + 1, @max_id,
|
91
|
+
page.delete(max_id))
|
92
|
+
# Adjust the max_id of the current page.
|
93
|
+
@max_id = max_id
|
94
|
+
|
95
|
+
new_page_record
|
96
|
+
end
|
97
|
+
|
98
|
+
def values
|
99
|
+
page.values
|
100
|
+
end
|
101
|
+
|
102
|
+
def <=>(pr)
|
103
|
+
@min_id <=> pr.min_id
|
104
|
+
end
|
105
|
+
|
106
|
+
def check
|
107
|
+
unless @min_id < @max_id
|
108
|
+
raise RuntimeError, "min_id must be smaller than max_id"
|
109
|
+
end
|
110
|
+
|
111
|
+
p = page
|
112
|
+
values = p.values
|
113
|
+
unless @page_entries == values.length
|
114
|
+
raise RuntimeError, "Mismatch between node page_entries " +
|
115
|
+
"(#{@page_entries}) and number of values (#{p.values.length})"
|
116
|
+
end
|
117
|
+
|
118
|
+
values.each do |v|
|
119
|
+
if v < @min_id
|
120
|
+
raise RuntimeError, "Page value #{v} is smaller than min_id " +
|
121
|
+
"#{@min_id}"
|
122
|
+
end
|
123
|
+
if v > @max_id
|
124
|
+
raise RuntimeError, "Page value #{v} is larger than max_id #{@max_id}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
p.check
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def page
|
134
|
+
# The leaf pages reference the IDListPage objects only by their index.
|
135
|
+
# This method will convert the index into a reference to the actual
|
136
|
+
# object. These references should be very short-lived as a life
|
137
|
+
# reference prevents the page object from being collected.
|
138
|
+
@page_file.page(self)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|