Event Notification pattern implemented

This patch adds an 'Event' class which may be used to issue various
notifications to other MuranoPl classes in an Event-driven manner.

Any object which is going to emit the notifications should declare the
instances of this new class as its public Runtime properties. The
objects going to subscribe for the notifications should pass
themselves into the 'subscribe' method of the Event along with the
names of their methods which will be used to handle the notification.

The specified handler methods must be present in the subscriber class
(if the method name is missing it will be defaulted to the
'handle%Eventname%') and has at least one standard (i.e. non-vararg
or kwarg) argument.

The class going to emit the notification should call the 'notify'
method of the event and pass itself as the first argument. All the
optional parameters of the event may be passed as varargs/kwargs of
the 'notify' call and will be passed all the way to the handler
methods.

Since this approach relies on the reflection this patch also fixes a
bug #1596647 since its fix is required for the argument reflection to
work properly. It also documents new reflection capabilities which
were added as part of the bugfix.

Targets-blueprint: application-development-framework
Closes-Bug: #1596647
Change-Id: Ifa7053e4c7b8456030e8df743f57ed812104b064
This commit is contained in:
Alexander Tivelkov 2016-06-20 23:01:30 +03:00
parent d9f0addfa0
commit e853cf861b
6 changed files with 424 additions and 5 deletions

View File

@ -306,10 +306,41 @@ following template::
Public is an optional parameter that specifies methods to be executed
by direct triggering after deployment.
.. _method_arguments:
Method arguments
++++++++++++++++
Arguments are optional too, and are declared using the same syntax
as class properties, except for the Usage attribute that is meaningless
for method parameters. For example, arguments also have a contract and
optional default::
as class properties. Same as properties, arguments also have contracts and
optional defaults.
Unlike class properties Arguments may have a different set of Usages:
.. list-table::
:header-rows: 1
:widths: 20 80
:stub-columns: 0
:class: borderless
* - | Value
- | Explanation
* - | Standard
- | Regular method argument. Holds a single value based on its contract.
This is the default value for the Usage key.
* - | VarArgs
- | A variable length argument. Method body sees it as a list of values,
each matching a contract of the argument.
* - | KwArgs
- | A keywrod-based argument, Method body sees it as a dict of values,
with keys being valid keyword strings and values matching a contract
of the argument.
Arguments example::
scaleRc:
Arguments:
@ -317,6 +348,17 @@ optional default::
Contract: $.string().notNull()
- newSize:
Contract: $.int().notNull()
- rest:
Contract: $.int()
Usage: VarArgs
- others:
Contract: $.int()
Usage: KwArgs
.. method_body:
Method body
+++++++++++
The Method body is an array of instructions that get executed sequentially.
There are 3 types of instructions that can be found in a workflow body:

View File

@ -248,14 +248,16 @@ Method arguments
- `True` if argument has default value, `False` otherwise
* - ``declaringMethod``
- method - owner of argument
* - ``usage``
- argument's usage type. See :ref:`method_arguments` for details
.. code-block:: yaml
- $firstArgument: $selectedMethod.arguments.first()
# store argument's name
- $argName = $firstArgument.name
- $argName: $firstArgument.name
# store owner's name
- $methodName = $firstArgument.declaringMethod.name
- $methodName: $firstArgument.declaringMethod.name
- $log.info("Hi, my name is {a_name} ! My owner is {m_name}",
a_name => $argName,
m_name => $methodName)

View File

@ -0,0 +1,104 @@
Namespaces:
=: io.murano.applications
std: io.murano
py: # empty, for python-originating exceptions
--- # ------------------------------------------------------------------ # ---
Name: Event
Properties:
name:
Contract: $.string().notNull()
Methods:
.init:
Body:
- $this._handlers: {}
subscribe:
Arguments:
- subscriber:
Contract: $.class(std:Object).notNull()
- methodName:
Contract: $.string()
Body:
- If: not $methodName
Then:
- $methodName: format('handle{0}', $this.name.substring(0,1).toUpper()+
$this.name.substring(1))
- Try:
- $method: typeinfo($subscriber).methods.where($.name = $methodName).single()
Catch:
With: py:StopIteration
Do:
- Throw: NoHandlerMethodException
Message: format('Unknown method {0} for
receiver {1} to handle event {2}',
$methodName, $subscriber, $this.name)
# This check ensures that the method passed as a handler has at least one
# standard (i.e. non vararg or kwarg) argument which is supposed to be
# "sender" object of the event.
# Although having the sender in the handler is not always nessesary it's
# still better to enforce its presence since it helps to prevent many
# hard-to-debug errors
- If: not $method.arguments.where($.usage=Standard).any()
Then:
- Throw: WrongHandlerMethodException
Message: format("Method {0} of handler {1} should accept at least
a 'sender' argument to handle event {2}",
$methodName, $subscriber, $this.name)
- $key: list($subscriber, $methodName)
- $this._handlers[$key]: $this._handlers.get($key, 0) + 1
unsubscribe:
Arguments:
- subscriber:
Contract: $.class(std:Object).notNull()
- methodName:
Contract: $.string()
Body:
- If: not $methodName
Then:
- $methodName: format('handle{0}', $this.name.substring(0,1).toUpper()+
$this.name.substring(1))
- $key: list($subscriber, $methodName)
- If: $key in $this._handlers.keys()
Then:
- $this._handlers[$key]: $this._handlers[$key] - 1
- If: $this._handlers[$key] = 0
Then:
- $this._handlers: $this._handlers.delete($key)
notify:
Arguments:
- sender:
Contract: $.notNull()
- args:
Contract: $
Usage: VarArgs
- kwargs:
Contract: $
Usage: KwArgs
Body:
- $combinedArgs: list($sender) + $args
- $this._handlers.keys().select(call($[1], $combinedArgs, $kwargs, $[0]))
notifyInParallel:
Arguments:
- sender:
Contract: $.notNull()
- args:
Contract: $
Usage: VarArgs
- kwargs:
Contract: $
Usage: KwArgs
Body:
- $combinedArgs: list($sender) + $args
- $this._handlers.keys().pselect(call($[1], $combinedArgs, $kwargs, $[0]))

