Supporting constraint-maintaining subscriptions

Supporting constraint-maintaining subscriptions

Status: IsReplaced by ActionPlans

Author

JimFulton

Problem

Anthony Baxter wrote:

        At the moment if you try to delete a registered service or utility,
        you get a system error. The traceback shows a dependency error on
        the registration object. This could be handled by one of two ways:

        before deleting, auto-delete the registration(s)
         or
        make the delete code catch the dependency error and present a nice
        message. 

        Which is preferred? Or both?

We may need to maintain certain constraints among objects. For example, an object may depend upon another. We don't want to allow the object depended on to be deleted while there are dependents. The object depended on may not be aware of the constraint. We want to be able to express the constraints independently, so we implement the constraints via event subscribers. A subscriber can listen for events and "veto" an operation.

When we generate an event, there may be more than one subscriber that has an issue with it. There needs to be a way of resolving these issues and the code generating the event shouldn't be responsible for resolving the issues specifically, although there should be a general way for it to do so.

The thing that makes this hard is that we want to be able to collect multiple issues. Exceptions don't work very well for this, unless there is something willing to collect them together.

One idea that Steve and I came up with was to generate tentative events to collect issues. Something like:

       # publish a tentative event
       event = Tentative(ObjectRemovedEvent(ob))
       publish(event)
       if event,issues:
          # There were issues, do something
          ....

       # Now that we've dealt with the issues, publish the event
       publish(event.event)

If we want to raise issues, we create two subscribers. One subscriber subscribes to tentative events and one subscribes to normal events. The tentative event subscribers can add issues to the event to be resolved:

       def notify_tentative_remove(tentative_event):
          real_event = tentative_event.event
          if has_dependents(event.object):
              tentative_event.addIssue(DependencyIssue(event.object))

This seems rather complicated for the code that generates events, especially since, I suppose, it could potentially apply to most events.

Another possibility is to provide a specialized event channel that can help with the tentative events. The application code would just publish the normal event. The channel gets the event and sends a tentative event to its subscribers. If there are no issues it sends the regular event.

If there are issues, the event channel could just raise a special exception (e.g. EventRejected?). The exception could contain the issues. At that point, the application code can catch the exception and try to deal with the issues. How would the application code deal with the issues? Well, in a number of ways. First, let's assume that EventRejected? exceptions have event and issues attributes. The issues attribute is an iterable of issues.

We can suppose some properties of issues:

  • We can display issues by displaying views of them
  • Some issues might have (or be adaptable to objects that have) a resolve method. If we have a resolve method, we can call it to resolve the issue.
  • Some issues might require confirmation. If this is the case, the user should be asked to confirm the action necessary to resolve the issue. If this is the case, the application should display the issue to the user and get him to confirm it before calling the fixup method.
  • Some issues may be unresolvable. I'm not sure we should allow this. Perhaps this could be useful in the case of issues that can't be simply resolved but that could be resolved through some more involved action by the user. This is a little bit risky, as you can end up in a position in which you can't resolve an issue and can't make progress.

If all of the issues are resolvable without confirmation, then the event channel can just resolve them. I suppose that if one of the events are not resolvable, the channel could raise a different exception (e.g. EventError?) that isn't caught by the app. In that case, the app only needs to worry about confirmation.

So, careful application code might look something like this:

        try:
           publish(ObjectRemovedEvent(ob))
        except EventRejected, exc:
           # We have some issues that can be resolved, but these
           # need to be confirmed.  We'll check to see if the user has
           # confirmed the issues.  We'll call a helper view that checks
           # this for us:
           if getViewProviding(exc, request, IVerifyConfirmation):
               # The user has said OK, do it. Publish a confirmed event:
               publish(ConfirmedEvent(ObjectRemovedEvent(ob), exc.issues)
           else:
               # Raise a confirmation exception that, when viewed, displays
               # a confirmation message and returns to the request URL.
               raise ConfirmationRequired(exc)

The ConfirmationRequired? exception has a view that:

  • Displays the issues and a confirmation button
  • Includes hidden fields that identify the issue. This is needed to make sure that we have confirmed all of the same issues when the application is reexecuted.

The IVerifyConfirmation? view compares the information in the hidden request data (setup by the ConfirmationRequired? view) with the issues in the exception.to make sure that all of the issues in the esception have been confirmed.

When the event channnel gets a ConfirmedEvent?, it:

  • Sends a tentative event to its subscribers, and collects all of the issues.
  • Compares the issues collected with the issues passed to it.
    • If the issues are the same, it knows that they have been confirmed and calls resolve on each of them
    • Otherwise, it raises EventRejected?.

If the application doesn't catch the exception, then the exception will be displayed to the user. In doing so it will display the issues. It will display the issues with a different view than the one used for confirmations. The issue views could provide UIs? or links to UIs? for resolving the issues. In this way, even if the application doesn't handle the issues, the issues could lead the user to take actions that will resolve the issues. Having done so, the user can then redo the application action, which should succeed.

A variation on the tentative subscription handling is to have tentative subscribers, rather thah tentative events. The event channel will first get tentative subscribers for the events. This is in contrast with getting regular subscribers for tentative events. This second alternative is a bit simpler and enabled by the fact that we now have subscription adapters that allow different notification interfaces.

Proposal

Implement the framework described above. :)



( 97 subscribers )