petra_core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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