perobs 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +113 -0
- data/Rakefile +22 -0
- data/lib/perobs/Array.rb +173 -0
- data/lib/perobs/BlockDB.rb +242 -0
- data/lib/perobs/Cache.rb +201 -0
- data/lib/perobs/DataBase.rb +115 -0
- data/lib/perobs/FileSystemDB.rb +171 -0
- data/lib/perobs/Hash.rb +175 -0
- data/lib/perobs/HashedBlocksDB.rb +153 -0
- data/lib/perobs/Object.rb +189 -0
- data/lib/perobs/ObjectBase.rb +159 -0
- data/lib/perobs/Store.rb +290 -0
- data/lib/perobs/version.rb +4 -0
- data/lib/perobs.rb +29 -0
- data/perobs.gemspec +23 -0
- data/spec/Array_spec.rb +94 -0
- data/spec/FileSystemDB_spec.rb +107 -0
- data/spec/Hash_spec.rb +96 -0
- data/spec/Object_spec.rb +108 -0
- data/spec/Store_spec.rb +412 -0
- data/spec/perobs_spec.rb +155 -0
- data/tasks/changelog.rake +169 -0
- data/tasks/gem.rake +50 -0
- data/tasks/rdoc.rake +14 -0
- data/tasks/test.rake +7 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9dd54b9f62dc6b5cc7129d25b9ba87e2d7aa3775
|
4
|
+
data.tar.gz: 1431e7ec23c7bf2c18fa65b7c3e14b33bc696b2b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dbf7166adf28acabef48594bb80721512f0b30156f66965e7077f6b4089e429c5d951b621c0c3322bc6c2a0b269e47042994dd9449d75cb00858e0d2a23bbbbc
|
7
|
+
data.tar.gz: 73ae5cbfd48a5bc3a53194961398ec6a0ff57a4ac4ed606ba0e3ab1922fcba89cbc72cb90327e2256e2c9709a2a2707bdea8a8bd9124ff973531b800ffc2f304
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Chris Schlaeger
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
# PEROBS - PErsistent Ruby OBject Store
|
2
|
+
|
3
|
+
PEROBS is a library that provides a persistent object store for Ruby
|
4
|
+
objects. Objects of your classes can be made persistent by deriving
|
5
|
+
them from PEROBS::Object. They will be in memory when needed and
|
6
|
+
transparently stored into a persistent storage. Currently only
|
7
|
+
filesystem based storage is supported, but back-ends for key/value
|
8
|
+
databases can be easily added.
|
9
|
+
|
10
|
+
This library is ideal for Ruby applications that work on huge, mostly
|
11
|
+
constant data sets and usually handle a small subset of the data at a
|
12
|
+
time. To ensure data consistency of a larger data set, you can use
|
13
|
+
transactions to make modifications of multiple objects atomic.
|
14
|
+
Transactions can be nested and are aborted when an exception is
|
15
|
+
raised.
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
It features a garbage collector that removes all objects that are no
|
20
|
+
longer in use. A build-in cache keeps access latencies to recently
|
21
|
+
used objects low and lazily flushes modified objects into the
|
22
|
+
persistend back-end.
|
23
|
+
|
24
|
+
Persistent objects must be created by deriving your class from
|
25
|
+
PEROBS::Object. Only instance variables that are declared via
|
26
|
+
po_attr will be persistent. All objects that are stored in persitant
|
27
|
+
instance variables must provide a to_json method that generates JSON
|
28
|
+
syntax that can be parsed into their original object again. It is
|
29
|
+
recommended that references to other objects are all going to persistent
|
30
|
+
objects again.
|
31
|
+
|
32
|
+
There are currently 3 kinds of persistent objects available:
|
33
|
+
|
34
|
+
* PEROBS::Object is the base class for all your classes that should be
|
35
|
+
persistent.
|
36
|
+
|
37
|
+
* PEROBS::Array provides an interface similar to the built-in Array class
|
38
|
+
but its objects are automatically stored.
|
39
|
+
|
40
|
+
* PEROBS::Hash provides an interface similar to the built-in Hash
|
41
|
+
class but its objects are automatically stored.
|
42
|
+
|
43
|
+
In addition to these classes, you also need to create a PEROBS::Store
|
44
|
+
object that owns your persistent objects. The store provides the
|
45
|
+
persistent database. If you are using the default serializer (JSON),
|
46
|
+
you can only use the subset of Ruby types that JSON supports.
|
47
|
+
Alternatively, you can use Marshal or YAML which support almost every
|
48
|
+
Ruby data type.
|
49
|
+
|
50
|
+
Here is an example how to use PEROBS. Let's define a class that models
|
51
|
+
a person with their family relations.
|
52
|
+
|
53
|
+
```
|
54
|
+
require 'perobs'
|
55
|
+
|
56
|
+
class Person < PEROBS::Object
|
57
|
+
|
58
|
+
po_attr :name, :mother, :father, :kids
|
59
|
+
|
60
|
+
def initialize(store, name)
|
61
|
+
super
|
62
|
+
attr_init(:name, name)
|
63
|
+
attr_init(:kids, PEROBS::Array.new)
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s
|
67
|
+
"#{@name} is the child of #{self.mother ? self.mother.name : 'unknown'} " +
|
68
|
+
"and #{self.father ? self.father.name : 'unknown'}.
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
store = PEROBS::Store.new('family')
|
74
|
+
store['grandpa'] = joe = Person.new('Joe')
|
75
|
+
store['grandma'] = jane = Person.new('Jane')
|
76
|
+
jim = Person.new('Jim')
|
77
|
+
jim.father = joe
|
78
|
+
joe.kids << jim
|
79
|
+
jim.mother = jane
|
80
|
+
jane.kids << jim
|
81
|
+
store.sync
|
82
|
+
```
|
83
|
+
|
84
|
+
When you run this script, a folder named 'family' will be created. It
|
85
|
+
contains the 3 Person objects.
|
86
|
+
|
87
|
+
## Installation
|
88
|
+
|
89
|
+
Add this line to your application's Gemfile:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
gem 'perobs'
|
93
|
+
```
|
94
|
+
|
95
|
+
And then execute:
|
96
|
+
|
97
|
+
$ bundle
|
98
|
+
|
99
|
+
Or install it yourself as:
|
100
|
+
|
101
|
+
$ gem install perobs
|
102
|
+
|
103
|
+
## Usage
|
104
|
+
|
105
|
+
TODO: Write usage instructions here
|
106
|
+
|
107
|
+
## Contributing
|
108
|
+
|
109
|
+
1. Fork it ( https://github.com/scrapper/perobs/fork )
|
110
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
111
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
112
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
113
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# Add the lib directory to the search path if it isn't included already
|
2
|
+
# lib = File.expand_path('../lib', __FILE__)
|
3
|
+
# $:.unshift lib unless $:.include?(lib)
|
4
|
+
|
5
|
+
require "bundler/gem_tasks"
|
6
|
+
require "rspec/core/rake_task"
|
7
|
+
require 'rake/clean'
|
8
|
+
require 'yard'
|
9
|
+
YARD::Rake::YardocTask.new
|
10
|
+
|
11
|
+
Dir.glob( 'tasks/*.rake').each do |fn|
|
12
|
+
begin
|
13
|
+
load fn;
|
14
|
+
rescue LoadError
|
15
|
+
puts "#{fn.split('/')[1]} tasks unavailable: #{$!}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
task :default => :spec
|
20
|
+
task :test => :spec
|
21
|
+
|
22
|
+
desc 'Run all unit and spec tests'
|
data/lib/perobs/Array.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = Array.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2015 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/ObjectBase'
|
29
|
+
|
30
|
+
module PEROBS
|
31
|
+
|
32
|
+
# An Array that is transparently persisted onto the back-end storage. It is
|
33
|
+
# very similar to the Ruby built-in Array class but has some additional
|
34
|
+
# limitations. The hash key must always be a String.
|
35
|
+
class Array < ObjectBase
|
36
|
+
|
37
|
+
# Create a new PersistentArray object.
|
38
|
+
# @param store [Store] The Store this hash is stored in
|
39
|
+
# @param size [Fixnum] The requested size of the Array
|
40
|
+
# @param default [Any] The default value that is returned when no value is
|
41
|
+
# stored for a specific key.
|
42
|
+
def initialize(store, size = 0, default = nil)
|
43
|
+
super(store)
|
44
|
+
@data = ::Array.new(size, default)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Equivalent to Array::[]
|
48
|
+
def [](index)
|
49
|
+
_dereferenced(@data[index])
|
50
|
+
end
|
51
|
+
|
52
|
+
# Equivalent to Array::[]=
|
53
|
+
def []=(index, obj)
|
54
|
+
@data[index] = _referenced(obj)
|
55
|
+
@store.cache.cache_write(self)
|
56
|
+
|
57
|
+
obj
|
58
|
+
end
|
59
|
+
|
60
|
+
# Equivalent to Array::<<
|
61
|
+
def <<(obj)
|
62
|
+
@store.cache.cache_write(self)
|
63
|
+
@data << _referenced(obj)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Equivalent to Array::+
|
67
|
+
def +(ary)
|
68
|
+
@store.cache.cache_write(self)
|
69
|
+
@data + ary
|
70
|
+
end
|
71
|
+
|
72
|
+
# Equivalent to Array::push
|
73
|
+
def push(obj)
|
74
|
+
@store.cache.cache_write(self)
|
75
|
+
@data.push(_referenced(obj))
|
76
|
+
end
|
77
|
+
|
78
|
+
# Equivalent to Array::pop
|
79
|
+
def pop
|
80
|
+
@store.cache.cache_write(self)
|
81
|
+
_dereferenced(@data.pop)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Equivalent to Array::clear
|
85
|
+
def clear
|
86
|
+
@store.cache.cache_write(self)
|
87
|
+
@data.clear
|
88
|
+
end
|
89
|
+
|
90
|
+
# Equivalent to Array::delete
|
91
|
+
def delete(obj)
|
92
|
+
@store.cache.cache_write(self)
|
93
|
+
@data.delete { |v| _dereferenced(v) == obj }
|
94
|
+
end
|
95
|
+
|
96
|
+
# Equivalent to Array::delete_at
|
97
|
+
def delete_at(index)
|
98
|
+
@store.cache.cache_write(self)
|
99
|
+
@data.delete_at(index)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Equivalent to Array::delete_if
|
103
|
+
def delete_if
|
104
|
+
@data.delete_if do |item|
|
105
|
+
yield(_dereferenced(item))
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Equivalent to Array::each
|
110
|
+
def each
|
111
|
+
@data.each do |item|
|
112
|
+
yield(_dereferenced(item))
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Equivalent to Array::empty?
|
117
|
+
def empty?
|
118
|
+
@data.empty?
|
119
|
+
end
|
120
|
+
|
121
|
+
# Equivalent to Array::include?
|
122
|
+
def include?(obj)
|
123
|
+
@data.each { |v| return true if _dereferenced(v) == obj }
|
124
|
+
|
125
|
+
false
|
126
|
+
end
|
127
|
+
|
128
|
+
# Equivalent to Array::length
|
129
|
+
def length
|
130
|
+
@data.length
|
131
|
+
end
|
132
|
+
alias size length
|
133
|
+
|
134
|
+
# Equivalent to Array::map
|
135
|
+
def map
|
136
|
+
@data.map do |item|
|
137
|
+
yield(_dereferenced(item))
|
138
|
+
end
|
139
|
+
end
|
140
|
+
alias collect map
|
141
|
+
|
142
|
+
# Return a list of all object IDs of all persistend objects that this Array
|
143
|
+
# is referencing.
|
144
|
+
# @return [Array of Fixnum or Bignum] IDs of referenced objects
|
145
|
+
def _referenced_object_ids
|
146
|
+
@data.each.select { |v| v && v.is_a?(POReference) }.map { |o| o.id }
|
147
|
+
end
|
148
|
+
|
149
|
+
# This method should only be used during store repair operations. It will
|
150
|
+
# delete all referenced to the given object ID.
|
151
|
+
# @param id [Fixnum/Bignum] targeted object ID
|
152
|
+
def _delete_reference_to_id(id)
|
153
|
+
@data.delete_if { |v| v && v.is_a?(POReference) && v.id == id }
|
154
|
+
end
|
155
|
+
|
156
|
+
# Restore the persistent data from a single data structure.
|
157
|
+
# This is a library internal method. Do not use outside of this library.
|
158
|
+
# @param data [Array] the actual Array object
|
159
|
+
# @private
|
160
|
+
def _deserialize(data)
|
161
|
+
@data = data
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
def _serialize
|
167
|
+
@data
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
|
@@ -0,0 +1,242 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = BlockDB.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2015 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 'json'
|
29
|
+
require 'json/add/core'
|
30
|
+
require 'json/add/struct'
|
31
|
+
|
32
|
+
module PEROBS
|
33
|
+
|
34
|
+
# This class manages the usage of the data blocks in the corresponding
|
35
|
+
# HashedBlocks object.
|
36
|
+
class BlockDB
|
37
|
+
|
38
|
+
# Create a new BlockDB object.
|
39
|
+
def initialize(dir, block_size)
|
40
|
+
@dir = dir
|
41
|
+
@block_size = block_size
|
42
|
+
|
43
|
+
@index_file_name = File.join(dir, 'index.json')
|
44
|
+
@block_file_name = File.join(dir, 'data')
|
45
|
+
read_index
|
46
|
+
end
|
47
|
+
|
48
|
+
# Write the given bytes with the given ID into the DB.
|
49
|
+
# @param id [Fixnum or Bignum] ID
|
50
|
+
# @param raw [String] sequence of bytes
|
51
|
+
def write_object(id, raw)
|
52
|
+
bytes = raw.bytesize
|
53
|
+
start_address = reserve_blocks(id, bytes)
|
54
|
+
if write_to_block_file(raw, start_address) != bytes
|
55
|
+
raise RuntimeError, 'Object length does not match written bytes'
|
56
|
+
end
|
57
|
+
write_index
|
58
|
+
end
|
59
|
+
|
60
|
+
# Read the entry for the given ID and return it as bytes.
|
61
|
+
# @param id [Fixnum or Bignum] ID
|
62
|
+
# @return [String] sequence of bytes
|
63
|
+
def read_object(id)
|
64
|
+
read_from_block_file(*find(id))
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# Find the data for the object with given id.
|
69
|
+
# @param id [Fixnum or Bignum] Object ID
|
70
|
+
# @return [Array] Returns an Array with two Fixnum entries. The first is
|
71
|
+
# the number of bytes and the second is the starting offset in the
|
72
|
+
# block storage file.
|
73
|
+
def find(id)
|
74
|
+
@entries.each do |entry|
|
75
|
+
if entry['id'] == id
|
76
|
+
return [ entry['bytes'], entry['first_block'] * @block_size ]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
|
83
|
+
# Write a string of bytes into the file at the given address.
|
84
|
+
# @param raw [String] bytes to write
|
85
|
+
# @param address [Fixnum] offset in the file
|
86
|
+
# @return [Fixnum] number of bytes written
|
87
|
+
def write_to_block_file(raw, address)
|
88
|
+
begin
|
89
|
+
File.write(@block_file_name, raw, address)
|
90
|
+
rescue => e
|
91
|
+
raise IOError,
|
92
|
+
"Cannot write block file #{@block_file_name}: #{e.message}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Read _bytes_ bytes from the file starting at offset _address_.
|
97
|
+
# @param bytes [Fixnum] number of bytes to read
|
98
|
+
# @param address [Fixnum] offset in the file
|
99
|
+
def read_from_block_file(bytes, address)
|
100
|
+
begin
|
101
|
+
File.read(@block_file_name, bytes, address)
|
102
|
+
rescue => e
|
103
|
+
raise IOError,
|
104
|
+
"Cannot read block file #{@block_file_name}: #{e.message}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Clear the mark on all entries in the index.
|
109
|
+
def clear_marks
|
110
|
+
@entries.each { |e| e['marked'] = false}
|
111
|
+
write_index
|
112
|
+
end
|
113
|
+
|
114
|
+
# Set a mark on the entry with the given ID.
|
115
|
+
# @param id [Fixnum or Bignum] ID of the entry
|
116
|
+
def mark(id)
|
117
|
+
found = false
|
118
|
+
@entries.each do |entry|
|
119
|
+
if entry['id'] == id
|
120
|
+
entry['marked'] = true
|
121
|
+
found = true
|
122
|
+
break
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
unless found
|
127
|
+
raise ArgumentError, "Cannot find an entry for ID #{id} to mark"
|
128
|
+
end
|
129
|
+
|
130
|
+
write_index
|
131
|
+
end
|
132
|
+
|
133
|
+
# Check if the entry for a given ID is marked.
|
134
|
+
# @param id [Fixnum or Bignum] ID of the entry
|
135
|
+
# @return [TrueClass or FalseClass] true if marked, false otherwise
|
136
|
+
def is_marked?(id)
|
137
|
+
@entries.each do |entry|
|
138
|
+
return entry['marked'] if entry['id'] == id
|
139
|
+
end
|
140
|
+
|
141
|
+
raise ArgumentError, "Cannot find an entry for ID #{id} to check"
|
142
|
+
end
|
143
|
+
|
144
|
+
# Remove all entries from the index that have not been marked.
|
145
|
+
def delete_unmarked_entries
|
146
|
+
@entries.delete_if { |e| e['marked'] == false }
|
147
|
+
write_index
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
# Reserve the blocks needed for the specified number of bytes with the
|
153
|
+
# given ID.
|
154
|
+
# @param id [Fixnum or Bignum] ID of the entry
|
155
|
+
# @param bytes [Fixnum] number of bytes for this entry
|
156
|
+
# @return [Fixnum] the start address of the reserved block
|
157
|
+
def reserve_blocks(id, bytes)
|
158
|
+
# size of the entry in blocks
|
159
|
+
blocks = size_in_blocks(bytes)
|
160
|
+
# index of first block after the last seen entry
|
161
|
+
end_of_last_entry = 0
|
162
|
+
# block index of best fit segment
|
163
|
+
best_fit_start = nil
|
164
|
+
# best fir segment size in blocks
|
165
|
+
best_fit_blocks = nil
|
166
|
+
# If there is already an entry for an object with the _id_, we mark it
|
167
|
+
# for deletion.
|
168
|
+
entry_to_delete = nil
|
169
|
+
|
170
|
+
@entries.each do |entry|
|
171
|
+
if entry['id'] == id
|
172
|
+
# We've found an old entry for this ID.
|
173
|
+
if entry['blocks'] >= blocks
|
174
|
+
# The old entry still fits. Let's just reuse it.
|
175
|
+
entry['bytes'] = bytes
|
176
|
+
entry['blocks'] = blocks
|
177
|
+
return entry['first_block'] * @block_size
|
178
|
+
end
|
179
|
+
# It does not fit. Ignore the entry and mark it for deletion.
|
180
|
+
entry_to_delete = entry
|
181
|
+
next
|
182
|
+
end
|
183
|
+
|
184
|
+
gap = entry['first_block'] - end_of_last_entry
|
185
|
+
if gap >= blocks &&
|
186
|
+
(best_fit_blocks.nil? || gap < best_fit_blocks)
|
187
|
+
# We've found a segment that fits the requested bytes and fits
|
188
|
+
# better than any previous find.
|
189
|
+
best_fit_start = end_of_last_entry
|
190
|
+
best_fit_blocks = gap
|
191
|
+
end
|
192
|
+
end_of_last_entry = entry['first_block'] + entry['blocks']
|
193
|
+
end
|
194
|
+
|
195
|
+
# Delete the old entry if requested.
|
196
|
+
@entries.delete(entry_to_delete) if entry_to_delete
|
197
|
+
|
198
|
+
# Create a new entry and insert it.
|
199
|
+
entry = {
|
200
|
+
'id' => id,
|
201
|
+
'bytes' => bytes,
|
202
|
+
'first_block' => best_fit_start || end_of_last_entry,
|
203
|
+
'blocks' => blocks,
|
204
|
+
'marked' => false
|
205
|
+
}
|
206
|
+
@entries << entry
|
207
|
+
@entries.sort! { |e1, e2| e1['first_block'] <=> e2['first_block'] }
|
208
|
+
|
209
|
+
entry['first_block'] * @block_size
|
210
|
+
end
|
211
|
+
|
212
|
+
def read_index
|
213
|
+
if File.exists?(@index_file_name)
|
214
|
+
begin
|
215
|
+
@entries = JSON.parse(File.read(@index_file_name))
|
216
|
+
rescue => e
|
217
|
+
raise RuntimeError,
|
218
|
+
"BlockDB file #{@index_file_name} corrupted: #{e.message}"
|
219
|
+
end
|
220
|
+
else
|
221
|
+
@entries = []
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def write_index
|
226
|
+
begin
|
227
|
+
File.write(@index_file_name, @entries.to_json)
|
228
|
+
rescue => e
|
229
|
+
raise RuntimeError,
|
230
|
+
"Cannot write BlockDB index file #{@index_file_name}: " +
|
231
|
+
e.message
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def size_in_blocks(bytes)
|
236
|
+
bytes / @block_size + (bytes % @block_size != 0 ? 1 : 0)
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
|