← Back to team overview

launchpad-dev team mailing list archive

New services infrastructure

 

Hi

I want to share some work that's being done to support some of the new
disclosure/sharing functionality. I've been doing some prototyping and
it's at the point where I think it may turn out to be a viable option to
move forward with. The info that follows will be a little raw but
hopefully interesting enough for those who care to put in their 2c worth.

tl;dr;
It's possible to easily implement a named service which exports methods
to the api and which can provide json data to the client for rendering.
Our standard lazr.restful library is used, but we are not attaching
service methods to domain objects which I believe is the wrong approach.
This new implementation is more in line with a SOA approach which we are
looking to adopt and allows the launchpadlib users and browser clients
to access flattened data via the same api.

Many of you will be aware from my past ramblings that one of the big
issues I have about how Launchpad hangs together is that we have
conflated domain objects with remoting abstractions (the same mistake
that EJB made in version 1). This is wrong for so many reasons; to try
and keep this email concise, buy me a beer if you want to hear my
extended thoughts in this area. This work also lends itself to a POPO
based approach for modelling our business domain objects.

Warning - pseudo code included below.

So we have a new sharing/permissions model and want to (for example)
create an an observer for a product. The current or "wrong" way would be:

IProduct
  @export_write_operation()
  def addObserver(person)

But distributions can also have observers added and so we would need to
add the same method to IDistribution as well. And the consumers of these
apis would need to have this distinction coded in and.....

The new way - define an AccessPolicyService:

** Defining and registering a Service **
----------------------------------------

class IAccessPolicyService(IService):

    export_as_webservice_entry(publish_web_link=False, as_of='beta')

    @export_write_operation()
    @call_with(user=REQUEST_USER)
    @operation_parameters(
        pillar=Reference(IPillar, title=_('Pillar'), required=True),
        observer=Reference(IPerson, title=_('Observer'), required=True),
        access_policy_type=Choice(vocabulary=AccessPolicyType))
    @operation_for_version('devel')
    def addPillarObserver(pillar, observer, access_policy_type, user):
        """Add an observer with the access policy to a pillar."""

The addPillarObserver method returns json data which representing the
result of the operation, sufficient to allow (for example) the view to
be updated in the appropriate way. For the +sharing view, that would be
to update the json request cache and add a new row to the observer table.

To expose the service, simply register it as a utility in zcml with a name:

<securedutility
    name="accesspolicy"
    class="lp.registry.services.accesspolicyservice.AccessPolicyService"
    provides="lp.app.interfaces.services.IService">
    <allow
interface="lp.registry.interfaces.accesspolicyservice.IAccessPolicyService"/>
</securedutility>

Services are traversed to via +services/<servicename>


** Service Invocation **
------------------------
There's 3 (consistent) ways to invoke the service apis.

1. Server side

service = getUtility(IService, 'accesspolicy')
service.addPillarObserver(....)

2. launchpadlib api

from launchpadlib.launchpad import Launchpad
lp = Launchpad.login_with('testing', version='devel')
# Launchpadlib can't do relative url's
service = self.launchpad.load(
    '%s/+services/accesspolicy' % self.launchpad._root_uri)
service.addPillarObserver(....)

3. javascript client

var lp_client = new Y.lp.client.Launchpad();
lp_client').named_post(
    '/+services/accesspolicy',
    'addPillarObserver', y_config);


** View rendering/form submission **
------------------------------------

The new +sharing page does not use any server side rendering. It has a
bit of TAL used for the page chrome but the business data is rendered in
the browser using mustache (and soon handlebars). Data for the view
model is obtained via a getPillarObservers() method on the service.
This is invoked by the LaunchpadView instance for +sharing and poked
into the json request cache, from where it is accessed when the YUI view
widget renders. The point here is that the exact same
getPillarObservers() api could be used by a launchpadlib client to get
json data for use in a script or tool or whatever.

eg the view initialize() method

def initialize(self):
    super(ProductSharingView, self).initialize()
    cache = IJSONRequestCache(self.request)
    cache.objects['access_policies'] = self.access_policies
    cache.objects['sharing_permissions'] = self.sharing_permissions
    cache.objects['observer_data'] = self.observer_data

where, for example:

def _getAccessPolicyService(self):
    return getUtility(IService, 'accesspolicy')

@property
def observer_data(self):
    service = self._getAccessPolicyService()
    return service.getPillarObservers(self.context)


When it comes time to write data from the view back to the server,
either to initiate an action or save a form or whatever, you can either:

i. use our standard LaunchpadFormView and associated infrastructure and
delegate the submit processing to an instance of the service via
invocation method 1 above, or

ii. invoke the service directly via an XHR call using lp client named_post


In conclusion, the approach outlined in this email is what we are using
to prototype the +sharing view for the disclosure project. So far, it's
working very nicely. One key advantage of this approach over just adding
methods to domain objects is that a service contract often will call for
aggregated and/or flattened data to be returned to a caller (eg view or
api user) and this requires gathering data from several places. So the
service acts as a fascade, allowing this to happen in a consistent,
single sourced way for the different consumers of that data. As I said
earlier, those unfortunate enough to hear me bang on previously about
this approach know I'm a fan of it and this
lib/lp/registry/interfaces/accesspolicyservice.py
greenfields disclosure work has finally provided an opportunity to allow
us/me to step outside the normal bounds of how we have previously done
things in Launchpad to do something new.

If you have any feedback, good or bad, please share. The approach is
being evaluated (criteria TBA) and we need to look at how we measure
it's success or otherwise and whether we want to continue down this road
or revert to a more traditional approach etc. If you are interested in
the core infrastructure (look for IService, ServiceFactory and friends):

  lib/lp/app/services.py
  lib/lp/app/browser/launchpad.py
  lib/lp/app/interfaces/services.py
  lib/lp/app/tests/test_services.py
  lib/lp/registry/configure.zcml
  lib/lp/registry/services/
  lib/lp/registry/browser/configure.zcml
  lib/lp/registry/interfaces/webservice.py
  lib/lp/registry/services/configure.zcml
  lib/lp/services/webservice/services.py

I've not landed the bespoke +sharing code yet which sits on top of all
this - everything works nicely but I want to put it behind a feature
flag etc.









Follow ups