View File

@ -0,0 +1,262 @@
Namespaces:
=: io.murano.applications.tests
tst: io.murano.test
apps: io.murano.applications
--- # ------------------------------------------------------------------ # ---
Name: TestSubscriber
Properties:
called:
Usage: Runtime
Default: 0
Contract: $.int()
lastSender:
Usage: Runtime
Contract: $
lastFoo:
Usage: Runtime
Contract: $
lastBar:
Usage: Runtime
Contract: $
Methods:
handleFoo:
Arguments:
- sender:
Contract: $.notNull()
- foo:
Contract: $
- bar:
Contract: $
Body:
- $this.called: $this.called + 1
- $this.lastFoo: $foo
- $this.lastBar: $bar
- $this.lastSender: $sender
handleWithNoExtraArgs:
Arguments:
- sender:
Contract: $.notNull()
Body:
- $this.called: $this.called + 1
- $this.lastSender: $sender
noArgsMethod:
Body:
varArgsKwArgsOnlyMethod:
Arguments:
- args:
Usage: VarArgs
Contract: $
- kwargs:
Usage: KwArgs
Contract: $
Body:
reset:
Body:
- $this.called: 0
- $this.lastSender: null
- $this.lastFoo: null
- $this.lastBar: null
--- # ------------------------------------------------------------------ # ---
Name: TestEmitter
Properties:
foo:
Usage: Runtime
Contract: $.class(apps:Event).notNull()
Default:
name: foo
Methods:
onFoo:
Body:
- $this.foo.notify($this)
--- # ------------------------------------------------------------------ # ---
Name: TestEvents
Extends: tst:TestFixture
Methods:
testSubscribeAndNotify:
Body:
- $subscriber: new(TestSubscriber)
- $event: new(apps:Event, name=>testEvent)
- $event.subscribe($subscriber, handleFoo)
- $event.notify($this, 'Hello Events', 42)
- $this.assertEqual(1, $subscriber.called)
- $this.assertEqual('Hello Events', $subscriber.lastFoo)
- $this.assertEqual(42, $subscriber.lastBar)
- $event.notify($this)
- $this.assertEqual(2, $subscriber.called)
testNotifyWithNoSubscribers:
Body:
- $event: new(apps:Event, name=>testEvent)
- $event.notify($this)
testUnableToNotifyWithUnexpectedArgs:
Body:
- $subscriber: new(TestSubscriber)
- $event: new(apps:Event, name=>testEvent)
- $event.subscribe($subscriber, handleFoo)
- $event.notify($this)
- $cought: false
- Try:
- $event.notify($this, qux=>1, baz=>2)
Catch:
With: 'yaql.language.exceptions.NoMatchingMethodException'
Do:
- $cought: true
- $this.assertTrue($cought)
testUnsubscribe:
Body:
- $subscriber: new(TestSubscriber)
- $event: new(apps:Event, name=>testEvent)
- $event.subscribe($subscriber, handleFoo)
- $event.notify($this)
- $this.assertEqual(1, $subscriber.called)
- $event.unsubscribe($subscriber, handleFoo)
- $event.notify($this)
- $this.assertEqual(1, $subscriber.called)
testSubscribeManyNotifyOnce:
Body:
- $subscriber: new(TestSubscriber)
- $event: new(apps:Event, name=>testEvent)
- $event.subscribe($subscriber, handleFoo)
- $event.subscribe($subscriber, handleFoo)
- $event.subscribe($subscriber, handleFoo)
- $event.subscribe($subscriber, handleFoo)
- $event.subscribe($subscriber, handleFoo)
- $event.notify($this)
- $this.assertEqual(1, $subscriber.called)
testUnsubscribeAsManyAsSubscribe:
Body:
- $subscriber: new(TestSubscriber)
- $event: new(apps:Event, name=>testEvent)
- $event.subscribe($subscriber, handleFoo)
- $event.subscribe($subscriber, handleFoo)
- $event.subscribe($subscriber, handleFoo)
- $event.notify($this)
- $this.assertEqual(1, $subscriber.called)
- $subscriber.reset()
- $event.unsubscribe($subscriber, handleFoo)
- $event.notify($this)
- $this.assertEqual(1, $subscriber.called)
- $subscriber.reset()
- $event.unsubscribe($subscriber, handleFoo)
- $event.notify($this)
- $this.assertEqual(1, $subscriber.called)
- $subscriber.reset()
- $event.unsubscribe($subscriber, handleFoo)
- $event.notify($this)
- $this.assertEqual(0, $subscriber.called)
testSubscribeSimple:
Body:
- $event: new(apps:Event, name=>foo)
- $subscriber: new(TestSubscriber)
- $event.subscribe($subscriber)
- $event.notify($this)
- $this.assertEqual(1, $subscriber.called)
testHandleWithNoExtraArgs:
Body:
- $event: new(apps:Event, name=>foo)
- $subscriber: new(TestSubscriber)
- $event.subscribe($subscriber, handleWithNoExtraArgs)
- $event.notify($this)
- $this.assertEqual(1, $subscriber.called)
- $this.assertEqual($this, $subscriber.lastSender)
testUnableToSubscribeWithWrongMethod:
Body:
- $event: new(apps:Event, name=>testEvent)
- $subscriber: new(TestSubscriber)
- $cought: false
- Try:
- $event.subscribe($subscriber, handleBar)
Catch:
With: apps:NoHandlerMethodException
Do:
- $cought: true
- $this.assertTrue($cought)
testUnableToSubscribeWithWrongSimpleMethod:
Body:
- $event: new(apps:Event, name=>testEvent)
- $subscriber: new(TestSubscriber)
- $cought: false
- Try:
- $event.subscribe($subscriber)
Catch:
With: apps:NoHandlerMethodException
Do:
- $cought: true
- $this.assertTrue($cought)
testUnableToSubscribeWithoutSender:
Body:
- $event: new(apps:Event, name=>testEvent)
- $subscriber: new(TestSubscriber)
- $cought: false
- Try:
- $event.subscribe($subscriber, noArgsMethod)
Catch:
With: apps:WrongHandlerMethodException
Do:
- $cought: true
- $this.assertTrue($cought)
testUnableToSubscribeWithoutStandardArgs:
Body:
- $event: new(apps:Event, name=>testEvent)
- $subscriber: new(TestSubscriber)
- $cought: false
- Try:
- $event.subscribe($subscriber, varArgsKwArgsOnlyMethod)
Catch:
With: apps:WrongHandlerMethodException
Do:
- $cought: true
- $this.assertTrue($cought)
testMultipleSubscribers:
Body:
- $subscriber1: new(TestSubscriber)
- $subscriber2: new(TestSubscriber)
- $event: new(apps:Event, name=>testEvent)
- $event.subscribe($subscriber1, handleFoo)
- $event.subscribe($subscriber2, handleFoo)
- $event.notify($this, 'Hello Events', 42)
- $this.assertEqual(1, $subscriber1.called)
- $this.assertEqual('Hello Events', $subscriber1.lastFoo)
- $this.assertEqual(42, $subscriber1.lastBar)
- $this.assertEqual(1, $subscriber2.called)
- $this.assertEqual('Hello Events', $subscriber2.lastFoo)
- $this.assertEqual(42, $subscriber2.lastBar)
testEmitterWithEvent:
Body:
- $emitter: new(TestEmitter)
- $subscriber: new(TestSubscriber)
- $emitter.foo.subscribe($subscriber)
- $emitter.onFoo()
- $this.assertEqual(1, $subscriber.called)
- $this.assertEqual($emitter, $subscriber.lastSender)

