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:
parent
d9f0addfa0
commit
e853cf861b
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]))
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue