Mercurial > pylearn
comparison doc/v2_planning/plugin_architecture_GD.txt @ 1139:9f0502f8c7a5
Example of the plugin architecture I had in mind
author | gdesjardins |
---|---|
date | Thu, 16 Sep 2010 13:27:17 -0400 |
parents | |
children | 9ff2242a817b |
comparison
equal
deleted
inserted
replaced
1138:9583e908c572 | 1139:9f0502f8c7a5 |
---|---|
1 Overview | |
2 ======== | |
3 | |
4 The "central authority" (CA) is the glue which takes care of interfacing plugins | |
5 with one another. It has 3 basic roles: | |
6 * it maintains a list of "registered" or "active" plugins | |
7 * it receives and queues the various messages sent by the plugins | |
8 * dispatches the messages to the recipient, based on various "events" | |
9 | |
10 Events can take different forms: | |
11 * the CA can trigger various events based on running time | |
12 * can be linked to messages emitted by the various plugins. Events can be | |
13 triggered based on the frequency of such messages. | |
14 * Once an event is triggered, it is relayed to the appropriate "recipient | |
15 plugin(s)" | |
16 | |
17 It is the responsibility of each plugin to inform the CA of which "events" it | |
18 cares about. | |
19 | |
20 | |
21 Generic Pseudo-code | |
22 =================== | |
23 | |
24 I'll try to write this in pseudo-python as best I can. I'll do this in | |
25 traditional OOP, as this is what I'm more comfortable with. I'll leave it up to | |
26 James and OB to python-ize this :) | |
27 | |
28 | |
29 class MessageX(Message): | |
30 """ | |
31 A message is basically a data container. This could very well be replaced by | |
32 a generic Python object. | |
33 """ | |
34 | |
35 class Plugin(object): | |
36 """ | |
37 The base plugin object doesn't do much. It contains a reference to the CA | |
38 (upon plugin being registered with the CA), provides boilerplate code | |
39 for storing which "events" this plugin is susceptible to, as well as code | |
40 for registering callback functions for the various messages. | |
41 """ | |
42 | |
43 CA = None # to be initialized upon plugin registration | |
44 active_msg = {} # dictionary of messages this plugin is susceptible to | |
45 callbacks = {} # mapping of message class names --> callback function | |
46 | |
47 def listen(msg_class, interval): | |
48 """ | |
49 :param msg_class: reference to the "message" class we are interested in. | |
50 These messages will be forwarded to this plugin, when | |
51 the trigger condition is met. | |
52 :param interval: integer. Forward the message to this plugin every 'interval' | |
53 such messages. | |
54 """ | |
55 self.active_msg[msg_class] = interval | |
56 | |
57 | |
58 def check_trigger(msg_class, time): | |
59 """ | |
60 Checks whether or not the "trigger" condition associated with message of | |
61 class 'msg_class' is satisfied or not. This could be the default | |
62 behavior, and be overridden by the various plugins. | |
63 """ | |
64 return time % self.active_msg[msg_class] == 0 | |
65 | |
66 | |
67 def handler(msg_class, callback): | |
68 """ | |
69 Decorator which registers a callback function for the given message | |
70 type. | |
71 | |
72 NOTE: I don't think what I wrote would work as a Python decorator. I am | |
73 not sure how to handle decoraters with multiple parameters (one | |
74 explicit, and the other as the reference to the function). I'm pretty | |
75 sure James or OB could figure it out though ! | |
76 | |
77 :params msg_class: reference to the message class for which we are | |
78 registering a callback function | |
79 :params callback : reference to which function to call for a given message | |
80 """ | |
81 | |
82 self.callbacks[msg_class] = callback | |
83 | |
84 | |
85 def execute(self, message): | |
86 """ | |
87 Boiler-plate code which executes the right callback function, for the | |
88 given message type. | |
89 """ | |
90 for (msg_class, callback) in self.callbacks.iteritems(): | |
91 if message.__class__ == msg_class: | |
92 callback(message) | |
93 | |
94 | |
95 class ProducerPlugin(Plugin): | |
96 | |
97 def dostuff(): | |
98 """ | |
99 A typical "producer" plugin. It basically performs an arbitrary action | |
100 and asks the CA to forward the results (in the form of a message) to | |
101 other plugins. | |
102 """ | |
103 | |
104 # iteratively do stuff and relay messages to other plugins | |
105 while(condition): | |
106 | |
107 msga = # do something | |
108 ca.send(msga) # ask CA to forward to other plugins | |
109 | |
110 | |
111 class ConsumerPlugin(Plugin): | |
112 | |
113 @handler(MessageA) | |
114 def func(msga): | |
115 """ | |
116 A consumer or "passive plugin" (eg. logger, etc). This function is | |
117 register as being the callback function for Message A objects. | |
118 """ | |
119 # do something with message A | |
120 | |
121 | |
122 class ConsumerProducerPlugin(Plugin): | |
123 | |
124 @handler(MessageA) | |
125 def func(msga): | |
126 """ | |
127 Example of a consumer / producer plugin. It receives MessageA messages, | |
128 processes the data, then asks the CA to send a new message (MessageB) as | |
129 the result of its computation. The CA will automatically forward to all | |
130 interested parties. | |
131 | |
132 :param msga: MessageA instance | |
133 """ | |
134 | |
135 data = dostuff(msga) # process message | |
136 msgb = MessageB(data) # generate new message for other plugins | |
137 ca.send(msgb) # ask CA to forward to other plugins | |
138 | |
139 | |
140 | |
141 class CentralAuthority(object): | |
142 | |
143 active_plugins = [] # contains a list of registered plugins | |
144 | |
145 mailmain = {} # dictionary which contains, for each message class, a | |
146 # list of plugins interested in this message | |
147 | |
148 event_count = {} # dictionary of "event" counts for various messages | |
149 | |
150 def register(plugin): | |
151 """ | |
152 Registers the plugin and adds it as a listener for the various messages | |
153 it is interested in. | |
154 :param plugin: plugin instance which we want to "activate" | |
155 """ | |
156 | |
157 # each plugin must have a reference to the CA | |
158 plugin.ca = self | |
159 | |
160 # maintain list of active plugins | |
161 active_plugins.append(plugin) | |
162 | |
163 # remember which messages this plugin cares about | |
164 for msg in plugin.active_msg.keys(): | |
165 self.mailman[msg].append(plugin) | |
166 self.event_count[msg] = 0 | |
167 | |
168 def send(msg): | |
169 """ | |
170 This function relays the message to the appropriate plugins, based on | |
171 their "trigger" condition. It also keeps track of the number of times | |
172 this event was raised. | |
173 | |
174 :param msg: message instance | |
175 """ | |
176 | |
177 event_count[msg.__class__] += 1 | |
178 | |
179 # for all plugins interested in this message ... | |
180 for plugin in self.mailman[msg.__class__]: | |
181 | |
182 # check if trigger condition is met | |
183 if plugin.check_trigger(msg, self.event_count[msg.__class__]): | |
184 | |
185 # have the plugin execute the message | |
186 plugin.execute(msg) | |
187 | |
188 | |
189 def run(self): | |
190 """ | |
191 This would be the main loop of the program. I won't go into details | |
192 because its still somewhat blurry in my head :) But basically, the CA | |
193 could be configured to send out its own messages, independently from all | |
194 other plugins. | |
195 | |
196 These could be "synchronous" messages such as: "5 seconds have passed", | |
197 or others such as "save state we are about to get killed". | |
198 | |
199 NOTE: seems like this would almost have to live in its own thread ... | |
200 """ | |
201 | |
202 # the following would be parametrized obviously | |
203 while(True): | |
204 msg = ElapsedTimeMessage(5) | |
205 self.send(msg) | |
206 sleep(5) | |
207 | |
208 | |
209 | |
210 Putting it all-together | |
211 ======================= | |
212 | |
213 | |
214 def main(): | |
215 | |
216 ca = CentralAuthority() | |
217 | |
218 producer = ProducerPlugin() | |
219 ca.register(producer) | |
220 | |
221 consumer = ConsumerPlugin() | |
222 consumer.listen(MessageB, 1) | |
223 ca.register(consumer)) | |
224 | |
225 other = ConsumerProducerPlugin() | |
226 other.listen(MessageB, 10) | |
227 ca.register(other) | |
228 | |
229 # this is the function call which gets the ball rolling | |
230 producer.dostuff() | |
231 | |
232 | |
233 DISCUSSION: blocking vs. non-blocking | |
234 ===================================== | |
235 | |
236 In the above example, I used "blocking" sends. However it is not-clear that this | |
237 is the best option. | |
238 | |
239 In the example, the producer basically acts as the main loop. It relinquishes | |
240 control of the main loop when the CA decides to forward the message to other | |
241 plugins. Control will only be returned once the cascade of send/receives | |
242 initiated with MessageA is complete (all subplugins have processed MessageA and | |
243 any messages sent as a side-effect have also been processed). | |
244 | |
245 This definitely imposes constraints on what the plugins can do, and how they do | |
246 it. For the type of single-processor / linear jobs we tend to run, this might be | |
247 enough (??). | |
248 | |
249 The good news is that going forward, the above plugin architecture can also | |
250 scale to distributed systems, by changing the sends to be non-blocking. Plugins | |
251 could then live on different machines and process data as they see fit. | |
252 Synchronization would be enforced by way of messages. In the above, the "main | |
253 producer" would thus become a consumer/producer who listens for "done processing | |
254 MessageA" messages and produces a new MessageA as a result. | |
255 | |
256 On single-processor systems, the synchronization overhead might be too costly | |
257 however. That is something we would have to investigate. On the plus side | |
258 however, our plugins would be "future proof" and lend themselves well to the | |
259 type of "massively parallel jobs" we wish to run (i.e. meta-learners, etc.) | |
260 | |
261 | |
262 | |
263 Logistic Regression | |
264 =================== | |
265 | |
266 | |
267 TO COME SOON (?) |