View File

@ -29,9 +29,11 @@ Classes:
io.murano.applications.BaseTemplateReplicaProvider: replication.yaml
io.murano.applications.TemplateReplicaProvider: replication.yaml
io.murano.applications.CloneReplicaProvider: replication.yaml
io.murano.applications.Event: events.yaml
io.murano.applications.tests.TestReplication: tests/TestReplication.yaml
io.murano.applications.tests.TestCloneReplicaProvider: tests/TestCloneReplicaProvider.yaml
io.murano.applications.tests.TestTemplateReplicaProvider: tests/TestTemplateReplicaProvider.yaml
io.murano.applications.tests.ReplicationTarget: tests/TestTemplateReplicaProvider.yaml
io.murano.applications.tests.NestedReplicationTarget: tests/TestTemplateReplicaProvider.yaml
io.murano.applications.tests.TestEvents: tests/TestEvents.yaml

View File

@ -194,6 +194,12 @@ def argument_has_default(method_argument):
return method_argument.has_default
@specs.yaql_property(dsl_types.MuranoMethodArgument)
@specs.name('usage')
def argument_usage(method_argument):
return method_argument.usage
@specs.yaql_property(dsl_types.MuranoMethodArgument)
@specs.name('declaring_method')
def argument_owner(method_argument):
@ -236,6 +242,7 @@ def register(context):
method_name, arguments, method_owner, method_invoke,
types, package_name, package_version,
argument_name, argument_has_default, argument_owner,
argument_usage,
cardinality, targets, inherited,
get_meta
)