# What is Zephyr?
Zephyr is a platform for building and testing modular applications. A modular application is an application whose functionality may dynamically be extended by modules. But what's a module?
Let's start off with a few examples of modular applications:
- Wordpress (opens new window)
- Visual Studio (opens new window)
- Intellij (opens new window)
- Gimp (opens new window)
The list goes on. It turns out that application modularity is a powerful capability. Unfortunately, building modular applications can be difficult: developers are left to either create their own module framework or use an existing module framework such as:
- An OSGi implementation Eclipse Equinox (opens new window) or Felix (opens new window)
- PF4j (opens new window)
- Other platform or language-specific module frameworks such as DLLs/Shared Objects
Often, solutions that work for one person's use-cases won't work for someone else's.
# Why another framework?
Zephyr was born from our frustrations with trying to wrangle many dynamic dependencies here at Sunshower.io (opens new window). Initially, we had just a few modules, and that worked well. As the number and complexity of our modules increased, so did the amount of time we spent fixing issues with the various modular frameworks that we'd attempted to use. Eventually, we'd come to realize that many existing solutions solve for problems that we did not have without providing clean solutions for the problems that we did:
Feature | Zephyr | Competitors |
---|---|---|
Thread-safety | Yes | No |
Sharing | Open by default | Isolated by default |
Topological Lifecycle Ordering | Yes | No |
Integrated Testing Support | Yes | No |
Integrated SPA UI Support | Yes | No |
Concurrent Module Lifecycle | Yes | No |
Package Format | Extensible | Fixed |
Global Framework Support | Supported | Unsupported |
Obviously, some frameworks will adopt one feature or convention whereas others won't, but these are broadly the themes that we encountered before developing Zephyr. Let's go over each!
# Thread-Safety
A thread-safe system is one that you can interact with concurrently such that its behavior is always well-defined. Thread-safety is a large and complex issue, and understandably, many existing systems don't attempt to address it: interact with the system from one thread or you're taking your life into your own hands! Zephyr takes the opposite approach: it should Just WorkTM whatever concurrency model you're dealing with.
A good example (and the one that users will probably use the most) is the ModuleContext
API:
public interface ModuleContext extends VolatileStorage {
<T> T unwrap(Class<T> type);
<T> Predicate<T> createFilter(Query<T> query);
<T> CapabilityRegistration<T> provide(CapabilityDefinition<T> capability);
<T> RequirementRegistration<T> createRequirement(Requirement<T> requirement);
Module getModule();
List<Module> getModules(Predicate<Module> filter);
ModuleTracker trackModules(Predicate<Module> filter);
ModuleTracker trackModules(Query<Module> filter);
ServiceTracker trackServices(Query<ServiceReference<?>> filter);
ServiceTracker trackServices(Predicate<ServiceReference<?>> filter);
<T> ServiceRegistration<T> register(ServiceDefinition<T> definition);
<T> ServiceRegistration<T> register(Class<T> type, String name, T value);
<T> ServiceRegistration<T> register(Class<T> type, T value);
<T> ServiceRegistration<T> register(Class<T> type, String name, Supplier<T> factory);
<T> ServiceRegistration<T> register(Class<T> type, Supplier<T> factory);
<T> List<ServiceReference<T>> getReferences(Class<T> type);
List<ServiceReference<?>> getReferences(Query<ServiceDefinition<?>> query);
}
An implementation of this interface is passed into each ModuleActivator
provided by each plugin
(note: Modules may supply as many ModuleActivators
as they like). Modules may retain strong
references to this activator, or any objects supplied by it without creating a memory-leak, including services
supplied by dependent modules.
# Gyre
In addition to supporting your concurrency model (whatever that may be), Zephyr also provides its own called Gyre which provides graph-oriented concurrency (opens new window) (contrasted with the event-oriented models that are currently quite fashionable these days). The Graph model allows us to model our computations as graphs (opens new window). When a graph is acyclic, then a topological order can be computed. From this topological order we can compute a parallel schedule (opens new window). A parallel schedule allows us to determine which tasks may be executed concurrently. This is the default approach that Zephyr takes.
# Sharing
As libraries and platforms have matured, the risk of incompatibilities between versions has diminished. This is reflected in the rise of concepts such as Semantic Versioning (opens new window) wherein dependents express valid version ranges of dependencies and expect a compatible version to be provided.
Moreover, most applications are not modular as modularity can increase complexity--sometimes dramatically. Moreover, the advent of distributed microservices solves the isolation problem more completely than a single-process environment ever can.
In order to simplify the development of, or transition to, modular applications from monolithic applications, Zephyr is permissive about classpath and resource sharing while providing powerful functionality to restrict sharing when it is detrimental.
# Topological Lifecycle Ordering
One of the principle challenges in managing dependencies is determining when a dependency remains valid.
In OSGi, for instance, you must track your references and bundles manually and determine if any are stale
(i.e. resolved from a bundle that has been stopped/unloaded/etc.). Zephyr models modules as a graph of
dependencies: if a mandatory dependency is stopped, its dependents will be stopped as well.
If an optional dependency is stopped, service-references become unavailable.
Lifecycle operations such as start
, stop
, or restart
are propagated in their correct order throughout
the module graph without developers needing to concern themselves with handling them: each module
may safely handle its own lifecycle without worrying about the lifecycle of its dependents or dependencies.
# Integrated Testing Support
Testing modular applications has always proved challenging. Zephyr provides powerful testing support in the form of JUnit 5 extensions, allowing you to write declarative, simple, reliable module tests.
Sample Declarative Integration Test
@ZephyrTest
@Modules({
/**
* install project module
* can also install modules from Maven, Gradle, Ivy, Grape, etc.
*/
@Module(project = "kernel-tests:test-plugins:test-plugin-spring"),
/**
* install kernel modules
*/
@Module(project = "kernel-modules:sunshower-yaml-reader", type = Module.Type.KernelModule)
})
/**
* specify module resource behavior
*/
@Clean(value = Clean.Mode.Before, context = Clean.Context.Method)
class KernelRegistryMementoSystemTest {
/**
* inject any platform dependencies
*/
@Inject
private Kernel kernel;
@Inject
private Zephyr zephyr;
@Inject
private ModuleManager moduleManager;
@Test
@Clean(mode = Clean.Mode.After) / clean up after a test
void ensurePluginIsRestartedWhenKernelIsRestarted() throws Exception {
zephyr.start("io.sunshower.spring:spring-plugin:1.0.0"); / start a module
assertEquals(
moduleManager.getModules(Lifecycle.State.Active).size(), 1, "must have one active plugin");
kernel.persistState().toCompletableFuture().get();
kernel.stop();
assertTrue(
moduleManager.getModules(Lifecycle.State.Active).isEmpty(), "kernel was stopped jfc");
kernel.start();
kernel.restoreState().toCompletableFuture().get();
assertEquals(
moduleManager.getModules(Lifecycle.State.Active).size(), 1, "kernel was started jfc");
}
/etc.
}
Subsequent sections will cover testing in much greater detail, but hopefully this will whet your appetite for the types of tests that can be written for Zephyr!
# Integrated SPA UI Support
It has traditionally been exceedingly difficult to build modular applications that support dynamic user interfaces, particularly as rich, single-page web applications (opens new window).
Zephyr supports modular single-page applications via its Aire Widget Toolkit (opens new window), allowing modules to easily add, remove, or augment user-applications. Modular SPAs are simple to test, apply feature-flags to, etc.
SPA UI Unit Test
@AireTest
@Routes(scanPackage = "com.aire.ux.test.vaadin.scenarios.routes")
class ComponentHierarchyNodeAdapterTest {
@ViewTest
@Navigate("main")
void ensureCssSelectorsWork(@Select("body > section") MainLayout layout) { /select a page component by css selectors
assertNotNull(layout);
}
@ViewTest
@Navigate("main")
void ensureCssSelectorWorksOnCollectionTypeOfElements(
@Select("body > section span") List<Element> children) { / select elements
assertNotNull(children);
assertEquals(2, children.size());
assertTrue(children.stream().allMatch(t -> t.getTag().equals("span")));
}
}
# Concurrent Module Lifecycle
Startup times for modular applications can become prohibitive as the number of modules increases. Zephyr automatically computes a fully-parallel module lifecycle schedule for each application deployed into it, and will safely and transparently manage their lifecycles concurrently. One Zephyr user with ~850 modules migrated from OSGi to Zephyr and saw their application startup time decrease from ~3 minutes to ~15 seconds.
# Extensible Package Format
Bundling modules can be a pain. Zephyr supports WAR and JAR applications by default (although it is not a servlet container), and additional formats may be easily supported via kernel-module.
# Examples
# Example 0: Definitions
We will usually depict a set of related modules as a graph, which is a collection of vertices and edges between them.
For instance, consider the graph below, consisting of the vertices A and B. The edge is the line between them (unnamed):
Undirected Graph | Directed Graph, A to B | Directed Graph, B to A |
Note the location of the arrow in each case as it indicates the direction of the relationship. Relational directions are important because they encode quite a bit of information. For instance, the notion of family is an example of an undirected relationship (A and B are family, whereas other relationships like mother imply a direction (A is mother of B).
It's not hard to think of quite a few objects and many relationships between them, but the relationships that are important for Zephyr's purposes are dependent on and depended on. When the relationship is A is dependent on B, then the arrow points from B to A as follows:
This is the convention that we will use through this documentation.