Skip to content

Commit 5a6b2f8

Browse files
authored
events: Refactor; move Event into core, so core can emit events (#6930)
* events: Refactor; move Event into core, so core can emit events Requires some slight trickery to invert dependencies. We can't have the caddy package import the caddyevents package, because caddyevents imports caddy. Interface to the rescue! Also add two new events, experimentally: started, and stopping. At the request of a sponsor. Also rename "Filesystems" to "FileSystems" to match Go convention (unrelated to events, was just bugging me when I noticed it). * Coupla bug fixes * lol whoops
1 parent ea77a9a commit 5a6b2f8

File tree

8 files changed

+170
-122
lines changed

8 files changed

+170
-122
lines changed

caddy.go

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,14 @@ type Config struct {
8181
// associated value.
8282
AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
8383

84-
apps map[string]App
85-
storage certmagic.Storage
84+
apps map[string]App
85+
storage certmagic.Storage
86+
eventEmitter eventEmitter
8687

8788
cancelFunc context.CancelFunc
8889

89-
// filesystems is a dict of filesystems that will later be loaded from and added to.
90-
filesystems FileSystems
90+
// fileSystems is a dict of fileSystems that will later be loaded from and added to.
91+
fileSystems FileSystems
9192
}
9293

9394
// App is a thing that Caddy runs.
@@ -442,6 +443,10 @@ func run(newCfg *Config, start bool) (Context, error) {
442443
}
443444
globalMetrics.configSuccess.Set(1)
444445
globalMetrics.configSuccessTime.SetToCurrentTime()
446+
447+
// TODO: This event is experimental and subject to change.
448+
ctx.emitEvent("started", nil)
449+
445450
// now that the user's config is running, finish setting up anything else,
446451
// such as remote admin endpoint, config loader, etc.
447452
return ctx, finishSettingUp(ctx, ctx.cfg)
@@ -509,7 +514,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
509514
}
510515

511516
// create the new filesystem map
512-
newCfg.filesystems = &filesystems.FilesystemMap{}
517+
newCfg.fileSystems = &filesystems.FileSystemMap{}
513518

514519
// prepare the new config for use
515520
newCfg.apps = make(map[string]App)
@@ -696,6 +701,9 @@ func unsyncedStop(ctx Context) {
696701
return
697702
}
698703

