Repository: gobuffalo/buffalo
Pull request #2243 (2022-04-18 13:49:53)

added input/status checks, testcases, and comments for road-to-v1
Improved the simple worker:

* Added input/status checks for
  * `Register()`, if the name and handler are defined correctly
  * `Perform()` and `PerformIn()`, if the worker's context was already canceled to prevent job submission
* Added wait group count for delayed jobs (PerformIn)
* Added locking in `Stop()` to prevent job submission when stopping
* Added comments for the road-to-v1 (#2242)

(This PR was started for #2241)


Patch:
diff --git a/default_context.go b/default_context.go
index 3f2eec99..e88acbf4 100644
--- a/default_context.go
+++ b/default_context.go
@@ -23,6 +23,9 @@ import (
 var _ Context = &DefaultContext{}
 var _ context.Context = &DefaultContext{}
 
+// TODO(sio4): #road-to-v1 - make DefaultContext private
+// and only allow it to be generated by App.newContext() or any similar.
+
 // DefaultContext is, as its name implies, a default
 // implementation of the Context interface.
 type DefaultContext struct {
@@ -67,16 +70,22 @@ func (d *DefaultContext) Param(key string) string {
 // Set a value onto the Context. Any value set onto the Context
 // will be automatically available in templates.
 func (d *DefaultContext) Set(key string, value interface{}) {
+	if d.data == nil {
+		d.data = &sync.Map{}
+	}
 	d.data.Store(key, value)
 }
 
 // Value that has previously stored on the context.
 func (d *DefaultContext) Value(key interface{}) interface{} {
-	if k, ok := key.(string); ok {
+	if k, ok := key.(string); ok && d.data != nil {
 		if v, ok := d.data.Load(k); ok {
 			return v
 		}
 	}
+	if d.Context == nil {
+		return nil
+	}
 	return d.Context.Value(key)
 }
 
@@ -235,6 +244,11 @@ func (d *DefaultContext) Redirect(status int, url string, args ...interface{}) e
 // Data contains all the values set through Get/Set.
 func (d *DefaultContext) Data() map[string]interface{} {
 	m := map[string]interface{}{}
+
+	if d.data == nil {
+		return m
+	}
+
 	d.data.Range(func(k, v interface{}) bool {
 		s, ok := k.(string)
 		if !ok {
diff --git a/go.mod b/go.mod
index ac291b6b..d62525b1 100644
--- a/go.mod
+++ b/go.mod
@@ -16,7 +16,7 @@ require (
 	github.com/gobuffalo/meta v0.3.1
 	github.com/gobuffalo/nulls v0.4.1
 	github.com/gobuffalo/plush/v4 v4.1.10
-	github.com/gobuffalo/pop/v6 v6.0.1
+	github.com/gobuffalo/pop/v6 v6.0.2
 	github.com/gobuffalo/tags/v3 v3.1.2
 	github.com/google/go-cmp v0.5.6 // indirect
 	github.com/gorilla/handlers v1.5.1
diff --git a/go.sum b/go.sum
index 57f5b2e0..c753f91c 100644
--- a/go.sum
+++ b/go.sum
@@ -115,6 +115,7 @@ github.com/gobuffalo/flect v0.2.4/go.mod h1:1ZyCLIbg0YD7sDkzvFdPoOydPtD8y9JQnrOR
 github.com/gobuffalo/flect v0.2.5 h1:H6vvsv2an0lalEaCDRThvtBfmg44W/QHXBCYUXf/6S4=
 github.com/gobuffalo/flect v0.2.5/go.mod h1:1ZyCLIbg0YD7sDkzvFdPoOydPtD8y9JQnrOROolUcM8=
 github.com/gobuffalo/genny/v2 v2.0.8/go.mod h1:R45scCyQfff2HysNJHNanjrpvPw4Qu+rM1MOMDBB5oU=
+github.com/gobuffalo/genny/v2 v2.0.9/go.mod h1:R45scCyQfff2HysNJHNanjrpvPw4Qu+rM1MOMDBB5oU=
 github.com/gobuffalo/github_flavored_markdown v1.1.0/go.mod h1:TSpTKWcRTI0+v7W3x8dkSKMLJSUpuVitlptCkpeY8ic=
 github.com/gobuffalo/github_flavored_markdown v1.1.1 h1:kUf8ginyBOTRXcKSTPsPAqlA25vQ80+xAspLIYaxmTU=
 github.com/gobuffalo/github_flavored_markdown v1.1.1/go.mod h1:yU32Pen+eorS58oxh/bNZx76zUOCJwmvyV5FBrvzOKQ=
@@ -136,8 +137,8 @@ github.com/gobuffalo/plush/v4 v4.1.9/go.mod h1:9OOII9uAM5pZnhWu1OkQnboXJjaWMQ7kc
 github.com/gobuffalo/plush/v4 v4.1.10 h1:sBTmJJ1Hpj4ExqC4NFmCvAgH6Iq6t58JaWvAl8NEAFc=
 github.com/gobuffalo/plush/v4 v4.1.10/go.mod h1:9OOII9uAM5pZnhWu1OkQnboXJjaWMQ7kcTl3zNcxvTM=
 github.com/gobuffalo/pop/v6 v6.0.0/go.mod h1:5rd3OnViLhjteR8+0i/mT9Q4CzkTzCoR7tm/9mmAic4=
-github.com/gobuffalo/pop/v6 v6.0.1 h1:4YhzxW4hVvf0xLW9zVkhPZFuH5VmBc4ffIIP/C++SLQ=
-github.com/gobuffalo/pop/v6 v6.0.1/go.mod h1:5NO7ehmyRjRctnbMDhIqKkkg6zvdueufYltxErfp9BU=
+github.com/gobuffalo/pop/v6 v6.0.2 h1:NuIMTwZ54aHK92ertwv3R2SepAvhTRlQj9/MLqO9Qdo=
+github.com/gobuffalo/pop/v6 v6.0.2/go.mod h1:1g+AV0EDe0rUoy1I3yA3aa9N5+/uC5RxtGbxI0KNe4c=
 github.com/gobuffalo/tags/v3 v3.0.2/go.mod h1:ZQeN6TCTiwAFnS0dNcbDtSgZDwNKSpqajvVtt6mlYpA=
 github.com/gobuffalo/tags/v3 v3.1.2 h1:68sHcwFFDstXyfbk5ovbGcQFDsupgVLs+lw1XZinHJw=
 github.com/gobuffalo/tags/v3 v3.1.2/go.mod h1:o3ldUfKv50jxWAC8eZHXMm8dnKW3YvyZUMr0xqUcZTI=
@@ -147,8 +148,9 @@ github.com/gobuffalo/validate/v3 v3.3.1/go.mod h1:Ehu8ieNJQuUM4peDDr/0VapzdGA7Rg
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
-github.com/gofrs/uuid v4.1.0+incompatible h1:sIa2eCvUTwgjbqXrPLfNwUf9S3i3mpH1O1atV+iL/Wk=
 github.com/gofrs/uuid v4.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
+github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -261,8 +263,9 @@ github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfG
 github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
 github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
 github.com/jackc/pgconn v1.10.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
-github.com/jackc/pgconn v1.10.1 h1:DzdIHIjG1AxGwoEEqS+mGsURyjt4enSmqzACXvVzOT8=
 github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
+github.com/jackc/pgconn v1.11.0 h1:HiHArx4yFbwl91X3qqIHtUFoiIfLNJXCQRsnzkiwwaQ=
+github.com/jackc/pgconn v1.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
 github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
 github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
 github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
@@ -278,25 +281,29 @@ github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod
 github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
 github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
 github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
-github.com/jackc/pgproto3/v2 v2.1.1 h1:7PQ/4gLoqnl87ZxL7xjO0DR5gYuviDCZxQJsUlFW1eI=
 github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns=
+github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
 github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
 github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
 github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
 github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
 github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
 github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
-github.com/jackc/pgtype v1.8.1 h1:9k0IXtdJXHJbyAWQgbWr1lU+MEhPXZz6RIXxfR5oxXs=
 github.com/jackc/pgtype v1.8.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
+github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38=
+github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
 github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
 github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
 github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
 github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
-github.com/jackc/pgx/v4 v4.13.0 h1:JCjhT5vmhMAf/YwBHLvrBn4OGdIQBiFG6ym8Zmdx570=
 github.com/jackc/pgx/v4 v4.13.0/go.mod h1:9P4X524sErlaxj0XSGZk7s+LD0eOyu1ZDUrrpznYDF0=
+github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w=
+github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw=
 github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w=
 github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
 github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
@@ -325,8 +332,9 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
 github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ=
+github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/luna-duclos/instrumentedsql v1.1.3 h1:t7mvC0z1jUt5A0UQ6I/0H31ryymuQRnJcWCiqV3lSAA=
 github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs=
 github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
diff --git a/worker/simple.go b/worker/simple.go
index 3f5788d4..76d9ef67 100644
--- a/worker/simple.go
+++ b/worker/simple.go
@@ -15,6 +15,9 @@ var _ Worker = &Simple{}
 // NewSimple creates a basic implementation of the Worker interface
 // that is backed using just the standard library and goroutines.
 func NewSimple() *Simple {
+	// TODO(sio4): #road-to-v1 - how to check if the worker is ready to work
+	// when worker should be initialized? how to check if worker is ready?
+	// and purpose of the context
 	return NewSimpleWithContext(context.Background())
 }
 
@@ -49,6 +52,10 @@ type Simple struct {
 
 // Register Handler with the worker
 func (w *Simple) Register(name string, h Handler) error {
+	if name == "" || h == nil {
+		return fmt.Errorf("name or handler cannot be empty/nil")
+	}
+
 	w.moot.Lock()
 	defer w.moot.Unlock()
 	if _, ok := w.handlers[name]; ok {
@@ -60,6 +67,7 @@ func (w *Simple) Register(name string, h Handler) error {
 
 // Start the worker
 func (w *Simple) Start(ctx context.Context) error {
+	// TODO(sio4): #road-to-v1 - define the purpose of Start clearly
 	w.Logger.Info("starting Simple background worker")
 
 	w.ctx, w.cancel = context.WithCancel(ctx)
@@ -68,20 +76,30 @@ func (w *Simple) Start(ctx context.Context) error {
 
 // Stop the worker
 func (w *Simple) Stop() error {
+	// prevent job submission when stopping
+	w.moot.Lock()
+	defer w.moot.Unlock()
+
 	w.Logger.Info("stopping Simple background worker")
 
+	w.cancel()
+
 	w.wg.Wait()
 	w.Logger.Info("all background jobs stopped completely")
-	w.cancel()
 	return nil
 }
 
 // Perform a job as soon as possibly using a goroutine.
 func (w *Simple) Perform(job Job) error {
+	// Perform should not allow a job submission if the worker is not running
+	if err := w.ctx.Err(); err != nil {
+		return fmt.Errorf("worker is not ready to perform a job: %v", err)
+	}
+
 	w.Logger.Debugf("performing job %s", job)
 
 	if job.Handler == "" {
-		err := fmt.Errorf("no handler name given for %s", job)
+		err := fmt.Errorf("no handler name given: %s", job)
 		w.Logger.Error(err)
 		return err
 	}
@@ -89,7 +107,7 @@ func (w *Simple) Perform(job Job) error {
 	w.moot.Lock()
 	defer w.moot.Unlock()
 	if h, ok := w.handlers[job.Handler]; ok {
-		// TODO: consider to implement timeout and/or cancellation
+		// TODO(sio4): #road-to-v1 - consider timeout and/or cancellation
 		w.wg.Add(1)
 		go func() {
 			defer w.wg.Done()
@@ -118,11 +136,20 @@ func (w *Simple) PerformAt(job Job, t time.Time) error {
 // PerformIn performs a job after waiting for a specified amount
 // using a goroutine.
 func (w *Simple) PerformIn(job Job, d time.Duration) error {
+	// Perform should not allow a job submission if the worker is not running
+	if err := w.ctx.Err(); err != nil {
+		return fmt.Errorf("worker is not ready to perform a job: %v", err)
+	}
+
+	w.wg.Add(1) // waiting job also should be counted
 	go func() {
+		defer w.wg.Done()
+
 		select {
 		case <-time.After(d):
 			w.Perform(job)
 		case <-w.ctx.Done():
+			// TODO(sio4): #road-to-v1 - it should be guaranteed to be performed
 			w.cancel()
 		}
 	}()


Test patch:
diff --git a/default_context_test.go b/default_context_test.go
index 2823828c..20378164 100644
--- a/default_context_test.go
+++ b/default_context_test.go
@@ -143,6 +143,15 @@ func Test_DefaultContext_GetSet(t *testing.T) {
 	r.Equal("Mark", c.Value("name").(string))
 }
 
+func Test_DefaultContext_Set_Unconfigured(t *testing.T) {
+	r := require.New(t)
+	c := DefaultContext{}
+
+	c.Set("name", "Yonghwan")
+	r.NotNil(c.Value("name"))
+	r.Equal("Yonghwan", c.Value("name").(string))
+}
+
 func Test_DefaultContext_Value(t *testing.T) {
 	r := require.New(t)
 	c := basicContext()
@@ -151,7 +160,12 @@ func Test_DefaultContext_Value(t *testing.T) {
 	c.Set("name", "Mark")
 	r.NotNil(c.Value("name"))
 	r.Equal("Mark", c.Value("name").(string))
-	r.Equal("Mark", c.Value("name").(string))
+}
+
+func Test_DefaultContext_Value_Unconfigured(t *testing.T) {
+	r := require.New(t)
+	c := DefaultContext{}
+	r.Nil(c.Value("name"))
 }
 
 func Test_DefaultContext_Render(t *testing.T) {
@@ -301,3 +315,68 @@ func Test_DefaultContext_Bind_JSON(t *testing.T) {
 
 	r.Equal("Mark", user.FirstName)
 }
+
+func Test_DefaultContext_Data(t *testing.T) {
+	r := require.New(t)
+	c := basicContext()
+
+	r.EqualValues(map[string]interface{}{}, c.Data())
+}
+
+func Test_DefaultContext_Data_Unconfigured(t *testing.T) {
+	r := require.New(t)
+	c := DefaultContext{}
+
+	r.EqualValues(map[string]interface{}{}, c.Data())
+}
+
+func Test_DefaultContext_String(t *testing.T) {
+	r := require.New(t)
+	c := basicContext()
+	c.Set("name", "Buffalo")
+	c.Set("language", "go")
+
+	r.EqualValues("language: go\n\nname: Buffalo", c.String())
+}
+
+func Test_DefaultContext_String_EmptyData(t *testing.T) {
+	r := require.New(t)
+	c := basicContext()
+	r.EqualValues("", c.String())
+}
+
+func Test_DefaultContext_String_EmptyData_Unconfigured(t *testing.T) {
+	r := require.New(t)
+	c := DefaultContext{}
+
+	r.EqualValues("", c.String())
+}
+
+func Test_DefaultContext_MarshalJSON(t *testing.T) {
+	r := require.New(t)
+	c := basicContext()
+	c.Set("name", "Buffalo")
+	c.Set("language", "go")
+
+	jb, err := c.MarshalJSON()
+	r.NoError(err)
+	r.EqualValues(`{"language":"go","name":"Buffalo"}`, string(jb))
+}
+
+func Test_DefaultContext_MarshalJSON_EmptyData(t *testing.T) {
+	r := require.New(t)
+	c := basicContext()
+
+	jb, err := c.MarshalJSON()
+	r.NoError(err)
+	r.EqualValues(`{}`, string(jb))
+}
+
+func Test_DefaultContext_MarshalJSON_EmptyData_Unconfigured(t *testing.T) {
+	r := require.New(t)
+	c := DefaultContext{}
+
+	jb, err := c.MarshalJSON()
+	r.NoError(err)
+	r.EqualValues(`{}`, string(jb))
+}
diff --git a/worker/simple_test.go b/worker/simple_test.go
index 6372d60c..7abf0b49 100644
--- a/worker/simple_test.go
+++ b/worker/simple_test.go
@@ -1,6 +1,7 @@
 package worker
 
 import (
+	"context"
 	"sync"
 	"testing"
 	"time"
@@ -8,76 +9,188 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+func sampleHandler(Args) error {
+	return nil
+}
+
+func Test_Simple_RegisterEmpty(t *testing.T) {
+	r := require.New(t)
+
+	w := NewSimple()
+	err := w.Register("", sampleHandler)
+	r.Error(err)
+}
+
+func Test_Simple_RegisterNil(t *testing.T) {
+	r := require.New(t)
+
+	w := NewSimple()
+	err := w.Register("sample", nil)
+	r.Error(err)
+}
+
+func Test_Simple_RegisterEmptyNil(t *testing.T) {
+	r := require.New(t)
+
+	w := NewSimple()
+	err := w.Register("", nil)
+	r.Error(err)
+}
+
+func Test_Simple_RegisterExisting(t *testing.T) {
+	r := require.New(t)
+
+	w := NewSimple()
+	err := w.Register("sample", sampleHandler)
+	r.NoError(err)
+
+	err = w.Register("sample", sampleHandler)
+	r.Error(err)
+}
+
+func Test_Simple_StartStop(t *testing.T) {
+	r := require.New(t)
+
+	w := NewSimple()
+	ctx := context.Background()
+	err := w.Start(ctx)
+	r.NoError(err)
+	r.NotNil(w.ctx)
+	r.Nil(w.ctx.Err())
+
+	err = w.Stop()
+	r.NoError(err)
+	r.NotNil(w.ctx)
+	r.NotNil(w.ctx.Err())
+}
+
 func Test_Simple_Perform(t *testing.T) {
 	r := require.New(t)
 
 	var hit bool
-	wg := &sync.WaitGroup{}
-	wg.Add(1)
 	w := NewSimple()
+	r.NoError(w.Start(context.Background()))
+
 	w.Register("x", func(Args) error {
 		hit = true
-		wg.Done()
 		return nil
 	})
 	w.Perform(Job{
 		Handler: "x",
 	})
-	wg.Wait()
+
+	// the worker should guarantee the job is finished before the worker stopped
+	r.NoError(w.Stop())
 	r.True(hit)
 }
 
-func Test_Simple_PerformAt(t *testing.T) {
+func Test_Simple_PerformBroken(t *testing.T) {
 	r := require.New(t)
 
 	var hit bool
-	wg := &sync.WaitGroup{}
-	wg.Add(1)
 	w := NewSimple()
+	r.NoError(w.Start(context.Background()))
+
 	w.Register("x", func(Args) error {
 		hit = true
-		wg.Done()
+
+		//Index out of bounds on purpose
+		println([]string{}[0])
+
 		return nil
 	})
-	w.PerformAt(Job{
+	w.Perform(Job{
 		Handler: "x",
-	}, time.Now().Add(5*time.Millisecond))
-	wg.Wait()
+	})
+
+	r.NoError(w.Stop())
 	r.True(hit)
 }
 
-func Test_Simple_PerformBroken(t *testing.T) {
+func Test_Simple_PerformWithEmptyJob(t *testing.T) {
+	r := require.New(t)
+
+	w := NewSimple()
+	r.NoError(w.Start(context.Background()))
+	defer w.Stop()
+
+	err := w.Perform(Job{})
+	r.Error(err)
+}
+
+func Test_Simple_PerformWithUnknownJob(t *testing.T) {
+	r := require.New(t)
+
+	w := NewSimple()
+	r.NoError(w.Start(context.Background()))
+	defer w.Stop()
+
+	err := w.Perform(Job{Handler: "unknown"})
+	r.Error(err)
+}
+
+/* TODO(sio4): #road-to-v1 - define the purpose of Start clearly
+   consider to make Perform to work only when the worker is started.
+func Test_Simple_PerformBeforeStart(t *testing.T) {
+	r := require.New(t)
+
+	w := NewSimple()
+	r.NoError(w.Register("sample", sampleHandler))
+
+	err := w.Perform(Job{Handler: "sample"})
+	r.Error(err)
+}
+*/
+
+func Test_Simple_PerformAfterStop(t *testing.T) {
+	r := require.New(t)
+
+	w := NewSimple()
+	r.NoError(w.Register("sample", sampleHandler))
+	r.NoError(w.Start(context.Background()))
+	r.NoError(w.Stop())
+
+	err := w.Perform(Job{Handler: "sample"})
+	r.Error(err)
+}
+
+func Test_Simple_PerformAt(t *testing.T) {
 	r := require.New(t)
 
 	var hit bool
+	w := NewSimple()
+	r.NoError(w.Start(context.Background()))
+
 	wg := &sync.WaitGroup{}
 	wg.Add(1)
 
-	w := NewSimple()
 	w.Register("x", func(Args) error {
 		hit = true
 		wg.Done()
-
-		//Index out of bounds on purpose
-		println([]string{}[0])
-
 		return nil
 	})
-
-	w.Perform(Job{
+	w.PerformAt(Job{
 		Handler: "x",
-	})
+	}, time.Now().Add(5*time.Millisecond))
+
+	// how long does the handler take for assignment? hmm,
+	time.Sleep(100 * time.Millisecond)
 	wg.Wait()
 	r.True(hit)
+
+	r.NoError(w.Stop())
 }
 
 func Test_Simple_PerformIn(t *testing.T) {
 	r := require.New(t)
 
 	var hit bool
+	w := NewSimple()
+	r.NoError(w.Start(context.Background()))
+
 	wg := &sync.WaitGroup{}
 	wg.Add(1)
-	w := NewSimple()
+
 	w.Register("x", func(Args) error {
 		hit = true
 		wg.Done()
@@ -86,14 +199,61 @@ func Test_Simple_PerformIn(t *testing.T) {
 	w.PerformIn(Job{
 		Handler: "x",
 	}, 5*time.Millisecond)
+
+	// how long does the handler take for assignment? hmm,
+	time.Sleep(100 * time.Millisecond)
 	wg.Wait()
 	r.True(hit)
+
+	r.NoError(w.Stop())
 }
 
-func Test_Simple_NoHandler(t *testing.T) {
+/* TODO(sio4): #road-to-v1 - define the purpose of Start clearly
+   consider to make Perform to work only when the worker is started.
+func Test_Simple_PerformInBeforeStart(t *testing.T) {
 	r := require.New(t)
 
 	w := NewSimple()
-	err := w.Perform(Job{})
+	r.NoError(w.Register("sample", sampleHandler))
+
+	err := w.PerformIn(Job{Handler: "sample"}, 5*time.Millisecond)
+	r.Error(err)
+}
+*/
+
+func Test_Simple_PerformInAfterStop(t *testing.T) {
+	r := require.New(t)
+
+	w := NewSimple()
+	r.NoError(w.Register("sample", sampleHandler))
+	r.NoError(w.Start(context.Background()))
+	r.NoError(w.Stop())
+
+	err := w.PerformIn(Job{Handler: "sample"}, 5*time.Millisecond)
 	r.Error(err)
 }
+
+/* TODO(sio4): #road-to-v1 - it should be guaranteed to be performed
+   consider to make PerformIn to guarantee the job execution
+func Test_Simple_PerformInFollowedByStop(t *testing.T) {
+	r := require.New(t)
+
+	var hit bool
+	w := NewSimple()
+	r.NoError(w.Start(context.Background()))
+
+	w.Register("x", func(Args) error {
+		hit = true
+		return nil
+	})
+	err := w.PerformIn(Job{
+		Handler: "x",
+	}, 5*time.Millisecond)
+	r.NoError(err)
+
+	// stop the worker immediately after PerformIn
+	r.NoError(w.Stop())
+
+	r.True(hit)
+}
+*/