Skip to content

Implementation of a Service Oriented Architecture Façade

Brian Weir edited this page May 24, 2023 · 3 revisions

Introduction

Given a complex subsystem like the UFS, it can be made easier to use by implementing a simple interface. To better handle the stages of such a complex system, rather than firmly wrapping the entire system, we can handle the various services through a series of drivers underneath this interface. This also allows us to access these drivers individually to support power users who may need more options than the interface might provide. To fulfill these intents, we propose a service-oriented architecture façade design pattern to provide a unified interface and to simplify the communication between components.

Current Configuration Layers

Currently, the apps within the Unified Forecast System (UFS) require direct user control of each run stage and specific settings of the configuration files. A summary of these stages for tier 1 supported machines is as below:

SRW Global Workflow HAFS

Run devbuild.sh or cmake

activate regional_workflow

modify config.yaml [set user, machine, workflow, tasks]

run generate_FV3LAM_wflow.py [runs setup.py, reads config_defaults.yaml, config.yaml & set_predef_grid_param.py]

run launch_FV3LAM_wflow.sh

under /sorc, run build_all.sh

Create necessary symlinks by running link_workflow.sh

Set up initial conditions file

run ./setup_expt.py with parameters

Usage: ./setup_expt.py forecast-only --pslot test --app ATM --idate 2020010100 --edate 2020010100 --resdet 384 --gfs_cyc 1 --comrot /home/$USER/COMROT --expdir /home/$USER/EXPDIR

go to EXPDIR and ensure config.base variables are correct

run workflow generator to create the ROCOTO configurations ./setup_xml.py $EXPDIR/$PSLOT

start run with rocotorun -d $PSLOT.db -w $PSLOT.xml

set up crontab

Build and install components with /sorc/install_hafs.sh or /sorc/install_all.sh

Edit config files: /parm/hafs_input.conf /parm/hafs.conf /parm/hafs_holdvars.conf /parm/hafs_basic.conf

Start run with /scripts/exhafs_launch.py

Usage: exhafs_launch.py 2014062400 95E case_root /path/to/parm [options]

Mandatory arguments: 2014062400 -- the cycle to run 95E -- storm id case_root -- FORECAST = real-time mode, HISTORY = retrospective mod /path/to/parm -- location of parm directory where standard conf files reside

Despite the variations, all three apps can be broken down into generic tasks:

  1. Build the app
  2. Define a configuration that includes the experiment parameters, its input files as well as its output
  3. Submit the HPC job

As these are similar between the apps in terms of user intent, they vary notably in required interaction, which is where we can see the benefits of the façade pattern. For the purposes of this façade, we will focus on the configuration step to maximize the benefit to end users.

Structure of a Façade Pattern Implementation

Below is a pseudocode example of what the scaffolding of a façade implementation might look like, referenced from Refactoring Guru. Here, the sub-tasks involved in the configuration generic task above can be placed as the subsystem blocks below:

from __future__ import annotations
 
 
class Facade:
    """
    The Facade class provides a simple interface to the complex logic of one or
    several subsystems. The Facade delegates the client requests to the
    appropriate objects within the subsystem. The Facade is also responsible for
    managing their lifecycle. All of this shields the client from the undesired
    complexity of the subsystem.
    """
 
    def __init__(self, subsystem1: Subsystem1, subsystem2: Subsystem2) -> None:
        """
        Depending on your application's needs, you can provide the Facade with
        existing subsystem objects or force the Facade to create them on its
        own.
        """
 
        self._subsystem1 = subsystem1 or Subsystem1()
        self._subsystem2 = subsystem2 or Subsystem2()
 
    def operation(self) -> str:
        """
        The Facade's methods are convenient shortcuts to the sophisticated
        functionality of the subsystems. However, clients get only to a fraction
        of a subsystem's capabilities.
        """
 
        results = []
        results.append("Facade initializes subsystems:")
        results.append(self._subsystem1.operation1())
        results.append(self._subsystem2.operation1())
        results.append("Facade orders subsystems to perform the action:")
        results.append(self._subsystem1.operation_n())
        results.append(self._subsystem2.operation_z())
        return "\n".join(results)
 
 
class Subsystem1:
    """
    The Subsystem can accept requests either from the facade or client directly.
    In any case, to the Subsystem, the Facade is yet another client, and it's
    not a part of the Subsystem.
    """
 
    def operation1(self) -> str:
        return "Subsystem1: Ready!"
 
    # ...
 
    def operation_n(self) -> str:
        return "Subsystem1: Go!"
 
 
class Subsystem2:
    """
    Some facades can work with multiple subsystems at the same time.
    """
 
    def operation1(self) -> str:
        return "Subsystem2: Get ready!"
 
    # ...
 
    def operation_z(self) -> str:
        return "Subsystem2: Fire!"
 
 
def client_code(facade: Facade) -> None:
    """
    The client code works with complex subsystems through a simple interface
    provided by the Facade. When a facade manages the lifecycle of the
    subsystem, the client might not even know about the existence of the
    subsystem. This approach lets you keep the complexity under control.
    """
 
    print(facade.operation(), end="")
 
 
if __name__ == "__main__":
    # The client code may have some of the subsystem's objects already created.
    # In this case, it might be worthwhile to initialize the Facade with these
    # objects instead of letting the Facade create new instances.
    subsystem1 = Subsystem1()
    subsystem2 = Subsystem2()
    facade = Facade(subsystem1, subsystem2)
    client_code(facade)

Implementation

The façade interface implementation itself sits one abstracted level above the experiment configuration layer and can fully control apps. Additionally, UW methods within the façade such as set_config can be exposed for individual apps to implement on their own.

The façade 'experiment_manager.py' can be called to handle the interfacing to the config subsystems of each app. When a user provides input about which app and the experiment configuration for that app, the façade will translate the input information and call the appropriate configuration subsystem to set up the experiment as configured.

This will be added in a series of development stages:

  1. First, the base structure of the façade will call the configuration subsystem of the given App directly 
  2. Next, the existing App configuration subsystem will be reduced in scope as UW Components are switched on to perform equivalent tasks. The tasks to be covered by this façade include:
    1. load config
    2. validate config
    3. create experiment directory
    4. create workflow manager files
    5. link fix files
  3. Over time, as new component drivers are added, they will be called upon to handle the validation of the user config that falls within their scope of a subcomponent.

Discussion and Feedback

Discussion and feedback pages for the wiki can be found here

Further Reading

https://github.com/ufs-community/workflow-tools/wiki/Migrating-Production-Workflows-to-the-Unified-Workflow-using-the-Strangler-Fig-Pattern

References

[^1]: Shvets, O. Facade. refactoring.guru. Retrieved May 24, 2023, from https://refactoring.guru/design-patterns/facade