704+
// TODO: This event is experimental and subject to change.
705+
ctx.emitEvent("stopping", nil)
706+
699707
// stop each app
700708
for name, a := range ctx.cfg.apps {
701709
err := a.Stop()
@@ -1038,6 +1046,92 @@ func Version() (simple, full string) {
10381046
return
10391047
}
10401048

1049+
// Event represents something that has happened or is happening.
1050+
// An Event value is not synchronized, so it should be copied if
1051+
// being used in goroutines.
1052+
//
1053+
// EXPERIMENTAL: Events are subject to change.
1054+
type Event struct {
1055+
// If non-nil, the event has been aborted, meaning
1056+
// propagation has stopped to other handlers and
1057+
// the code should stop what it was doing. Emitters
1058+
// may choose to use this as a signal to adjust their
1059+
// code path appropriately.
1060+
Aborted error
1061+
1062+
// The data associated with the event. Usually the
1063+
// original emitter will be the only one to set or
1064+
// change these values, but the field is exported
1065+
// so handlers can have full access if needed.
1066+
// However, this map is not synchronized, so
1067+
// handlers must not use this map directly in new
1068+
// goroutines; instead, copy the map to use it in a
1069+
// goroutine. Data may be nil.
1070+
Data map[string]any
1071+
1072+
id uuid.UUID
1073+
ts time.Time
1074+
name string
1075+
origin Module
1076+
}
1077+
1078+
// NewEvent creates a new event, but does not emit the event. To emit an
1079+
// event, call Emit() on the current instance of the caddyevents app insteaad.
1080+
//
1081+
// EXPERIMENTAL: Subject to change.
1082+
func NewEvent(ctx Context, name string, data map[string]any) (Event, error) {
1083+
id, err := uuid.NewRandom()
1084+
if err != nil {
1085+
return Event{}, fmt.Errorf("generating new event ID: %v", err)
1086+
}
1087+
name = strings.ToLower(name)
1088+
return Event{
1089+
Data: data,
1090+
id: id,
1091+
ts: time.Now(),
1092+
name: name,
1093+
origin: ctx.Module(),
1094+
}, nil
1095+
}
1096+
1097+
func (e Event) ID() uuid.UUID { return e.id }
1098+
func (e Event) Timestamp() time.Time { return e.ts }
1099+
func (e Event) Name() string { return e.name }
1100+
func (e Event) Origin() Module { return e.origin } // Returns the module that originated the event. May be nil, usually if caddy core emits the event.
1101+
1102+
// CloudEvent exports event e as a structure that, when
1103+
// serialized as JSON, is compatible with the
1104+
// CloudEvents spec.
1105+
func (e Event) CloudEvent() CloudEvent {
1106+
dataJSON, _ := json.Marshal(e.Data)
1107+
return CloudEvent{
1108+
ID: e.id.String(),
1109+
Source: e.origin.CaddyModule().String(),
1110+
SpecVersion: "1.0",
1111+
Type: e.name,
1112+
Time: e.ts,
1113+
DataContentType: "application/json",
1114+
Data: dataJSON,
1115+
}
1116+
}
1117+
1118+
// CloudEvent is a JSON-serializable structure that
1119+
// is compatible with the CloudEvents specification.
1120+
// See https://cloudevents.io.
1121+
// EXPERIMENTAL: Subject to change.
1122+
type CloudEvent struct {
1123+
ID string `json:"id"`
1124+
Source string `json:"source"`
1125+
SpecVersion string `json:"specversion"`
1126+
Type string `json:"type"`
1127+
Time time.Time `json:"time"`
1128+
DataContentType string `json:"datacontenttype,omitempty"`
1129+
Data json.RawMessage `json:"data,omitempty"`
1130+
}
1131+
1132+
// ErrEventAborted cancels an event.
1133+
var ErrEventAborted = errors.New("event aborted")
1134+
10411135
// ActiveContext returns the currently-active context.
10421136
// This function is experimental and might be changed
10431137
// or removed in the future.

context.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,14 @@ func (ctx *Context) OnCancel(f func()) {
9191
ctx.cleanupFuncs = append(ctx.cleanupFuncs, f)
9292
}
9393

94-
// Filesystems returns a ref to the FilesystemMap.
94+
// FileSystems returns a ref to the FilesystemMap.
9595
// EXPERIMENTAL: This API is subject to change.
96-
func (ctx *Context) Filesystems() FileSystems {
96+
func (ctx *Context) FileSystems() FileSystems {
9797
// if no config is loaded, we use a default filesystemmap, which includes the osfs
9898
if ctx.cfg == nil {
99-
return &filesystems.FilesystemMap{}
99+
return &filesystems.FileSystemMap{}
100100
}
101-
return ctx.cfg.filesystems
101+
return ctx.cfg.fileSystems
102102
}
103103

104104
// Returns the active metrics registry for the context
@@ -277,6 +277,14 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error)
277277
return result, nil
278278
}
279279

280+
// emitEvent is a small convenience method so the caddy core can emit events, if the event app is configured.
281+
func (ctx Context) emitEvent(name string, data map[string]any) Event {
282+
if ctx.cfg == nil || ctx.cfg.eventEmitter == nil {
283+
return Event{}
284+
}
285+
return ctx.cfg.eventEmitter.Emit(ctx, name, data)
286+
}
287+
280288
// loadModulesFromSomeMap loads modules from val, which must be a type of map[string]any.
281289
// Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module
282290
// name) or as a regular map (key is not the module name, and module name is defined inline).
@@ -429,6 +437,14 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
429437

430438
ctx.moduleInstances[id] = append(ctx.moduleInstances[id], val)
431439

440+
// if the loaded module happens to be an app that can emit events, store it so the
441+
// core can have access to emit events without an import cycle
442+
if ee, ok := val.(eventEmitter); ok {
443+
if _, ok := ee.(App); ok {
444+
ctx.cfg.eventEmitter = ee
445+
}
446+
}
447+
432448
return val, nil
433449
}
434450

