Skip to content

Commit

Permalink
feat: add event bus + a few events emitted from TaskGraph
Browse files Browse the repository at this point in the history
  • Loading branch information
edvald committed Nov 30, 2018
1 parent 8bbc389 commit 3c19e36
Show file tree
Hide file tree
Showing 10 changed files with 389 additions and 151 deletions.
219 changes: 115 additions & 104 deletions garden-service/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions garden-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"dockerode": "^2.5.7",
"elegant-spinner": "^1.0.1",
"escape-string-regexp": "^1.0.5",
"eventemitter2": "^5.0.1",
"execa": "^1.0.0",
"fs-extra": "^7.0.0",
"get-port": "^4.0.0",
Expand Down Expand Up @@ -138,6 +139,7 @@
"snyk": "^1.105.0",
"testdouble": "^3.8.2",
"testdouble-chai": "^0.5.0",
"timekeeper": "^2.1.2",
"tmp-promise": "^1.0.5",
"ts-node": "^7.0.1",
"tslint": "^5.11.0",
Expand Down
61 changes: 61 additions & 0 deletions garden-service/src/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2018 Garden Technologies, Inc. <info@garden.io>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { EventEmitter2 } from "eventemitter2"
import { TaskResult } from "./task-graph"
import { ModuleVersion } from "./vcs/base"

/**
* This simple class serves as the central event bus for a Garden instance. Its function
* is mainly to consolidate all events for the instance, to ensure type-safety.
*
* See below for the event interfaces.
*/
export class EventBus extends EventEmitter2 {
constructor() {
super({
wildcard: false,
newListener: false,
maxListeners: 100, // we may need to adjust this
})
}

emit<T extends EventName>(name: T, payload: Events[T]) {
return super.emit(name, payload)
}

on<T extends EventName>(name: T, listener: (payload: Events[T]) => void) {
return super.on(name, listener)
}

onAny(listener: <T extends EventName>(name: T, payload: Events[T]) => void) {
return super.onAny(<any>listener)
}

once<T extends EventName>(name: T, listener: (payload: Events[T]) => void) {
return super.once(name, listener)
}

// TODO: wrap more methods to make them type-safe
}

/**
* The supported events and their interfaces.
*/
export interface Events {
_test: string,
taskPending: {
addedAt: Date,
key: string,
version: ModuleVersion,
},
taskComplete: TaskResult,
taskError: TaskResult,
}

export type EventName = keyof Events
18 changes: 12 additions & 6 deletions garden-service/src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ import { ModuleAndRuntimeActions, Plugins, RegisterPluginParam } from "./types/p
import { SUPPORTED_PLATFORMS, SupportedPlatform } from "./constants"
import { platform, arch } from "os"
import { LogEntry } from "./logger/log-entry"
import { EventBus } from "./events"

