Modularization - uyuni-project/uyuni GitHub Wiki

Motivation

Modularization of the Java codebase is driven by the following aspects:

  • Testability: make it easy to test individual components of Uyuni in isolation without a full multi-VM setup running.
  • Understandability: make the APIs and components clear so it is possible to understand how things are supposed to work without the need to look at its implementation and all its interactions.
  • Reusability: decouple components as much as possible to the point where they can be easily reused as a library or run standalone.

The goal of those aspects is to make working with the codebase more productive. Each area suggested below can be tackled individually and the refactoring can be done gradually in multiple repeatable steps. After each step the code should be compiling with all tests still passing.

Preparation

  • Pick one of the areas like Salt, filesystem, database, task scheduling or different topics of business logic like CVE audit, etc.
  • The first step is to gather all the requirements Uyuni has towards this area. This means finding all operations Uyuni currently needs from i.e salt. From this set of operations we create an interface.
  • Next step is to implement this interface with the existing code we already have for those operations. This can be done by moving code or delegating in case moving requires a lot of extra refactorings. Note: only put methods in the interface that are actually used outside the implementation of the interface. Everything else will be private and will be considered an implementation detail.
  • Then we need to point existing use sites of those operations to use this interface.
  • At this point we should have a pretty good overview of what set of operations we have and we have centralized them in an interface that is now used. We should be in a state where everything works as before.

Refinement

  • Review the interface by consolidation of all the operations, dropping possible duplicates or deprecated functionality, merge similar methods etc.
  • Refine the interface with more precise types, consistent method names, signatures and error handling. (in case of salt there are some wildcard methods that let you do anything which leads to people putting fragile parsing logic inside business logic)
  • Make batch operations the default and single target a special case. This is so implementations can be done with scalability in mind.
  • Merge related existing implementations across the codebase to implement the interface
  • Make sure the interface does not imply a specific implementation (i.e don't reference salt if all you do is gathering information from a system.)
  • Refactor other components to take an implementation of this interface as an argument instead of reaching into global space.
  • Write new and reuse existing tests for the interface
  • Move interface with related datatypes and implementations into package with fitting name for the component.

Interface Guidelines

  • It should be focused on a particular area and not mix different topics.
  • It should be minimal meaning only the minimal set of methods needed by our business logic.
  • It should give immediately useful information and not burden the user to do much additional work to get to the useful information (i.e don't return plain json and let business logic parse it).
  • It should be precise in its types and minimize invalid usage.
  • Do not be scared to define multiple interface to separate areas properly and let them build on top of each other.

Bootstrapping

Bootstrapping is about creating instances of the different "services" and assembling them to then pass them on into our business logic. The assembly should happen at the very start and then everything below that will only get things passed down but not instanciate any services themselves. The start in this case is multiple places: tomcat and taskomatic are different processes and have their own entry points so both need to have some bootstrapping code. In tomcat the situation is maybe slightly more complicated since our business logic lives below filters which are instanciated by tomcat logic based on a configuration file (xml format).

  • One way to go could be looking into programmatic initialization and registration of filters.
  • Use some limited shared global state just to pass the initial instances between entry points.
  • In the worst case duplicating instances per filter so spark and xmlrpc would not share the same instances.

Initial Example

There is an initial example extracting most of the interactions with Salt into an interface. It's not complete yet as it lacks more refinement, e.g. getting rid of JsonElement in results or splitting it up into more topical interface (e.g. having libvirt interaction in its own interface). But it should give a general idea of how this approach looks applied to our code.

Drawbacks

  • Suggested refactorings may not always be trivial and can lead to short term breakage.
  • Measures will have to be found to ensure established interfaces will continue to be respected.