async def handle_resource_watching_cause( lifecycle: lifecycles.LifeCycleFn, registry: registries.OperatorRegistry, memory: containers.ResourceMemory, cause: causation.ResourceWatchingCause, ) -> None: """ Handle a received event, log but ignore all errors. This is a lightweight version of the cause handling, but for the raw events, without any progress persistence. Multi-step calls are also not supported. If the handler fails, it fails and is never retried. Note: K8s-event posting is skipped for `kopf.on.event` handlers, as they should be silent. Still, the messages are logged normally. """ handlers = registry.get_resource_watching_handlers(cause=cause) outcomes = await _execute_handlers( lifecycle=lifecycle, handlers=handlers, cause=cause, state=states.State.from_scratch(handlers=handlers), default_errors=registries.ErrorsMode.IGNORED, ) # Store the results, but not the handlers' progress. states.deliver_results(outcomes=outcomes, patch=cause.patch)
async def execute( *, fns: Optional[Iterable[invocation.Invokable]] = None, handlers: Optional[Iterable[handlers_.ResourceHandler]] = None, registry: Optional[registries.ResourceChangingRegistry] = None, lifecycle: Optional[lifecycles.LifeCycleFn] = None, cause: Optional[causation.BaseCause] = None, ) -> None: """ Execute the handlers in an isolated lifecycle. This function is just a public wrapper for `execute` with multiple ways to specify the handlers: either as the raw functions, or as the pre-created handlers, or as a registry (as used in the object handling). If no explicit functions or handlers or registry are passed, the sub-handlers of the current handler are assumed, as accumulated in the per-handler registry with ``@kopf.on.this``. If the call to this method for the sub-handlers is not done explicitly in the handler, it is done implicitly after the handler is exited. One way or another, it is executed for the sub-handlers. """ # Restore the current context as set in the handler execution cycle. lifecycle = lifecycle if lifecycle is not None else sublifecycle_var.get() cause = cause if cause is not None else cause_var.get() parent_handler: handlers_.BaseHandler = handler_var.get() parent_prefix = parent_handler.id if parent_handler is not None else None # Validate the inputs; the function signatures cannot put these kind of restrictions, so we do. if len([v for v in [fns, handlers, registry] if v is not None]) > 1: raise TypeError( "Only one of the fns, handlers, registry can be passed. Got more.") elif fns is not None and isinstance(fns, collections.abc.Mapping): subregistry = registries.ResourceChangingRegistry(prefix=parent_prefix) for id, fn in fns.items(): subregistry.register(fn=fn, id=id) elif fns is not None and isinstance(fns, collections.abc.Iterable): subregistry = registries.ResourceChangingRegistry(prefix=parent_prefix) for fn in fns: subregistry.register(fn=fn) elif fns is not None: raise ValueError( f"fns must be a mapping or an iterable, got {fns.__class__}.") elif handlers is not None: subregistry = registries.ResourceChangingRegistry(prefix=parent_prefix) for handler in handlers: subregistry.append(handler=handler) # Use the registry as is; assume that the caller knows what they do. elif registry is not None: subregistry = registry # Prevent double implicit execution. elif subexecuted_var.get(): return # If no explicit args were passed, implicitly use the accumulated handlers from `@kopf.on.this`. else: subexecuted_var.set(True) subregistry = subregistry_var.get() # The sub-handlers are only for upper-level causes, not for lower-level events. if not isinstance(cause, causation.ResourceChangingCause): raise RuntimeError( "Sub-handlers of event-handlers are not supported and have " "no practical use (there are no retries or state tracking).") # Execute the real handlers (all or few or one of them, as per the lifecycle). subhandlers = subregistry.get_handlers(cause=cause) state = states.State.from_body(body=cause.body, handlers=subhandlers) outcomes = await execute_handlers_once( lifecycle=lifecycle, handlers=subhandlers, cause=cause, state=state, ) state = state.with_outcomes(outcomes) state.store(patch=cause.patch) states.deliver_results(outcomes=outcomes, patch=cause.patch) # Escalate `HandlerChildrenRetry` if the execute should be continued on the next iteration. if not state.done: raise HandlerChildrenRetry(delay=state.delay)
def test_store_result(handler, expected_patch, result): patch = {} outcomes = {handler.id: HandlerOutcome(final=True, result=result)} deliver_results(outcomes=outcomes, patch=patch) assert patch == expected_patch
async def handle_resource_changing_cause( lifecycle: lifecycles.LifeCycleFn, registry: registries.OperatorRegistry, memory: containers.ResourceMemory, cause: causation.ResourceChangingCause, ) -> Optional[float]: """ Handle a detected cause, as part of the bigger handler routine. """ logger = cause.logger patch = cause.patch # TODO get rid of this alias body = cause.body # TODO get rid of this alias delay = None done = None skip = None # Regular causes invoke the handlers. if cause.reason in causation.HANDLER_REASONS: title = causation.TITLES.get(cause.reason, repr(cause.reason)) logger.debug(f"{title.capitalize()} event: %r", body) if cause.diff and cause.old is not None and cause.new is not None: logger.debug(f"{title.capitalize()} diff: %r", cause.diff) handlers = registry.get_resource_changing_handlers(cause=cause) state = states.State.from_body(body=cause.body, handlers=handlers) if handlers: outcomes = await _execute_handlers( lifecycle=lifecycle, handlers=handlers, cause=cause, state=state, ) state = state.with_outcomes(outcomes) state.store(patch=cause.patch) states.deliver_results(outcomes=outcomes, patch=cause.patch) if state.done: logger.info(f"All handlers succeeded for {title}.") state.purge(patch=cause.patch, body=cause.body) done = state.done delay = state.delay else: skip = True # Regular causes also do some implicit post-handling when all handlers are done. if done or skip: extra_fields = registry.get_extra_fields(resource=cause.resource) lastseen.refresh_essence(body=body, patch=patch, extra_fields=extra_fields) if cause.reason == causation.Reason.DELETE: logger.debug("Removing the finalizer, thus allowing the actual deletion.") finalizers.remove_finalizers(body=body, patch=patch) # Once all handlers have succeeded at least once for any reason, or if there were none, # prevent further resume-handlers (which otherwise happens on each watch-stream re-listing). memory.fully_handled_once = True # Informational causes just print the log lines. if cause.reason == causation.Reason.GONE: logger.debug("Deleted, really deleted, and we are notified.") if cause.reason == causation.Reason.FREE: logger.debug("Deletion event, but we are done with it, and we do not care.") if cause.reason == causation.Reason.NOOP: logger.debug("Something has changed, but we are not interested (the essence is the same).") # For the case of a newly created object, or one that doesn't have the correct # finalizers, lock it to this operator. Not all newly created objects will # produce an 'ACQUIRE' causation event. This only happens when there are # mandatory deletion handlers registered for the given object, i.e. if finalizers # are required. if cause.reason == causation.Reason.ACQUIRE: logger.debug("Adding the finalizer, thus preventing the actual deletion.") finalizers.append_finalizers(body=body, patch=patch) # Remove finalizers from an object, since the object currently has finalizers, but # shouldn't, thus releasing the locking of the object to this operator. if cause.reason == causation.Reason.RELEASE: logger.debug("Removing the finalizer, as there are no handlers requiring it.") finalizers.remove_finalizers(body=body, patch=patch) # The delay is then consumed by the main handling routine (in different ways). return delay