export interface ActionHandlerMap<T extends keyof PluginActions> {
[actionName: string]: PluginActions[T]
Expand Down Expand Up @@ -159,6 +160,7 @@ export class Garden {
public readonly vcs: VcsHandler
public readonly cache: TreeCache
public readonly actions: ActionHelper
public readonly events: EventBus

constructor(
public readonly projectRoot: string,
Expand Down Expand Up @@ -203,12 +205,15 @@ export class Garden {
this.actionHandlers = <PluginActionMap>fromPairs(pluginActionNames.map(n => [n, {}]))
this.moduleActionHandlers = <ModuleActionMap>fromPairs(moduleActionNames.map(n => [n, {}]))

this.taskGraph = new TaskGraph(this.log)
this.taskGraph = new TaskGraph(this, this.log)
this.actions = new ActionHelper(this)
this.hotReloadScheduler = new HotReloadScheduler()
this.events = new EventBus()
}

static async factory(currentDirectory: string, { env, config, log, plugins = {} }: ContextOpts = {}) {
static async factory<T extends typeof Garden>(
this: T, currentDirectory: string, { env, config, log, plugins = {} }: ContextOpts = {},
): Promise<InstanceType<T>> {
let parsedConfig: GardenConfig

if (config) {
Expand Down Expand Up @@ -288,25 +293,26 @@ export class Garden {

const buildDir = await BuildDir.factory(projectRoot)

const garden = new Garden(
const garden = new this(
projectRoot,
projectName,
environmentName,
variables,
projectSources,
buildDir,
log,
)
) as InstanceType<T>

// Register plugins
for (const [name, pluginFactory] of Object.entries({ ...builtinPlugins, ...plugins })) {
garden.registerPlugin(name, pluginFactory)
// This cast is required for the linter to accept the instance type hackery.
(<Garden>garden).registerPlugin(name, pluginFactory)
}

// Load configured plugins
// Validate configuration
for (const provider of Object.values(mergedProviderConfigs)) {
await garden.loadPlugin(provider.name, provider)
await (<Garden>garden).loadPlugin(provider.name, provider)
}

return garden
Expand Down
12 changes: 11 additions & 1 deletion garden-service/src/task-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { BaseTask, TaskDefinitionError } from "./tasks/base"

import { LogEntry } from "./logger/log-entry"
import { toGardenError } from "./exceptions"
import { Garden } from "./garden"

class TaskGraphError extends Error { }

Expand Down Expand Up @@ -45,7 +46,7 @@ export class TaskGraph {
private resultCache: ResultCache
private opQueue: PQueue

constructor(private log: LogEntry, private concurrency: number = DEFAULT_CONCURRENCY) {
constructor(private garden: Garden, private log: LogEntry, private concurrency: number = DEFAULT_CONCURRENCY) {
this.roots = new TaskNodeMap()
this.index = new TaskNodeMap()
this.inProgress = new TaskNodeMap()
Expand Down Expand Up @@ -87,6 +88,13 @@ export class TaskGraph {
}

this.index.addNode(node)

this.garden.events.emit("taskPending", {
addedAt: new Date(),
key: node.getKey(),
version: task.version,
})

await this.addDependencies(node)

if (node.getDependencies().length === 0) {
Expand Down Expand Up @@ -142,8 +150,10 @@ export class TaskGraph {

try {
result = await node.process(dependencyResults)
this.garden.events.emit("taskComplete", result)
} catch (error) {
result = { type, description, error }
this.garden.events.emit("taskError", result)
this.logTaskError(node, error)
this.cancelDependants(node)
} finally {
Expand Down
1 change: 1 addition & 0 deletions garden-service/src/util/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type HookCallback = (callback?: () => void) => void

const exitHookNames: string[] = [] // For debugging/testing/inspection purposes

export type ValueOf<T> = T[keyof T]
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
export type Diff<T, U> = T extends U ? never : T
export type Nullable<T> = { [P in keyof T]: T[P] | null }
Expand Down
68 changes: 57 additions & 11 deletions garden-service/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ import { remove, readdirSync, existsSync } from "fs-extra"
import { containerModuleSpecSchema } from "../src/plugins/container"
import { testGenericModule, buildGenericModule } from "../src/plugins/generic"
import { TaskResults } from "../src/task-graph"
import {
validate,
} from "../src/config/common"
import { validate, PrimitiveMap } from "../src/config/common"
import {
GardenPlugin,
PluginActions,
Expand All @@ -34,13 +32,14 @@ import {
RunTaskParams,
SetSecretParams,
} from "../src/types/plugin/params"
import {
helpers,
} from "../src/vcs/git"
import {
ModuleVersion,
} from "../src/vcs/base"
import { helpers } from "../src/vcs/git"
import { ModuleVersion } from "../src/vcs/base"
import { GARDEN_DIR_NAME } from "../src/constants"
import { EventBus, Events } from "../src/events"
import { ValueOf } from "../src/util/util"
import { SourceConfig } from "../src/config/project"
import { BuildDir } from "../src/build-dir"
import timekeeper = require("timekeeper")

export const dataDir = resolve(__dirname, "data")
export const examplesDir = resolve(__dirname, "..", "..", "examples")
Expand Down Expand Up @@ -230,15 +229,54 @@ export const makeTestModule = (params: Partial<ModuleConfig> = {}) => {
return { ...defaultModuleConfig, ...params }
}

export const makeTestGarden = async (projectRoot: string, extraPlugins: Plugins = {}) => {
interface EventLogEntry {
name: string
payload: ValueOf<Events>
}

/**
* Used for test Garden instances, to log emitted events.
*/
class TestEventBus extends EventBus {
log: EventLogEntry[]

constructor() {
super()
this.log = []
}

emit<T extends keyof Events>(name: T, payload: Events[T]) {
this.log.push({ name, payload })
return super.emit(name, payload)
}
}

export class TestGarden extends Garden {
events: TestEventBus

constructor(
public readonly projectRoot: string,
public readonly projectName: string,
environmentName: string,
variables: PrimitiveMap,
public readonly projectSources: SourceConfig[] = [],
public readonly buildDir: BuildDir,
log?,
) {
super(projectRoot, projectName, environmentName, variables, projectSources, buildDir, log)
this.events = new TestEventBus()
}
}

export const makeTestGarden = async (projectRoot: string, extraPlugins: Plugins = {}): Promise<TestGarden> => {
const testPlugins = {
"test-plugin": testPlugin,
"test-plugin-b": testPluginB,
"test-plugin-c": testPluginC,
}
const plugins = { ...testPlugins, ...extraPlugins }

return Garden.factory(projectRoot, { plugins })
return TestGarden.factory(projectRoot, { plugins })
}

export const makeTestGardenA = async (extraPlugins: Plugins = {}) => {
Expand Down Expand Up @@ -319,3 +357,11 @@ export function getExampleProjects() {
const names = readdirSync(examplesDir).filter(n => existsSync(join(examplesDir, n, "garden.yml")))
return fromPairs(names.map(n => [n, join(examplesDir, n)]))
}

export function freezeTime(date?: Date) {
if (!date) {
date = new Date()
}
timekeeper.freeze(date)
return date
}
6 changes: 5 additions & 1 deletion garden-service/test/setup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as td from "testdouble"
import * as timekeeper from "timekeeper"
import { Logger } from "../src/logger/logger"
import { LogLevel } from "../src/logger/log-node"
import { makeTestGardenA } from "./helpers"
Expand All @@ -18,4 +19,7 @@ before(async function(this: any) {
})

beforeEach(() => { })
afterEach(() => td.reset())
afterEach(() => {
td.reset()
timekeeper.reset()
})
48 changes: 48 additions & 0 deletions garden-service/test/src/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2018 Garden Technologies, Inc. <info@garden.io>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { EventBus } from "../../src/events"
import { expect } from "chai"

describe("EventBus", () => {
let events: EventBus

beforeEach(() => {
events = new EventBus()
})

it("should send+receive events", (done) => {
events.on("_test", (payload) => {
expect(payload).to.equal("foo")
done()
})
events.emit("_test", "foo")
})

describe("onAny", () => {
it("should add listener for any supported event", (done) => {
events.onAny((name, payload) => {
expect(name).to.equal("_test")
expect(payload).to.equal("foo")
done()
})
events.emit("_test", "foo")
})
})

describe("once", () => {
it("should add a listener that only gets called once", (done) => {
events.once("_test", (payload) => {
expect(payload).to.equal("foo")
done()
})
events.emit("_test", "foo")
events.emit("_test", "bar")
})
})
})
Loading

0 comments on commit 3c19e36

Please sign in to comment.