petra_core 0.0.1

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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +83 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +5 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +13 -0
  9. data/Gemfile.lock +74 -0
  10. data/MIT-LICENSE +20 -0
  11. data/README.md +726 -0
  12. data/Rakefile +8 -0
  13. data/bin/console +8 -0
  14. data/bin/setup +8 -0
  15. data/examples/continuation_error.rb +125 -0
  16. data/examples/dining_philosophers.rb +138 -0
  17. data/examples/showcase.rb +54 -0
  18. data/lib/petra/components/entries/attribute_change.rb +29 -0
  19. data/lib/petra/components/entries/attribute_change_veto.rb +37 -0
  20. data/lib/petra/components/entries/attribute_read.rb +20 -0
  21. data/lib/petra/components/entries/object_destruction.rb +22 -0
  22. data/lib/petra/components/entries/object_initialization.rb +19 -0
  23. data/lib/petra/components/entries/object_persistence.rb +26 -0
  24. data/lib/petra/components/entries/read_integrity_override.rb +42 -0
  25. data/lib/petra/components/entry_set.rb +87 -0
  26. data/lib/petra/components/log_entry.rb +342 -0
  27. data/lib/petra/components/proxy_cache.rb +209 -0
  28. data/lib/petra/components/section.rb +543 -0
  29. data/lib/petra/components/transaction.rb +405 -0
  30. data/lib/petra/components/transaction_manager.rb +214 -0
  31. data/lib/petra/configuration/base.rb +132 -0
  32. data/lib/petra/configuration/class_configurator.rb +309 -0
  33. data/lib/petra/configuration/configurator.rb +67 -0
  34. data/lib/petra/core_ext.rb +27 -0
  35. data/lib/petra/exceptions.rb +181 -0
  36. data/lib/petra/persistence_adapters/adapter.rb +154 -0
  37. data/lib/petra/persistence_adapters/file_adapter.rb +239 -0
  38. data/lib/petra/proxies/abstract_proxy.rb +149 -0
  39. data/lib/petra/proxies/enumerable_proxy.rb +44 -0
  40. data/lib/petra/proxies/handlers/attribute_read_handler.rb +45 -0
  41. data/lib/petra/proxies/handlers/missing_method_handler.rb +47 -0
  42. data/lib/petra/proxies/method_handlers.rb +213 -0
  43. data/lib/petra/proxies/module_proxy.rb +12 -0
  44. data/lib/petra/proxies/object_proxy.rb +310 -0
  45. data/lib/petra/util/debug.rb +45 -0
  46. data/lib/petra/util/extended_attribute_accessors.rb +51 -0
  47. data/lib/petra/util/field_accessors.rb +35 -0
  48. data/lib/petra/util/registrable.rb +48 -0
  49. data/lib/petra/util/test_helpers.rb +9 -0
  50. data/lib/petra/version.rb +5 -0
  51. data/lib/petra.rb +100 -0
  52. data/lib/tasks/petra_tasks.rake +5 -0
  53. data/petra.gemspec +36 -0
  54. metadata +208 -0
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'petra'
6
+
7
+ require 'pry'
8
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Style/GlobalVars
4
+
5
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
6
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'spec', 'support')
7
+ require 'petra'
8
+ require 'faker'
9
+
10
+ #
11
+ # This example shows why continuations and production code / code
12
+ # that uses external libraries are not a good combination.
13
+ #
14
+
15
+ #----------------------------------------------------------------
16
+ # Sample Class Definitions
17
+ #----------------------------------------------------------------
18
+
19
+ class SimpleUser
20
+ attr_accessor :first_name
21
+ attr_accessor :last_name
22
+
23
+ def initialize
24
+ @first_name, @last_name = Faker::Name.name.split(' ')
25
+ end
26
+
27
+ def save
28
+ # Do nothing, we just want an explicit save method.
29
+ # We could also set every attribute write to also be a persistence method
30
+ end
31
+ end
32
+
33
+ Petra.configure do
34
+ configure_class 'SimpleUser' do
35
+ proxy_instances true
36
+
37
+ attribute_reader? do |method_name|
38
+ %w[first_name last_name].include?(method_name.to_s)
39
+ end
40
+
41
+ attribute_writer? do |method_name|
42
+ %w[first_name= last_name=].include?(method_name.to_s)
43
+ end
44
+
45
+ persistence_method? do |method_name|
46
+ %w[save].include?(method_name.to_s)
47
+ end
48
+ end
49
+ end
50
+
51
+ class ConfidentialData < String
52
+ def read?
53
+ !!@read
54
+ end
55
+
56
+ def read!
57
+ @read = true
58
+ end
59
+ end
60
+
61
+ class SimpleHandler
62
+ def with_confidential_data(string)
63
+ @confidential_data = ConfidentialData.new(string)
64
+ yield
65
+ rescue Exception
66
+ # The data might have been compromised! Delete it!
67
+ @confidential_data = nil
68
+ raise
69
+ end
70
+
71
+ def do_confidential_stuff(user)
72
+ puts "User #{user.first_name} #{user.last_name} is very confidential."
73
+ user.last_name = user.last_name + ' ' + ('I' * @confidential_data.length) + '.'
74
+ ensure
75
+ @confidential_data.read!
76
+ end
77
+ end
78
+
79
+ #----------------------------------------------------------------
80
+ # Helper Methods
81
+ #----------------------------------------------------------------
82
+
83
+ # rubocop:disable Security/Eval
84
+ def transaction(id_no)
85
+ Petra.transaction(identifier: eval("$t_id_#{id_no}", nil, __FILE__, __LINE__)) do
86
+ yield
87
+ rescue Petra::ValueComparisonError => e
88
+ e.ignore!
89
+ e.continue!
90
+ end
91
+ end
92
+
93
+ # rubocop:enable Security/Eval
94
+
95
+ #----------------------------------------------------------------
96
+ # Actual Example
97
+ #----------------------------------------------------------------
98
+
99
+ # Create 2 transaction identifiers
100
+ $t_id_1 = Petra.transaction {}
101
+ $t_id_2 = Petra.transaction {}
102
+
103
+ # Instantiate a handler and a SimpleUser object proxy
104
+ handler = SimpleHandler.new
105
+ user = SimpleUser.petra.new
106
+
107
+ transaction(1) do
108
+ user.first_name
109
+ user.last_name = Faker::Name.last_name
110
+ user.save
111
+ end
112
+
113
+ transaction(2) do
114
+ user.first_name, user.last_name = Faker::Name.name.split(' ')
115
+ user.save
116
+ Petra.commit!
117
+ end
118
+
119
+ transaction(1) do
120
+ handler.with_confidential_data('Ulf.') do
121
+ handler.do_confidential_stuff(user) #=> Undefined method #read! for nil:NilClass...
122
+ end
123
+ end
124
+
125
+ # rubocop:enable Style/GlobalVars
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
4
+ require 'petra'
5
+
6
+ # This file contains a transaction based solution to the dining philosophers problem
7
+ # with five philosophers. It uses the transactions' retry mechanic to ensure
8
+ # that both sticks have to be taken at the same time.
9
+
10
+ class Philosopher
11
+ attr_reader :number
12
+
13
+ def initialize(number, *sticks)
14
+ @number = number
15
+ @left_stick, @right_stick = sticks.map(&:petra)
16
+ end
17
+
18
+ def eating?
19
+ !!@eating
20
+ end
21
+
22
+ def think
23
+ sleep(rand(5))
24
+ end
25
+
26
+ def take_stick(stick)
27
+ fail Petra::Retry if stick.taken
28
+ stick.taken = true
29
+ stick.save
30
+ end
31
+
32
+ def put_stick(stick)
33
+ stick.taken = false
34
+ stick.save
35
+ end
36
+
37
+ def take_sticks
38
+ Petra.transaction(identifier: "philosopher_#{@number}") do
39
+ take_stick(@left_stick)
40
+ take_stick(@right_stick)
41
+ Petra.commit!
42
+ rescue Petra::LockError => e
43
+ e.retry!
44
+ rescue Petra::ReadIntegrityError, Petra::WriteClashError => e
45
+ e.retry!
46
+ end
47
+ end
48
+
49
+ def put_sticks
50
+ Petra.transaction(identifier: "philosopher_#{@number}") do
51
+ put_stick(@left_stick)
52
+ put_stick(@right_stick)
53
+ Petra.commit!
54
+ end
55
+ end
56
+
57
+ def eat
58
+ take_sticks
59
+
60
+ @eating = true
61
+ sleep(2)
62
+ @eating = false
63
+
64
+ put_sticks
65
+ end
66
+
67
+ def live
68
+ loop do
69
+ think
70
+ eat
71
+ end
72
+ end
73
+ end
74
+
75
+ class Stick < Mutex
76
+ attr_reader :number
77
+
78
+ def initialize(number)
79
+ @number = number
80
+ end
81
+
82
+ alias taken locked?
83
+
84
+ def taken=(new_value)
85
+ if new_value
86
+ try_lock || fail(Exception, 'Already locked!')
87
+ else
88
+ unlock
89
+ end
90
+ end
91
+
92
+ def save; end
93
+ end
94
+
95
+ Petra.configure do
96
+ log_level :warn
97
+
98
+ configure_class 'Stick' do
99
+ proxy_instances true
100
+
101
+ attribute_reader? do |method_name|
102
+ %w[taken].include?(method_name.to_s)
103
+ end
104
+
105
+ attribute_writer? do |method_name|
106
+ %w[taken=].include?(method_name.to_s)
107
+ end
108
+
109
+ persistence_method? do |method_name|
110
+ %w[save].include?(method_name)
111
+ end
112
+ end
113
+ end
114
+
115
+ # If not set, a thread would silently fail without
116
+ # interrupting the main thread.
117
+ Thread.abort_on_exception = true
118
+
119
+ sticks = Array.new(5) { |i| Stick.new(i) }
120
+ philosophers = Array.new(5) { |i| Philosopher.new(i, sticks[i], sticks[(i + 1) % 5]) }
121
+
122
+ philosophers.map do |phil|
123
+ t = Thread.new { phil.live }
124
+ t.name = "Philosopher #{phil.number}"
125
+ end
126
+
127
+ # The output may contain some invalid states as it might happen
128
+ # during a commit phase with only one stick taken.
129
+ loop do
130
+ philosophers.each_with_index do |phil, idx|
131
+ stick = sticks[idx]
132
+ STDOUT.write stick.taken ? ' _ ' : ' | '
133
+ STDOUT.write phil.eating? ? ' 😁 ' : ' 😑 '
134
+ end
135
+
136
+ STDOUT.write("\r")
137
+ sleep(0.2)
138
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
4
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'spec', 'support', 'classes')
5
+ require 'petra'
6
+ require 'simple_user'
7
+ require 'simple_user_with_auto_save'
8
+
9
+ Petra.configure do
10
+ log_level :warn
11
+ end
12
+
13
+ def log(message, identifier = 'External')
14
+ puts [identifier, message].join(': ')
15
+ end
16
+
17
+ user = Classes::SimpleUserWithAutoSave.petra.new('John', 'Doe')
18
+
19
+ # Start a new transaction and start changing attributes
20
+ Petra.transaction(identifier: 'tr1') do
21
+ user.first_name = 'Foo'
22
+ end
23
+
24
+ # No changes outside the transaction yet...
25
+ log user.name #=> 'John Doe'
26
+
27
+ # Continue the same transaction
28
+ Petra.transaction(identifier: 'tr1') do
29
+ log(user.name, 'tr1') #=> 'Foo Doe'
30
+ user.last_name = 'Bar'
31
+ end
32
+
33
+ # Another transaction changes a value already changed in 'tr1'
34
+ Petra.transaction(identifier: 'tr2') do
35
+ log(user.name, 'tr2') #=> John Doe
36
+ user.first_name = 'Moo'
37
+ Petra.commit!
38
+ end
39
+
40
+ log user.name #=> 'Moo Doe'
41
+
42
+ # Try to commit our first transaction
43
+ Petra.transaction(identifier: 'tr1') do
44
+ log(user.name, 'tr1')
45
+ Petra.commit!
46
+ rescue Petra::WriteClashError => e
47
+ # => "The attribute `first_name` has been changed externally and in the transaction. (Petra::WriteClashError)"
48
+ # Let's use our value and go on with committing the transaction
49
+ e.use_ours!
50
+ e.continue!
51
+ end
52
+
53
+ # The actual object is updated with the values from tr1
54
+ log user.name #=> 'Foo Bar'
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Components
5
+ module Entries
6
+ class AttributeChange < Petra::Components::LogEntry
7
+ field_accessor :old_value
8
+ field_accessor :new_value
9
+ field_accessor :method
10
+
11
+ def self.kind
12
+ :attribute_change
13
+ end
14
+
15
+ def apply!
16
+ # Check if there is an an attribute change veto which is newer than this
17
+ # attribute change. If there is, we may not apply this entry.
18
+ # TODO: Check if this behaviour is sufficient.
19
+ return if transaction.attribute_change_veto?(load_proxy, attribute: attribute)
20
+
21
+ # Otherwise, use the logged method to set the new attribute value
22
+ proxied_object.send(method, new_value)
23
+ end
24
+
25
+ Petra::Components::LogEntry.register_entry_type(kind, self)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Components
5
+ module Entries
6
+ #
7
+ # Tells the system to ignore all attribute changes we made to the current
8
+ # attribute during the transaction.
9
+ #
10
+ class AttributeChangeVeto < Petra::Components::LogEntry
11
+ # Mostly for debugging purposes: The external value that caused
12
+ # the creation of this log entry
13
+ field_accessor :external_value
14
+
15
+ def self.kind
16
+ :attribute_change_veto
17
+ end
18
+
19
+ #
20
+ # As for ReadIntegrityOverrides, we have to make sure that
21
+ # AttributeChangeVetoes are always persisted.
22
+ #
23
+ def persist?
24
+ true
25
+ end
26
+
27
+ def persist_on_retry?
28
+ true
29
+ end
30
+
31
+ def apply!; end
32
+
33
+ Petra::Components::LogEntry.register_entry_type(kind, self)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Components
5
+ module Entries
6
+ class AttributeRead < Petra::Components::LogEntry
7
+ field_accessor :method
8
+ field_accessor :value
9
+
10
+ def self.kind
11
+ :attribute_read
12
+ end
13
+
14
+ def apply!; end
15
+
16
+ Petra::Components::LogEntry.register_entry_type(:attribute_read, self)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Components
5
+ module Entries
6
+ class ObjectDestruction < Petra::Components::LogEntry
7
+ field_accessor :method
8
+
9
+ def self.kind
10
+ :object_destruction
11
+ end
12
+
13
+ def apply!
14
+ # TODO: React to `false` responses from destruction methods?
15
+ proxied_object.send(method)
16
+ end
17
+
18
+ Petra::Components::LogEntry.register_entry_type(:object_destruction, self)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Components
5
+ module Entries
6
+ class ObjectInitialization < Petra::Components::LogEntry
7
+ field_accessor :method
8
+
9
+ def self.kind
10
+ :object_initialization
11
+ end
12
+
13
+ def apply!; end
14
+
15
+ Petra::Components::LogEntry.register_entry_type(kind, self)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Components
5
+ module Entries
6
+ class ObjectPersistence < Petra::Components::LogEntry
7
+ field_accessor :method
8
+
9
+ # Arguments given to the persistence method.
10
+ # This is especially necessary for persistence methods which are
11
+ # also attribute writers or similar.
12
+ field_accessor :args
13
+
14
+ def self.kind
15
+ :object_persistence
16
+ end
17
+
18
+ def apply!
19
+ proxied_object.send(method, *(args || []))
20
+ end
21
+
22
+ Petra::Components::LogEntry.register_entry_type(kind, self)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petra
4
+ module Components
5
+ module Entries
6
+ #
7
+ # Tells the system not to raise further ReadIntegrityErrors for the given attribute
8
+ # as long as the external value stays the same.
9
+ #
10
+ class ReadIntegrityOverride < Petra::Components::LogEntry
11
+ # The external attribute value at the time this log entry
12
+ # was created. It is used to determine whether a new ReadIntegrityError has
13
+ # to be raised or not.
14
+ field_accessor :external_value
15
+
16
+ def self.kind
17
+ :read_integrity_override
18
+ end
19
+
20
+ #
21
+ # ReadIntegrityOverrides always have to be persisted:
22
+ # They are only generated if an exception (ReadIntegrityError, etc) happened
23
+ # which in most cases (except for a rescue within the transaction proc itself)
24
+ # means that its execution stopped and the only thing left is persisting the transaction.
25
+ # Therefore, this log entry will most likely be the last one in the current section
26
+ # and would be lost if we wouldn't persist it.
27
+ #
28
+ def persist?
29
+ true
30
+ end
31
+
32
+ def persist_on_retry?
33
+ true
34
+ end
35
+
36
+ def apply!; end
37
+
38
+ Petra::Components::LogEntry.register_entry_type(kind, self)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'petra/components/log_entry'
4
+
5
+ module Petra
6
+ module Components
7
+ #
8
+ # An EntrySet is a collection of log entries for a certain section.
9
+ # It may be used to chain-filter entries, e.g. only object_persisted entries for a certain proxy.
10
+ #
11
+ # TODO: Probably be Enumerator::Lazy...
12
+ #
13
+ class EntrySet < Array
14
+
15
+ #----------------------------------------------------------------
16
+ # Filters
17
+ #----------------------------------------------------------------
18
+
19
+ def for_proxy(proxy)
20
+ wrap { select { |e| e.for_object?(proxy.__object_key) } }
21
+ end
22
+
23
+ def of_kind(kind)
24
+ wrap { select { |e| e.kind?(kind) } }
25
+ end
26
+
27
+ def for_attribute_key(key)
28
+ wrap { select { |e| e.attribute_key.to_s == key.to_s } }
29
+ end
30
+
31
+ def object_persisted
32
+ wrap { select(&:object_persisted?) }
33
+ end
34
+
35
+ def not_object_persisted
36
+ wrap { reject(&:object_persisted?) }
37
+ end
38
+
39
+ def latest
40
+ last
41
+ end
42
+
43
+ #----------------------------------------------------------------
44
+ # Persistence / Commit
45
+ #----------------------------------------------------------------
46
+
47
+ #
48
+ # Applies all log entries which were marked as object persisted
49
+ # The log entry itself decides whether it is actually executed or not.
50
+ #
51
+ def apply!
52
+ object_persisted.each(&:apply!)
53
+ end
54
+
55
+ #
56
+ # Tells each log entry to enqueue for persisting.
57
+ # The individual log entries may decided whether they actually want
58
+ # to be persisted or not.
59
+ #
60
+ def enqueue_for_persisting!
61
+ each(&:enqueue_for_persisting!)
62
+ end
63
+
64
+ def prepare_for_retry!
65
+ select(&:persist_on_retry?).each(&:enqueue_for_persisting!)
66
+ end
67
+
68
+ #----------------------------------------------------------------
69
+ # Wrapped Array Methods
70
+ #----------------------------------------------------------------
71
+
72
+ def reverse(*)
73
+ wrap { super }
74
+ end
75
+
76
+ def sort(*)
77
+ wrap { super }
78
+ end
79
+
80
+ private
81
+
82
+ def wrap
83
+ self.class.new(yield)
84
+ end
85
+ end
86
+ end
87
+ end