petra_core 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 +9 -0
- data/.rspec +3 -0
- data/.rubocop.yml +83 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +74 -0
- data/MIT-LICENSE +20 -0
- data/README.md +726 -0
- data/Rakefile +8 -0
- data/bin/console +8 -0
- data/bin/setup +8 -0
- data/examples/continuation_error.rb +125 -0
- data/examples/dining_philosophers.rb +138 -0
- data/examples/showcase.rb +54 -0
- data/lib/petra/components/entries/attribute_change.rb +29 -0
- data/lib/petra/components/entries/attribute_change_veto.rb +37 -0
- data/lib/petra/components/entries/attribute_read.rb +20 -0
- data/lib/petra/components/entries/object_destruction.rb +22 -0
- data/lib/petra/components/entries/object_initialization.rb +19 -0
- data/lib/petra/components/entries/object_persistence.rb +26 -0
- data/lib/petra/components/entries/read_integrity_override.rb +42 -0
- data/lib/petra/components/entry_set.rb +87 -0
- data/lib/petra/components/log_entry.rb +342 -0
- data/lib/petra/components/proxy_cache.rb +209 -0
- data/lib/petra/components/section.rb +543 -0
- data/lib/petra/components/transaction.rb +405 -0
- data/lib/petra/components/transaction_manager.rb +214 -0
- data/lib/petra/configuration/base.rb +132 -0
- data/lib/petra/configuration/class_configurator.rb +309 -0
- data/lib/petra/configuration/configurator.rb +67 -0
- data/lib/petra/core_ext.rb +27 -0
- data/lib/petra/exceptions.rb +181 -0
- data/lib/petra/persistence_adapters/adapter.rb +154 -0
- data/lib/petra/persistence_adapters/file_adapter.rb +239 -0
- data/lib/petra/proxies/abstract_proxy.rb +149 -0
- data/lib/petra/proxies/enumerable_proxy.rb +44 -0
- data/lib/petra/proxies/handlers/attribute_read_handler.rb +45 -0
- data/lib/petra/proxies/handlers/missing_method_handler.rb +47 -0
- data/lib/petra/proxies/method_handlers.rb +213 -0
- data/lib/petra/proxies/module_proxy.rb +12 -0
- data/lib/petra/proxies/object_proxy.rb +310 -0
- data/lib/petra/util/debug.rb +45 -0
- data/lib/petra/util/extended_attribute_accessors.rb +51 -0
- data/lib/petra/util/field_accessors.rb +35 -0
- data/lib/petra/util/registrable.rb +48 -0
- data/lib/petra/util/test_helpers.rb +9 -0
- data/lib/petra/version.rb +5 -0
- data/lib/petra.rb +100 -0
- data/lib/tasks/petra_tasks.rake +5 -0
- data/petra.gemspec +36 -0
- metadata +208 -0
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Petra
|
4
|
+
|
5
|
+
#----------------------------------------------------------------
|
6
|
+
# Exception Base classes
|
7
|
+
#----------------------------------------------------------------
|
8
|
+
|
9
|
+
# Generic error class wrapping all custom petra exceptions
|
10
|
+
class PetraError < StandardError
|
11
|
+
end
|
12
|
+
|
13
|
+
#
|
14
|
+
# Error class which accepts an options hash and sets its key/value pairs
|
15
|
+
# as instance variables. Inherited classes therefore only have to specify the
|
16
|
+
# corresponding attribute readers
|
17
|
+
#
|
18
|
+
class ExtendedError < PetraError
|
19
|
+
def initialize(**options)
|
20
|
+
options.each do |k, v|
|
21
|
+
instance_variable_set("@#{k}", v)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Used whenever a configuration value differs from what petra expects it to be
|
27
|
+
class ConfigurationError < PetraError
|
28
|
+
end
|
29
|
+
|
30
|
+
# Used for generic errors during transaction persistence
|
31
|
+
class PersistenceError < PetraError
|
32
|
+
end
|
33
|
+
|
34
|
+
# Thrown when a transaction should be persisted, but is locked by another instance
|
35
|
+
class TransactionLocked < PetraError
|
36
|
+
end
|
37
|
+
|
38
|
+
class HandlerException < ExtendedError
|
39
|
+
|
40
|
+
def retry!
|
41
|
+
fail Petra::Retry
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Resets the currently active transaction
|
46
|
+
# This will stop the transaction execution, so make sure that you wrap
|
47
|
+
# important code which has to be executed afterwards in an `ensure`
|
48
|
+
#
|
49
|
+
def reset_transaction!
|
50
|
+
@reset = true
|
51
|
+
Petra.transaction_manager.reset_transaction
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Requests a section rollback on the currently active transaction
|
56
|
+
# This will stop the transaction execution, so make sure that you wrap
|
57
|
+
# important code which has to be executed afterwards in an `ensure`
|
58
|
+
#
|
59
|
+
def rollback_transaction!
|
60
|
+
@rollback = true
|
61
|
+
Petra.transaction_manager.rollback_transaction
|
62
|
+
end
|
63
|
+
|
64
|
+
alias rollback! rollback_transaction!
|
65
|
+
alias reset! reset_transaction!
|
66
|
+
|
67
|
+
def continue!
|
68
|
+
fail Petra::ContinuationError, 'The transaction processing cannot be resumed.' unless continuable?
|
69
|
+
@continuation.call
|
70
|
+
end
|
71
|
+
|
72
|
+
def continuable?
|
73
|
+
false
|
74
|
+
end
|
75
|
+
|
76
|
+
protected
|
77
|
+
|
78
|
+
def continuation?
|
79
|
+
!!@continuation
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class ValueComparisonError < HandlerException
|
84
|
+
attr_reader :object # The affected proxy
|
85
|
+
attr_reader :attribute # The affected attribute
|
86
|
+
attr_reader :external_value # The new external attribute value
|
87
|
+
|
88
|
+
#
|
89
|
+
# Tells the current transaction to ignore further errors of this kind
|
90
|
+
# until the attribute value is changed again externally.
|
91
|
+
#
|
92
|
+
# @param [Boolean] update_value
|
93
|
+
# If set to +true+, the read set entry for this attribute is updated with the
|
94
|
+
# new external value. This means that the new value will be visible inside of
|
95
|
+
# the transaction until it changes again.
|
96
|
+
#
|
97
|
+
# Otherwise, the exception is completely ignored and will have no impact
|
98
|
+
# on the values displayed inside the transaction.
|
99
|
+
#
|
100
|
+
def ignore!(update_value: false)
|
101
|
+
Petra.current_transaction.current_section.log_read_integrity_override(object,
|
102
|
+
attribute: attribute,
|
103
|
+
external_value: external_value,
|
104
|
+
update_value: update_value)
|
105
|
+
end
|
106
|
+
|
107
|
+
def continuable?
|
108
|
+
!@reset && !@rollback
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Thrown when a read attribute changed its value externally
|
113
|
+
# If we read AND changed the attribute, a ReadWriteIntegrityError is raised instead
|
114
|
+
class ReadIntegrityError < ValueComparisonError
|
115
|
+
attr_reader :last_read_value # The value we last read for this attribute (before it was changed)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Thrown when an attribute that we previously read AND changed
|
119
|
+
# was also changed externally.
|
120
|
+
class WriteClashError < ValueComparisonError
|
121
|
+
attr_reader :our_value
|
122
|
+
|
123
|
+
#
|
124
|
+
# Tells the transaction to ignore all changes previously done to the current
|
125
|
+
# attribute in the transaction.
|
126
|
+
#
|
127
|
+
def undo_changes!
|
128
|
+
Petra.current_transaction.current_section.log_attribute_change_veto(object,
|
129
|
+
attribute: attribute,
|
130
|
+
external_value: external_value)
|
131
|
+
end
|
132
|
+
|
133
|
+
alias their_value external_value
|
134
|
+
alias use_ours! ignore!
|
135
|
+
alias use_theirs! undo_changes!
|
136
|
+
end
|
137
|
+
|
138
|
+
#----------------------------------------------------------------
|
139
|
+
# Transaction Flow Error Classes
|
140
|
+
#----------------------------------------------------------------
|
141
|
+
|
142
|
+
# Used internally when a lock could not be acquired (non-suspending locking)
|
143
|
+
class LockError < HandlerException
|
144
|
+
attr_reader :lock_type
|
145
|
+
attr_reader :lock_name
|
146
|
+
|
147
|
+
def initialize(lock_type: 'general', lock_name: 'general', processed: false)
|
148
|
+
@lock_type = lock_type
|
149
|
+
@lock_name = lock_name
|
150
|
+
@processed = processed
|
151
|
+
end
|
152
|
+
|
153
|
+
def processed?
|
154
|
+
@processed
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
class ControlFlowException < PetraError
|
159
|
+
end
|
160
|
+
|
161
|
+
# This error is thrown only to tell the transaction manager to
|
162
|
+
# abort the current transaction's execution.
|
163
|
+
# This is necessary e.g. after successfully committing a transaction
|
164
|
+
class AbortTransaction < ControlFlowException
|
165
|
+
end
|
166
|
+
|
167
|
+
# An error class which is never passed on out of petra.
|
168
|
+
# It is used to cause a rollback for the currently active petra transaction
|
169
|
+
class Rollback < ControlFlowException
|
170
|
+
end
|
171
|
+
|
172
|
+
# See +Rollback+, this error class is used to trigger a complete
|
173
|
+
# reset on the currently active petra transaction
|
174
|
+
# TODO: Nested transactions anyone?
|
175
|
+
class Reset < ControlFlowException
|
176
|
+
end
|
177
|
+
|
178
|
+
class Retry < ControlFlowException
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'petra/util/registrable'
|
4
|
+
|
5
|
+
module Petra
|
6
|
+
module PersistenceAdapters
|
7
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
8
|
+
|
9
|
+
#
|
10
|
+
# This class acts as an interface and specifies the methods that
|
11
|
+
# a transaction adapter has to implement.
|
12
|
+
#
|
13
|
+
class Adapter
|
14
|
+
include Petra::Util::Registrable
|
15
|
+
acts_as_register :adapter
|
16
|
+
|
17
|
+
class << self
|
18
|
+
alias [] registered_adapter
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# @abstract
|
23
|
+
#
|
24
|
+
# Persists the transaction steps which happened after
|
25
|
+
# the last changes were persisted.
|
26
|
+
#
|
27
|
+
def persist!
|
28
|
+
not_implemented
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Adds the given log entry to the queue to be persisted next.
|
33
|
+
# Fails if the queue already contains the log entry.
|
34
|
+
#
|
35
|
+
# @param [Petra::Components::LogEntry] log_entry
|
36
|
+
#
|
37
|
+
def enqueue(log_entry)
|
38
|
+
if queue.include?(log_entry)
|
39
|
+
fail Petra::PersistenceError, 'A log entry can only be added to a persistence queue once'
|
40
|
+
end
|
41
|
+
queue << log_entry
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# @abstract
|
46
|
+
#
|
47
|
+
# @return [Array<String>] the identifiers of all transactions which are
|
48
|
+
# currently persisted (>= one section finished, but not committed)
|
49
|
+
#
|
50
|
+
def transaction_identifiers
|
51
|
+
not_implemented
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# @abstract
|
56
|
+
#
|
57
|
+
# @param [Petra::Components::Transaction] _transaction
|
58
|
+
#
|
59
|
+
# @return [Array<String>] the names of all savepoints which were previously persisted
|
60
|
+
# for the given transaction
|
61
|
+
#
|
62
|
+
def savepoints(_transaction)
|
63
|
+
not_implemented
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# @abstract
|
68
|
+
#
|
69
|
+
# @param [Petra::Components::Section] _section
|
70
|
+
#
|
71
|
+
# @return [Array<Petra::Components::LogEntry>] All log entries which were previously
|
72
|
+
# persisted for the given section
|
73
|
+
#
|
74
|
+
def log_entries(_section)
|
75
|
+
not_implemented
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Resets the given transaction, meaning that all persisted information is removed
|
80
|
+
#
|
81
|
+
def reset_transaction(_transaction)
|
82
|
+
not_implemented
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# @abstract
|
87
|
+
#
|
88
|
+
# Executes the given block after acquiring a global lock
|
89
|
+
#
|
90
|
+
# The actual implementation must ensure that an acquired lock is released in case of
|
91
|
+
# an exception!
|
92
|
+
#
|
93
|
+
# @param [Boolean] suspend
|
94
|
+
# If set to +false+, the method will not suspend if the global lock could not be
|
95
|
+
# acquired. Instead, a Petra::LockError is thrown
|
96
|
+
#
|
97
|
+
# @raise [Petra::LockError] see +suspend+
|
98
|
+
#
|
99
|
+
def with_global_lock(suspend: true)
|
100
|
+
not_implemented
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# @abstract
|
105
|
+
#
|
106
|
+
# Executes the given block after acquiring a transaction based lock,
|
107
|
+
# meaning that other processes which execute something in the same transaction's context
|
108
|
+
# have to wait / abort
|
109
|
+
#
|
110
|
+
# The actual implementation must ensure that an acquired lock is released in case of
|
111
|
+
# an exception!
|
112
|
+
#
|
113
|
+
# @param [Boolean] suspend
|
114
|
+
# If set to +false+, the method will not suspend if the transaction lock could not be
|
115
|
+
# acquired. Instead, a Petra::LockError is thrown
|
116
|
+
#
|
117
|
+
# @raise [Petra::LockError] see +suspend+
|
118
|
+
#
|
119
|
+
def with_transaction_lock(_identifier, suspend: true)
|
120
|
+
not_implemented
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# @abstract
|
125
|
+
#
|
126
|
+
# Executes the given block after acquiring the lock for the given proxy (object)
|
127
|
+
#
|
128
|
+
# The actual implementation must ensure that an acquired lock is released in case of
|
129
|
+
# an exception!
|
130
|
+
#
|
131
|
+
# @param [Petra::Proxies::ObjectProxy] _proxy
|
132
|
+
#
|
133
|
+
# @param [Boolean] suspend
|
134
|
+
# See #with_global_lock
|
135
|
+
#
|
136
|
+
# @raise [Petra::LockError]
|
137
|
+
#
|
138
|
+
def with_object_lock(_proxy, suspend: true)
|
139
|
+
not_implemented
|
140
|
+
end
|
141
|
+
|
142
|
+
protected
|
143
|
+
|
144
|
+
def queue
|
145
|
+
@queue ||= []
|
146
|
+
end
|
147
|
+
|
148
|
+
def clear_queue!
|
149
|
+
@queue = []
|
150
|
+
end
|
151
|
+
end
|
152
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'petra/persistence_adapters/adapter'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module Petra
|
7
|
+
module PersistenceAdapters
|
8
|
+
class FileAdapter < Adapter
|
9
|
+
|
10
|
+
#----------------------------------------------------------------
|
11
|
+
# Configuration
|
12
|
+
#----------------------------------------------------------------
|
13
|
+
|
14
|
+
# TODO: change this to use e.g. the field accessors
|
15
|
+
class << self
|
16
|
+
def storage_directory
|
17
|
+
::Pathname.new(@storage_directory || '/tmp/petra')
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_writer :storage_directory
|
21
|
+
end
|
22
|
+
|
23
|
+
def persist!
|
24
|
+
return true if queue.empty?
|
25
|
+
|
26
|
+
# rubocop:disable Style/WhileUntilDo
|
27
|
+
# We currently only allow entries for one transaction in the queue
|
28
|
+
with_transaction_lock(queue.first.transaction_identifier) do
|
29
|
+
while (entry = queue.shift) do
|
30
|
+
identifier = create_entry_file(entry)
|
31
|
+
entry.mark_as_persisted!(identifier)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
# rubocop:enable Style/WhileUntilDo
|
35
|
+
end
|
36
|
+
|
37
|
+
def transaction_identifiers
|
38
|
+
# Wait until no other transaction is doing stuff that might lead to inconsistent data
|
39
|
+
with_global_lock do
|
40
|
+
ensure_directory_existence('transactions')
|
41
|
+
storage_file_name('transactions').children.select(&:directory?).map(&:basename).map(&:to_s)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def savepoints(transaction)
|
46
|
+
with_transaction_lock(transaction.identifier) do
|
47
|
+
return [] unless File.exist? storage_file_name('transactions', transaction.identifier)
|
48
|
+
storage_file_name('transactions', transaction.identifier).children.select(&:directory?).map do |f|
|
49
|
+
::YAML.load_file(f.join('information.yml').to_s)[:savepoint]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def log_entries(section)
|
55
|
+
with_transaction_lock(section.transaction.identifier) do
|
56
|
+
section_dir = storage_file_name(*section_dirname(section))
|
57
|
+
|
58
|
+
# If the given section has never been persisted before, we don't have to
|
59
|
+
# search further for log entries
|
60
|
+
return [] unless section_dir.exist?
|
61
|
+
|
62
|
+
section_dir.children.select { |f| f.extname == '.entry' }.map do |f|
|
63
|
+
entry_hash = ::YAML.load_file(f.to_s)
|
64
|
+
Petra::Components::LogEntry.from_hash(section, entry_hash)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Removes everything that was persisted while executing the given transaction
|
71
|
+
#
|
72
|
+
def reset_transaction(transaction)
|
73
|
+
with_transaction_lock(transaction) do
|
74
|
+
if storage_file_name('transactions', transaction.identifier).exist?
|
75
|
+
FileUtils.rm_r(storage_file_name('transactions', transaction.identifier))
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def with_global_lock(**options, &block)
|
81
|
+
with_file_lock('global.persistence', **options, &block)
|
82
|
+
rescue Petra::LockError => e
|
83
|
+
raise e if e.processed?
|
84
|
+
exception = Petra::LockError.new(lock_type: 'global', lock_name: 'global.persistence', processed: true)
|
85
|
+
raise exception, 'The global lock could not be acquired.'
|
86
|
+
end
|
87
|
+
|
88
|
+
def with_transaction_lock(transaction, **options, &block)
|
89
|
+
identifier = transaction.is_a?(Petra::Components::Transaction) ? transaction.identifier : transaction
|
90
|
+
with_file_lock("transaction.#{identifier}", **options, &block)
|
91
|
+
rescue Petra::LockError => e
|
92
|
+
raise e if e.processed?
|
93
|
+
exception = Petra::LockError.new(lock_type: 'transaction', lock_name: identifier, processed: true)
|
94
|
+
raise exception, "The transaction lock '#{identifier}' could not be acquired."
|
95
|
+
end
|
96
|
+
|
97
|
+
def with_object_lock(proxy, **options, &block)
|
98
|
+
key = proxy.__object_key.gsub(/[^a-zA-Z0-9]/, '-')
|
99
|
+
with_file_lock("proxy.#{key}", **options, &block)
|
100
|
+
rescue Petra::LockError => e
|
101
|
+
raise e if e.processed?
|
102
|
+
exception = Petra::LockError.new(lock_type: 'object', lock_name: proxy.__object_key, processed: true)
|
103
|
+
raise exception, "The object lock '#{proxy.__object_id}' could not be acquired."
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
#
|
109
|
+
# The Ruby version of `mkdir -p`
|
110
|
+
#
|
111
|
+
# @param [*Array] path
|
112
|
+
# The path to the directory in a format #storage_file_name understands
|
113
|
+
#
|
114
|
+
def ensure_directory_existence(*path)
|
115
|
+
FileUtils.mkdir_p(storage_file_name(*path))
|
116
|
+
end
|
117
|
+
|
118
|
+
#
|
119
|
+
# Executes the given block after acquiring a lock on the given filename
|
120
|
+
# If the lock is already held by this process, but not with the same file handle,
|
121
|
+
# the function will not try to acquire it again.
|
122
|
+
#
|
123
|
+
# @param [String] filename
|
124
|
+
#
|
125
|
+
# @param [Boolean] suspend
|
126
|
+
# If set to +false+, a LockError is thrown if the lock could not be acquired.
|
127
|
+
#
|
128
|
+
# @raise [Petra::LockError] If +suspend+ is set to +false+ and the lock could not be acquired
|
129
|
+
#
|
130
|
+
# TODO: sanitize file names
|
131
|
+
#
|
132
|
+
def with_file_lock(filename, suspend: true)
|
133
|
+
Thread.current[:__petra_file_locks] ||= []
|
134
|
+
lock_held = Thread.current[:__petra_file_locks].include?(lock_file_name(filename).to_s)
|
135
|
+
|
136
|
+
return yield if lock_held
|
137
|
+
|
138
|
+
File.open(lock_file_name(filename), File::RDWR | File::CREAT, 0o644) do |f|
|
139
|
+
if suspend
|
140
|
+
f.flock(File::LOCK_EX)
|
141
|
+
else
|
142
|
+
unless f.flock(File::LOCK_EX | File::LOCK_NB)
|
143
|
+
Petra.logger.debug "#{Thread.current.name}: Could not acquire '#{filename}'", :red
|
144
|
+
fail Petra::LockError
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
Petra.logger.debug "#{Thread.current.name}: Acquired Lock: #{filename}", :purple
|
149
|
+
|
150
|
+
Thread.current[:__petra_file_locks] << lock_file_name(filename).to_s
|
151
|
+
|
152
|
+
begin
|
153
|
+
yield
|
154
|
+
ensure
|
155
|
+
# Should be done automatically when the file handle is closed, but who knows
|
156
|
+
f.flock(File::LOCK_UN)
|
157
|
+
Petra.logger.debug "#{Thread.current.name}: Released Lock: #{filename}", :cyan
|
158
|
+
Thread.current[:__petra_file_locks].delete(lock_file_name(filename).to_s)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
#
|
164
|
+
# Builds the path to a given file based on the configured storage directory
|
165
|
+
#
|
166
|
+
# @example STORAGE_DIR/oompa/loompa
|
167
|
+
# storage_file_name('oompa', 'loompa')
|
168
|
+
#
|
169
|
+
def storage_file_name(*parts)
|
170
|
+
self.class.storage_directory.join(*parts)
|
171
|
+
end
|
172
|
+
|
173
|
+
#
|
174
|
+
# @param [String] filename
|
175
|
+
#
|
176
|
+
# @return [String] the path to a lockfile with the given name
|
177
|
+
#
|
178
|
+
def lock_file_name(filename)
|
179
|
+
# Make sure the locks directory actually exists
|
180
|
+
ensure_directory_existence('locks')
|
181
|
+
storage_file_name('locks', "petra.#{filename}.lock")
|
182
|
+
end
|
183
|
+
|
184
|
+
#
|
185
|
+
# Opens a file within the storage directory and yields its handle
|
186
|
+
#
|
187
|
+
def with_storage_file(*parts, mode: 'r', perm: 0o644, &block)
|
188
|
+
File.open(storage_file_name(*parts), mode, perm, &block)
|
189
|
+
end
|
190
|
+
|
191
|
+
#
|
192
|
+
# Creates a directory for the given section.
|
193
|
+
# This includes an `information.yml` file within the directory
|
194
|
+
# which contains information about the current savepoint and transaction
|
195
|
+
#
|
196
|
+
# @param [Petra::Components::Section] section
|
197
|
+
#
|
198
|
+
def add_section_directory(section)
|
199
|
+
dir = section_dirname(section)
|
200
|
+
ensure_directory_existence(*dir)
|
201
|
+
|
202
|
+
# If there is already a section directory/information file, we are done.
|
203
|
+
return if storage_file_name(*dir, 'information.yml').exist?
|
204
|
+
|
205
|
+
section_hash = {transaction_identifier: section.transaction.identifier,
|
206
|
+
savepoint: section.savepoint,
|
207
|
+
savepoint_version: section.savepoint_version}
|
208
|
+
with_storage_file(*dir, 'information.yml', mode: 'w') do |f|
|
209
|
+
::YAML.dump(section_hash, f)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
#
|
214
|
+
# Creates a file for the given LogEntry.
|
215
|
+
# It contains the necessary information to deserialize it later.
|
216
|
+
#
|
217
|
+
# These files are placed within a section directory (/transaction/section/entry)
|
218
|
+
#
|
219
|
+
# @param [Petra::Components::LogEntry] entry
|
220
|
+
#
|
221
|
+
def create_entry_file(entry)
|
222
|
+
add_section_directory(entry.section)
|
223
|
+
t = Time.now
|
224
|
+
filename = "#{t.to_i}.#{t.nsec}.entry"
|
225
|
+
with_storage_file(*section_dirname(entry.section), filename, mode: 'w') do |f|
|
226
|
+
::YAML.dump(entry.to_h(entry_identifier: filename), f)
|
227
|
+
end
|
228
|
+
filename
|
229
|
+
end
|
230
|
+
|
231
|
+
#
|
232
|
+
# @return [Array<String>] The directory name components for the given section within STORAGE_DIR
|
233
|
+
#
|
234
|
+
def section_dirname(section)
|
235
|
+
['transactions', section.transaction.identifier, section.savepoint_version.to_s]
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|