shatter 0.0.1

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