Very freestyle programming for now, until I have a good look and feel The new version i'm working on will use etcd (embed standalone, embed etcd with follower/leader, remote etcd leader/follower) to handle the state, for now i just want to rush the general idea! On one of the branches i have a satifying concept, i'm completely re-writting everything to make a new start with etcd From frustration and instinct to reason and usability
I did a small experiment about it
Gronos is a concurrent application management library for Go, designed to simplify the process of managing multiple concurrent applications within a single program. It provides a structured approach to application lifecycle management, error handling, and inter-application communication.
I saw myself writting the same boilerplate code over and over again for Domain Driven Applications specifically so I made a library to stop wasting time and repeating myself. Now we can all enjoy a tool to sandbox lifecycled functions aka "runtime applications" broadly known as lifecycle functions.
Warning:
This is an experimental project aimed at simplifying the development of Domain-Driven Design (DDD) applications with Event-Driven Architecture (EDA) from the start. The goal is to provide a comprehensive toolbox that, once fully developed, will be refined to improve usability.
I believe we shouldn't have to choose between building a traditional application and later transitioning to DDD and EDA. We should be able to implement EDA from the beginning and take advantage of its scalability. If all domains and workers can operate across multiple binaries, why not within a single one? The application should be flexible enough to function either as a standalone or distributed system, allowing us to split and scale as needed.
I'm going to change a direction a bit with that library...
Roadmap notes:
- add hierarchy of lifecycle function
- a sub-lifecycle function could dies but the supervisor will revive it
- a sub-lifecycle function could be be restricted for terminating another lifecyle function that is not in the same hierarchy
- restrict messaging rights based on hierarchy
- toolbox to create a Shepherd node (gronos instance guiding and managing) and Flock node (processes being guided)
Stuff like that, i have some vision of what i want and NEED, especially what i want to see in our industry.
- Features
- Installation
- Usage
- Advanced Usage
- Configuration
- Best Practices
- Examples
- Detailed Documentation
- Contributing
- License
- Concurrent Application Management: Manage multiple applications running concurrently with ease.
- Type-Safe Keys: Use any comparable type as keys for your applications.
- Dynamic Application Management: Add or remove applications at runtime.
- Graceful Shutdown: Properly shut down all managed applications.
- Error Propagation: Centralized error handling for all managed applications.
- Worker Functionality: Easily create and manage periodic tasks.
- Iterator Pattern: Implement repeating sequences of tasks effortlessly.
- Internal Messaging System: Allow inter-application communication.
- Flexible Configuration: Customize behavior with various options.
- Watermill Integration: Incorporate event-driven architecture and message routing.
To install Gronos, use go get
:
go get github.com/davidroman0O/gronos
Ensure your go.mod
file contains the following line:
require github.com/davidroman0O/gronos v<latest-version>
Replace <latest-version>
with the most recent version of Gronos.
To create a new Gronos instance, use the New
function with a basic "Hello World" application:
import (
"context"
"fmt"
"time"
"github.com/davidroman0O/gronos"
)
ctx := context.Background()
g, errChan := gronos.New[string](ctx, map[string]gronos.RuntimeApplication{
"hello-world": func(ctx context.Context, shutdown <-chan struct{}) error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("Hello, World!")
case <-ctx.Done():
return ctx.Err()
case <-shutdown:
return nil
}
}
},
})
The New
function returns a Gronos instance and an error channel. The generic parameter (in this case, string
) defines the type of keys used to identify applications.
Applications in Gronos are defined as functions with the following signature:
type RuntimeApplication func(ctx context.Context, shutdown <-chan struct{}) error
Here's an example of a simple application:
func simpleApp(ctx context.Context, shutdown <-chan struct{}) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-shutdown:
return nil
default:
// Do work here
time.Sleep(time.Second)
fmt.Println("Working...")
}
}
}
Add applications to Gronos using the Add
method:
g.Add("myApp", simpleApp)
Gronos starts managing applications as soon as they're added. To stop all applications and shut down Gronos:
g.Shutdown()
g.Wait()
Errors from applications are sent to the error channel returned by New
:
go func() {
for err := range errChan {
log.Printf("Error: %v\n", err)
}
}()
The Worker
function creates applications that perform periodic tasks:
worker := gronos.Worker(time.Second, gronos.NonBlocking, func(ctx context.Context) error {
fmt.Println("Periodic task executed")
return nil
})
g.Add("periodicTask", worker)
The Iterator
function creates applications that execute a sequence of tasks in a loop:
tasks := []gronos.CancellableTask{
func(ctx context.Context) error {
fmt.Println("Task 1 executed")
return nil
},
func(ctx context.Context) error {
fmt.Println("Task 2 executed")
return nil
},
}
iterApp := gronos.Iterator(context.Background(), tasks)
g.Add("taskSequence", iterApp)
Gronos provides an internal messaging system for communication between applications. The available public messages for runtime applications are:
func communicatingApp(ctx context.Context, shutdown <-chan struct{}) error {
bus, err := gronos.UseBus(ctx)
if err != nil {
return err
}
// Available messages:
// Add a new runtime application
done, addMsg := gronos.MsgAdd("newApp", newAppFunc)
bus(addMsg)
<-done
// Force cancel shutdown for an application
bus(gronos.MsgForceCancelShutdown("appName", errors.New("force cancel reason")))
// Force terminate shutdown for an application
bus(gronos.MsgForceTerminateShutdown("appName"))
// ... rest of the application logic
return nil
}
These messages allow you to dynamically add new applications, force cancel a shutdown, or force terminate a shutdown for specific applications.
Gronos provides integration with the Watermill library, allowing you to easily incorporate event-driven architecture and message routing into your applications.
import (
"github.com/davidroman0O/gronos"
watermillext "github.com/davidroman0O/gronos/extensions/watermill"
)
func main() {
ctx := context.Background()
watermillMiddleware := watermillext.NewWatermillMiddleware[string](watermill.NewStdLogger(true, true))
g, errChan := gronos.New[string](ctx, map[string]gronos.RuntimeApplication{
"setup": setupApp,
},
gronos.WithExtension[string](watermillMiddleware),
)
// ... rest of your Gronos setup
}
func setupApp(ctx context.Context, shutdown <-chan struct{}) error {
com, err := gronos.UseBus(ctx)
if err != nil {
return err
}
pubSub := gochannel.NewGoChannel(gochannel.Config{}, watermill.NewStdLogger(false, false))
doneAddPublisher, msgAddPublisher := watermillext.MsgAddPublisher("pubsub", pubSub)
com(msgAddPublisher) // send message
<- doneAddPublisher // wait for it to be processed (you're not forced to but cool to have)
doneAddSubscriber, msgAddSubscriber := watermillext.MsgAddSubscriber("pubsub", pubSub)
com(msgAddSubscriber)
<- doneAddSubscriber
// ... rest of your setup
return nil
}
This integration allows you to use Watermill's powerful messaging capabilities within your Gronos applications, enabling sophisticated pub/sub patterns and message routing.
Gronos supports various configuration options:
g, errChan := gronos.New[string](ctx, nil,
gronos.WithShutdownBehavior[string](gronos.ShutdownAutomatic),
gronos.WithGracePeriod[string](5 * time.Second),
gronos.WithMinRuntime[string](10 * time.Second),
)
Available options:
WithShutdownBehavior
: Define how Gronos should handle shutdowns.WithGracePeriod
: Set the grace period for shutdowns.WithMinRuntime
: Set the minimum runtime before allowing shutdown.
- Error Handling: Always handle errors from the error channel to prevent goroutine leaks.
- Context Usage: Use the provided context for cancellation and timeout management.
- Graceful Shutdown: Implement proper shutdown logic in your applications to ensure clean exits.
- Resource Management: Properly manage resources (e.g., close file handles, database connections) in your applications.
- Avoid Blocking: In
Worker
andIterator
tasks, avoid long-running operations that could block other tasks.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/davidroman0O/gronos"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
g, errChan := gronos.New[string](ctx, map[string]gronos.RuntimeApplication{
"hello-world": func(ctx context.Context, shutdown <-chan struct{}) error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("Hello, World!")
case <-ctx.Done():
return ctx.Err()
case <-shutdown:
return nil
}
}
},
})
// Error handling goroutine
go func() {
for err := range errChan {
log.Printf("Error: %v\n", err)
}
}()
// Add another application
g.Add("app1", func(ctx context.Context, shutdown <-chan struct{}) error {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("App1 is running")
case <-ctx.Done():
return ctx.Err()
case <-shutdown:
return nil
}
}
})
// Run for 10 seconds
time.Sleep(10 * time.Second)
// Shutdown Gronos
g.Shutdown()
// Wait for all applications to finish
g.Wait()
}
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/davidroman0O/gronos"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
g, errChan := gronos.New[string](ctx, nil)
// Error handling
go func() {
for err := range errChan {
log.Printf("Error: %v\n", err)
}
}()
// Add a worker
worker := gronos.Worker(time.Second, gronos.NonBlocking, func(ctx context.Context) error {
fmt.Println("Worker task executed")
return nil
})
g.Add("worker", worker)
// Add an iterator
tasks := []gronos.CancellableTask{
func(ctx context.Context) error {
fmt.Println("Iterator task 1")
return nil
},
func(ctx context.Context) error {
fmt.Println("Iterator task 2")
return nil
},
}
iterator := gronos.Iterator(context.Background(), tasks)
g.Add("iterator", iterator)
// Run for 10 seconds
time.Sleep(10 * time.Second)
// Shutdown and wait
g.Shutdown()
g.Wait()
}
For more detailed information about specific features, please refer to the following documents:
Contributions to Gronos are welcome! Please follow these steps:
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature
) - Commit your changes (
git commit -m 'Add some AmazingFeature'
) - Push to the branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
Please make sure to update tests as appropriate and adhere to the existing coding style.
Gronos is released under the MIT License. See the LICENSE file for details.
For more information, please check the documentation or open an issue on GitHub.