shatter 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 21861829b74cc78af7c6dc8db6b9bab8fd13e87c
4
+ data.tar.gz: 57b5858ca1af53a8c035d83c818f46df455409ea
5
+ SHA512:
6
+ metadata.gz: cb13accb271cb65b0da9b641544b46131cbef0c2a97f3332e0ad841126aa8e5005058432f39c196db168cf910c70225ffcb3f54ed1824bbd7df915588c102f43
7
+ data.tar.gz: 6b54e5c27425d2c25f3fa4bb898a5425c6c8a38635d82d273de626417f7ec9b6141ee0084e57d117c4971e2b3b0bef8ba81d4c52b415d35b02cbc3085930f563
data/lib/shatter.rb ADDED
@@ -0,0 +1,163 @@
1
+ require 'funtools'
2
+ require 'shatter/controller'
3
+
4
+ module Shatter
5
+ extend self
6
+ VERSION = '0.0.1'
7
+ PORTRANGE = 9479..9749
8
+
9
+ controller = nil
10
+ set = ->(new_controller) { controller ||= new_controller }
11
+
12
+ # Public: Set the Controller for the currently running process. This will be
13
+ # used directly if the current process is expected to be passed messages.
14
+ #
15
+ # parent - Pid to which to report the process's new Pid. (default: nil)
16
+ #
17
+ # Returns nothing.
18
+ define_method(:init) { |parent=nil| set.(Controller.new(parent)) }
19
+
20
+ # Public: Clear the process's Controller, then call init to set a new one.
21
+ # This is needed after calling fork in order to obtain a new socket.
22
+ #
23
+ # parent - Pid representing the parent process from which the new process has
24
+ # been forked.
25
+ #
26
+ # Returns nothing.
27
+ define_method(:chip) { |parent| controller=nil; init(parent) }
28
+
29
+ # Public: Wrap Controller#receive, passing a given block to the current
30
+ # Controller.
31
+ #
32
+ # block - Proc which will process messages passed to it from the Controller's
33
+ # mailbox.
34
+ #
35
+ # Returns the result of block being passed messages from Controller's mailbox.
36
+ define_method(:receive) { |&block| controller.receive(&block) }
37
+
38
+ # Public: Call a given method on a Module, passing in given arguments.
39
+ #
40
+ # mod - Module which contains the method referenced by fun.
41
+ # fun - Symbol or String referencing a method on the Module provided.
42
+ # args - Arguments to be passed to the method.
43
+ #
44
+ # Returns the result of invoking fun(*args) on mod.
45
+ define_method(:mfa) { |mod, fun, args| mod.send(fun, *args) }
46
+
47
+ # Public - Fetch the current Controllers pid if it is set.
48
+ #
49
+ # Returns a Pid or else nil.
50
+ define_method(:pid) { controller.pid if controller }
51
+
52
+ # Public - Return the Pid of the process which spawned the current process if
53
+ # one exists.
54
+ #
55
+ # Returns a Pid or else nil.
56
+ define_method(:parent) { controller.parent if controller }
57
+ end
58
+
59
+ module Kernel
60
+ private
61
+
62
+ # Public: Fork a new process to perform a given job.
63
+ #
64
+ # mod - Module which contains the method referenced by fun.
65
+ # fun - Symbol or String referencing a method on the Module provided.
66
+ # args - Arguments to be passed to the method.
67
+ #
68
+ # Returns nothing.
69
+ def chip(mod, fun, *args)
70
+ forked = fork do
71
+ Shatter.chip(Shatter.pid)
72
+ Shatter.mfa(mod, fun, args)
73
+ end
74
+ Process::detach(forked)
75
+ end
76
+
77
+ # Public: Return the current Controller's Pid if applicable.
78
+ #
79
+ # Returns a Pid or else nil.
80
+ def pid
81
+ Shatter.pid
82
+ end
83
+
84
+ # Public: Wrap Pid#pass.
85
+ #
86
+ # Returns nothing.
87
+ def pass(target, *messages)
88
+ target.pass(*messages)
89
+ end
90
+
91
+ # Public: Return the current Controller's Parent's Pid if applicable.
92
+ #
93
+ # Returns a Pid or else nil.
94
+ def parent
95
+ Shatter.parent
96
+ end
97
+
98
+ match_proc = ->(a, b) do
99
+ if([a,b].map { |o| o.is_a?(Enumerable) && a.class == b.class }.all?)
100
+ raise ArgumentError unless a.length == b.length
101
+
102
+ zipped = a.is_a?(Hash) ? a.sort.zip(b.sort) : a.zip(b)
103
+
104
+ zipped.reduce(true) do |c, e|
105
+ c && match.(*e)
106
+ end
107
+ else
108
+ a.nil? || a == b
109
+ end
110
+ end
111
+
112
+ # Public: Define a pattern to be matched within the context of a receive
113
+ # block, providing a block to execute when pattern is matched.
114
+ #
115
+ # l - Any number of arguments to define a pattern against which to match.
116
+ # b - Proc to pass the arguments to if all pattern matches succeed.
117
+ #
118
+ # Raises SyntaxError if the thread is not called within the context of a
119
+ # receive block (as signified by the thread variable :matches).
120
+ #
121
+ # Returns nothing.
122
+ define_method :match do |*l, &b|
123
+ unless Thread.current[:matches].is_a?(Array)
124
+ raise(SyntaxError, 'syntax error, unexpected match')
125
+ end
126
+
127
+ Thread.current[:matches] << ->(n) do
128
+ ->(*m) do
129
+ if m.length == n.length
130
+ raise NoMatch unless m.zip(n).map { |e| match_proc.(*e) }.all?
131
+ instance_exec(*n, &b)
132
+ end
133
+ end.(*l)
134
+ end
135
+ end
136
+
137
+ # Public: Build a pattern list and wrap Controller#receive, receiving items
138
+ # from the Controller's mailbox and responding to them as appropriate.
139
+ #
140
+ # block - Proc containing pattern definitons.
141
+ #
142
+ # Returns nothing.
143
+ def receive(&block)
144
+ old_matches = Thread.current[:matches]
145
+ Thread.current[:matches] = []
146
+
147
+ yield
148
+
149
+ # item is always an array due to the way Controller#receive and Pid#pass
150
+ # work
151
+ Shatter.receive do |item|
152
+ Thread.current[:matches].each do |pattern|
153
+ begin
154
+ return instance_exec(item, &pattern)
155
+ rescue NoMatch
156
+ end
157
+ end
158
+ instance_exec { raise NoMatch }
159
+ end
160
+
161
+ Thread::current[:matches] = old_matches
162
+ end
163
+ end
@@ -0,0 +1,167 @@
1
+ require 'shatter/pid'
2
+ require 'shatter/pidlist'
3
+ require 'socket'
4
+ require 'thread'
5
+
6
+ module Shatter
7
+ class Controller
8
+ attr_reader :parent
9
+
10
+ # Public: Initialize the Controller, setting up the listening Socket and
11
+ # thread to manage the mailbox.
12
+ #
13
+ # parent - Pid of the parent Controller, if applicable.
14
+ def initialize(parent)
15
+ @parent = parent
16
+ @socket = listen
17
+ @mailbox = Queue.new
18
+ @known = Shatter::Pidlist.new
19
+ @chunks = {}
20
+
21
+ pass(@parent, :system, [:childpid, pid]) if @parent
22
+ Thread.new { mailbox_loop }
23
+ end
24
+
25
+ # Public: Pass items in the current mailbox to a given block, removing them
26
+ # permanently from the mailbox unless the item doesn't match any pattern in
27
+ # the block.
28
+ #
29
+ # Returns nothing.
30
+ def receive(&block)
31
+ newbox = Queue.new
32
+ item = @mailbox.pop
33
+ instance_exec { block.(item) }
34
+ rescue NoMatch
35
+ newbox << item
36
+ retry
37
+ ensure
38
+ restore_mailbox(@mailbox, newbox, nil)
39
+ end
40
+
41
+ # Public: Return the Pid representing the Controller, initializing the
42
+ # struct if necessary.
43
+ #
44
+ # Returns a Pid.
45
+ def pid
46
+ unless @pid
47
+ _, port, _, ip = @socket.addr.map(&:freeze)
48
+ @pid = Shatter::Pid.new($$, ip, port, '')
49
+ end
50
+ @pid
51
+ end
52
+
53
+ private
54
+
55
+ # Internal: Accept connections on the listening socket, accepting messages
56
+ # and putting them into the mailbox. This should be run within its own
57
+ # thread.
58
+ #
59
+ # Does not return.
60
+ deftail :mailbox_loop do
61
+ connection = @socket.accept
62
+ Thread.new do
63
+ data = connection.read
64
+ begin
65
+ message = Marshal.load(data)
66
+ @mailbox << message unless handle_message(message)
67
+ rescue ArgumentError
68
+ end
69
+ end
70
+ mailbox_loop
71
+ end
72
+
73
+ # Internal: Open a listening socket on the first available port within a
74
+ # given range.
75
+ #
76
+ # socket - TCPServer if one has been established, or nil. (default: nil)
77
+ # range - Range of ports to try to bind to. (default: 9479..9749)
78
+ # port - Fixnum indicating the port to attempt to bind to.
79
+ # (default: lowest port in range)
80
+ #
81
+ # Returns a listening TCPSocket.
82
+ deftail :listen do |socket=nil, range=PORTRANGE, port=range.min|
83
+ socket ? socket : listen(get_socket(port, range.max), range, port+1)
84
+ end
85
+
86
+ # Internal: Restore the state of a mailbox from a mailbox and temporary
87
+ # mailbox, first emptying the mailbox into the temporary queue and then
88
+ # vise versa, keeping the order consistent.
89
+ #
90
+ # a - Queue representing the original mailbox.
91
+ # b - Queue representing the temporary target mailbox.
92
+ # c - Status indication - true indicating that the original mailbox is
93
+ # ready to be filled, with any other value indicating that the original
94
+ # mailbox is still in need of being emptied.
95
+ #
96
+ # Returns nothing.
97
+ defpattern :restore_mailbox do
98
+ restore_mailbox(nil, nil, true) do |a,b,_|
99
+ drain_mailbox(a, b)
100
+ end
101
+ restore_mailbox(nil, nil, nil) do |a,b,_|
102
+ restore_mailbox(b, a, drain_mailbox(a, b))
103
+ end
104
+ end
105
+
106
+ # Internal: Check for any messages which are meant to be handled by the
107
+ # system itself (indicated by [:system, [:type, payload]] structure).
108
+ # Handle any which are encountered, or else return false. Handlers must
109
+ # return a truthy value to avoid the system message being added to the
110
+ # mailbox.
111
+ #
112
+ # message - Message to be checked.
113
+ #
114
+ # Returns false unless a message is handled.
115
+ defpattern :handle_message do
116
+ handle_message([:system, [:childpid, nil]]) { |_, pid| add_pid(pid.last) }
117
+ handle_message(nil) { |_| false }
118
+ end
119
+
120
+ # Internal: Add a Pid of another Controller to a list of known Pids.
121
+ #
122
+ # pid - Pid representing another Controller.
123
+ #
124
+ # Returns the Pid.
125
+ def add_pid(pid)
126
+ @known.insert(pid)
127
+ end
128
+
129
+ # Internal: Remove all messages from a given source Queue, inserting them
130
+ # into a target Queue.
131
+ #
132
+ # source - Queue from which to drain messages.
133
+ # target - Queue into which messages should be pushed.
134
+ #
135
+ # Returns true once source is empty.
136
+ def drain_mailbox(source, target)
137
+ loop { target << source.pop(true) }
138
+ rescue ThreadError
139
+ true
140
+ end
141
+
142
+ # Internal: Attempt to start a TCPServer on a given port.
143
+ #
144
+ # port - Fixnum indicating the port to which to attempt to bind.
145
+ # max - Fixnum representing the maximum port allowable.
146
+ #
147
+ # Raises StandardError if port is greater than max.
148
+ #
149
+ # Returns a TCPServer if bound, or nil if the port is in use.
150
+ def get_socket(port, max)
151
+ raise StandardError, "Cannot allocate a port" if port > max
152
+
153
+ TCPServer.open('127.0.0.1', port)
154
+ rescue Errno::EADDRINUSE
155
+ nil
156
+ end
157
+
158
+ # listen :: Maybe Socket -> Range -> Fixnum -> Socket
159
+ settype(:listen, [IPSocket, NilClass], Range, Fixnum, IPSocket)
160
+ # restore_mailbox :: Queue -> Queue -> Bool -> Bool
161
+ settype(:restore_mailbox, Queue, Queue, [TrueClass, NilClass], TrueClass)
162
+ # drain_mailbox :: Queue -> Queue -> Bool
163
+ settype(:drain_mailbox, Queue, Queue, TrueClass)
164
+ # get_socket :: Fixnum -> Fixnum -> Maybe Socket
165
+ settype(:get_socket, Fixnum, Fixnum, [NilClass, IPSocket])
166
+ end
167
+ end
@@ -0,0 +1,26 @@
1
+ require 'socket'
2
+
3
+ module Shatter
4
+ class Pid < Struct.new(:pid, :host, :port, :name)
5
+ # Public: Open a socket to a given Controller and send any number of
6
+ # messages to it.
7
+ #
8
+ # messages - Any number of arguments which will constitute a message to be
9
+ # sent to the Controller.
10
+ #
11
+ # Returns nothing.
12
+ def pass(*messages)
13
+ socket = TCPSocket.new(host, port)
14
+ socket.send(Marshal.dump(messages), 0)
15
+ socket.close
16
+ end
17
+
18
+ # Public: Represent the Pid as a String.
19
+ #
20
+ # Returns a String.
21
+ def inspect
22
+ "<#{pid} #{host}:#{port}#{" (#{name})" unless name.empty?}>"
23
+ end
24
+ alias :to_s :inspect
25
+ end
26
+ end
@@ -0,0 +1,60 @@
1
+ require 'funtools'
2
+ require 'thread'
3
+
4
+ module Shatter
5
+ class Pidlist
6
+ include Enumerable
7
+
8
+ # Public: Initialize the internal state of the Pidlist.
9
+ def initialize
10
+ @known = []
11
+ @mutex = Mutex.new
12
+ end
13
+
14
+ # Public: Iterate through known Pids.
15
+ #
16
+ # Yields each item in the list of known Pids.
17
+ #
18
+ # Returns nothing.
19
+ def each(&block)
20
+ @known.each(&block)
21
+ end
22
+
23
+ # Public: Add a Pid to the list of known Pids.
24
+ #
25
+ # pid - Pid to be tracked.
26
+ #
27
+ # Returns nothing.
28
+ def insert(pid)
29
+ mutex.synchronize do
30
+ known << pid
31
+ end
32
+ end
33
+
34
+ # Internal: Create a method which allows for fetching a Pid by a given
35
+ # attribute.
36
+ #
37
+ # Signature
38
+ #
39
+ # by_<kind>
40
+ #
41
+ # kind - String containing the name of the value by which to select.
42
+ def self.build_fetch_by(kind)
43
+ define_method("by_#{kind}") do |val|
44
+ select { |pid| pid.send(kind) == val }
45
+ end
46
+ end
47
+
48
+ build_fetch_by('name')
49
+ build_fetch_by('pid')
50
+ build_fetch_by('host')
51
+ build_fetch_by('port')
52
+
53
+ # insert :: Pid -> [Pid]
54
+ settype(:insert, Pid, Array)
55
+
56
+ private
57
+
58
+ attr_reader :known, :mutex
59
+ end
60
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shatter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Tina Wuest
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: funtools
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.7'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.7.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.7'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.7.1
33
+ description: Framework to facilitate distributed computing with Ruby
34
+ email: tina@wuest.me
35
+ executables: []
36
+ extensions: []
37
+ extra_rdoc_files: []
38
+ files:
39
+ - lib/shatter.rb
40
+ - lib/shatter/controller.rb
41
+ - lib/shatter/pid.rb
42
+ - lib/shatter/pidlist.rb
43
+ homepage: https://gitlab.com/wuest/shatter
44
+ licenses:
45
+ - MIT
46
+ metadata: {}
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubyforge_project:
63
+ rubygems_version: 2.2.2
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: Distributed ruby library
67
+ test_files: []