@@ -600,3 +616,11 @@ func (ctx *Context) WithValue(key, value any) Context {
600616
exitFuncs: ctx.exitFuncs,
601617
}
602618
}
619+
620+
// eventEmitter is a small interface that inverts dependencies for
621+
// the caddyevents package, so the core can emit events without an
622+
// import cycle (i.e. the caddy package doesn't have to import
623+
// the caddyevents package, which imports the caddy package).
624+
type eventEmitter interface {
625+
Emit(ctx Context, eventName string, data map[string]any) Event
626+
}

internal/filesystems/map.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,35 @@ import (
77
)
88

99
const (
10-
DefaultFilesystemKey = "default"
10+
DefaultFileSystemKey = "default"
1111
)
1212

13-
var DefaultFilesystem = &wrapperFs{key: DefaultFilesystemKey, FS: OsFS{}}
13+
var DefaultFileSystem = &wrapperFs{key: DefaultFileSystemKey, FS: OsFS{}}
1414

1515
// wrapperFs exists so can easily add to wrapperFs down the line
1616
type wrapperFs struct {
1717
key string
1818
fs.FS
1919
}
2020

21-
// FilesystemMap stores a map of filesystems
21+
// FileSystemMap stores a map of filesystems
2222
// the empty key will be overwritten to be the default key
2323
// it includes a default filesystem, based off the os fs
24-
type FilesystemMap struct {
24+
type FileSystemMap struct {
2525
m sync.Map
2626
}
2727

2828
// note that the first invocation of key cannot be called in a racy context.
29-
func (f *FilesystemMap) key(k string) string {
29+
func (f *FileSystemMap) key(k string) string {
3030
if k == "" {
31-
k = DefaultFilesystemKey
31+
k = DefaultFileSystemKey
3232
}
3333
return k
3434
}
3535

3636
// Register will add the filesystem with key to later be retrieved
3737
// A call with a nil fs will call unregister, ensuring that a call to Default() will never be nil
38-
func (f *FilesystemMap) Register(k string, v fs.FS) {
38+
func (f *FileSystemMap) Register(k string, v fs.FS) {
3939
k = f.key(k)
4040
if v == nil {
4141
f.Unregister(k)
@@ -47,31 +47,31 @@ func (f *FilesystemMap) Register(k string, v fs.FS) {
4747
// Unregister will remove the filesystem with key from the filesystem map
4848
// if the key is the default key, it will set the default to the osFS instead of deleting it
4949
// modules should call this on cleanup to be safe
50-
func (f *FilesystemMap) Unregister(k string) {
50+
func (f *FileSystemMap) Unregister(k string) {
5151
k = f.key(k)
52-
if k == DefaultFilesystemKey {
53-
f.m.Store(k, DefaultFilesystem)
52+
if k == DefaultFileSystemKey {
53+
f.m.Store(k, DefaultFileSystem)
5454
} else {
5555
f.m.Delete(k)
5656
}
5757
}
5858

5959
// Get will get a filesystem with a given key
60-
func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) {
60+
func (f *FileSystemMap) Get(k string) (v fs.FS, ok bool) {
6161
k = f.key(k)
6262
c, ok := f.m.Load(strings.TrimSpace(k))
6363
if !ok {
64-
if k == DefaultFilesystemKey {
65-
f.m.Store(k, DefaultFilesystem)
66-
return DefaultFilesystem, true
64+
if k == DefaultFileSystemKey {
65+
f.m.Store(k, DefaultFileSystem)
66+
return DefaultFileSystem, true
6767
}
6868
return nil, ok
6969
}
7070
return c.(fs.FS), true
7171
}
7272

7373
// Default will get the default filesystem in the filesystem map
74-
func (f *FilesystemMap) Default() fs.FS {
75-
val, _ := f.Get(DefaultFilesystemKey)
74+
func (f *FileSystemMap) Default() fs.FS {
75+
val, _ := f.Get(DefaultFileSystemKey)
7676
return val
7777
}

0 commit comments

Comments
 (0)