4 out of #100DaysOfCode - Data singleton

2020-01-06

Written by Guillaume Moigneu

In my previous post, I was loading the fixtures data on every request. While this allow to always use the most up to date data, this is has no value in our context as the data does not change.

How can we make sure we always reuse the same fixtures data for all requests? Let’s go over an implementation of the singleton pattern in Golang.

First, let modify our action to only use the data to generate our users. Then we need to get that data. We will use another package to isolate the different concerns:

	import (
		...
		data "randomdata/data"
	)

	func UsersHandler(c buffalo.Context) error {
		...
		// Load required locales values
		data.Load(c)
		locales := data.Locales()
		firstnames, lastnames := data.Users()
		...
	}

Each request will get the data from this package. But now we need to make sure we reuse the same data. In that new package, we will implement once.Do:

	package data

	import (
		"github.com/gobuffalo/buffalo"
		"github.com/gobuffalo/pop"
		"randomdata/models"
		"github.com/pkg/errors"
		"sync"
		"fmt"
	)

	var once sync.Once
	type firstnamesType map[string][]models.Firstname
	type lastnamesType map[string][]models.Lastname
	type localesType []models.Locale

	var (
		firstnames firstnamesType
		lastnames lastnamesType
		locales localesType
	)

	func Load(c buffalo.Context) error {

		var err error

		once.Do(func() {
			firstnames = make(map[string][]models.Firstname)
			lastnames = make(map[string][]models.Lastname)

			tx, ok := c.Value("tx").(*pop.Connection) // you get your connection here
			if !ok {
				err = errors.WithStack(errors.New("no transaction found"))
			}

			fmt.Println("=====")
			fmt.Println("Querying locales")
			fmt.Println("=====")

			tx.All(&locales)

			fmt.Println("=====")
			fmt.Println("Querying users")
			fmt.Println("=====")
			
			// Load required locales values
			
			f := []models.Firstname{}
			l := []models.Lastname{}
			for _, locale := range locales {
				q := tx.Where("locale = ?", locale.Code)
				if len(c.Param("gender")) > 0 {
					q.Where("gender = ?", c.Param("gender"))
				}
				q.All(&f)
				firstnames[locale.Code] = f

				q = tx.Where("locale = ?", locale.Code)
				q.All(&l)
				lastnames[locale.Code] = l
			}
		})

		return err
	}

	func Users() (firstnamesType, lastnamesType) {
		return firstnames, lastnames
	}

	func Locales() (localesType) {
		return locales
	}

Everything in the anonymous func wrapped by once.Do will only be executed one time - on the first request. So even if we call Load thousand times, it will be only run once.

The results are impressive. Now that all my content is only loaded on the first request.

If we retest with ab, we now have the following results:

Requests per second:    6628.71 [#/sec] (mean)
Time per request:       1.509 [ms] (mean)

6628 requests per second is definitely better than the 50 rps we got while querying the db on each run.