Notes

/Berlin Go Homelab Kubernetes Everything

Development environment in 2020

Development environment in 2020

Last year I made this one, after involving myself into a very random discussion on Twitter, about the unnecessary complexity and none-sense of modern days’ development environments.

Little things of Go HTTP handlers

Every time I sketch an HTTP API in Go, I wrap the code of request handlers around these small but very convenient bits.

My handlers are methods or functions, that serve a request and, either write a (positive) response or return an error.

// HandlerFunc is an HTTP handler function, that handles an HTTP request.
// It writes the response to http.ResponseWriter or returns an error.
type HandlerFunc func(w http.ResponseWriter, r *http.Request) error

// Handler is an adaptor for HandlerFunc, that converts the handler into http.Handler.
// It makes sure all errors returned from h are handled in a consistent manner.
func Handler(h HandlerFunc) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        err := h(w, r)
        if err != nil {
            handleError(w, r, err)
        }
    })
}

An error returned from HandlerFunc can be an indicator of a failure in request processing, statusError, or a general “something didn’t work”-error. The later can contain the internal details, that the API must never expose to the user.

// StatusError wraps an error err and contains the suggestion regarding to
// how the error should be communicated to the user.
//
// code must be a valid HTTP status code; text is the message to reply to user.
func StatusError(code int, text string, err error) error {
    return &statusError{
        Code: code,
        Text: text,
        Err:  err,
    }
}

type statusError struct {
    Code int
    Text string
    Err  error
}

func (s statusError) Error() string {
    return s.Text
}

func (s statusError) Unwrap() error {
    return s.Err
}

handleError is a helper function, which makes sure all errors returned from HandlerFunc are handled and replied to the user consistently. The internal details — the cause of the error — aren’t exposed to the user, but the helper can provide a unified logging and metrics, which would be convenient when debugging the error later:

var ErrNotFound = StatusError(http.StatusNotFound, "Nothing found", nil)

func handleError(w http.ResponseWriter, r *http.Request, err error) {
    var statusErr *statusError
    if !errors.As(err, &statusErr) {
        statusErr = &statusError{Code: http.StatusInternalServerError, Text: "Internal server error", Err: err}
    }

    rid := RequestIDFromContext(r.Context())
    resp := errResponse{
        RequestID: rid,
        Error:     statusErr.Text,
    }
    replyJSON(w, resp, statusErr.Code)

    // underlying error can be nil, as a special case, when the error is a client-side problem
    if err := errors.Unwrap(statusErr); err != nil {
        log.Errorw("request failed", "request-id", rid, "uri", r.RequestURI, "error", err)
    }
}

replyJSON is a helper function, which writes a JSON string to http.ResponseWriter, setting the proper HTTP headers.

func replyJSON(w http.ResponseWriter, v interface{}, code int) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    err := json.NewEncoder(w).Encode(v)
    if err != nil {
        io.WriteString(w, `{"code":500,"error":`+strconv.Quote(err.Error())+`}`)
    }
}

How does it look in practice?

Below is an extract from a hypothetical API, with one single route /api/login, that takes an email, and replies with JSON, that contains this account’s ID.

func setupRoutes(mux *http.ServeMux) {
    authHandler := NewAuthHandler(···)
    mux.Handle("/api/login", Handler(authHandler.HandleLogin))
}

type AuthHandler {
    // internal dependencies
}

func (h *AuthHandler) HandleLogin(w http.ResponseWriter, r *http.Request) error {
    ctx := r.Context()

    req, err := DecodeLoginRequest(r)
    if err != nil {
        return fmt.Errorf("decode login request: %w", err)
    }
    if req.Email == "" {
        return StatusError(http.StatusBadRequest, "email is required", nil)
    }

    accID, err := h.datastore.GetAccountByEmail(ctx, req.Email)
    if errors.Is(err, ErrNotExists) {
        return StatusError(http.StatusForbidden, "account does not exist", err)
    }
    if err != nil {
        return fmt.Errorf("get account for %q: %w", req.Email, err)
    }

    resp := struct {
        ID string `json:"id"`
    }{
        ID: accID,
    }
    // ReplyJSON is a wrapper around internal replyJSON, that always responses with http.StatusOK
    ReplyJSON(w, resp)

    return nil
}

Do you have your own little things, that help you to lay out the boilerplate? Discuss this note on Twitter or Reddit.

Go

(You don't) Insert unicode NULL character as Postgres jsonb

With JSON data type it’s easy to treat Postgres as a document database, which doesn’t need strong schema. One can define a table, that has a field of a type jsonb and insert any valid JSON string (a “document”).

I’ve learned lately, that Postgres’s jsonb prohibits insertion of a valid JSON string if the string contains NULL (U+0000) character. Postgres’s own docs on JSON Types says:

RFC 7159 permits JSON strings to contain Unicode escape sequences denoted by \uXXXX. In the input function for the json type, Unicode escapes are allowed regardless of the database encoding, and are checked only for syntactic correctness. However, the input function for jsonb is stricter: it disallows Unicode escapes for non-ASCII characters (those above U+007F) unless the database encoding is UTF8. The jsonb type also rejects \u0000 (because that cannot be represented in PostgreSQL’s text type), and it insists that any use of Unicode surrogate pairs to designate characters outside the Unicode Basic Multilingual Plane be correct.

In my case, a Go backend inserts tracing logs to Postgres. A trace consists of multiple “spans”, some of which can contain the reply from an external API. As we found out, sometimes, in the event of a failure, the API replies with an empty GIF <facepalm/>. Our backend converts the response to a string, marshals it to a JSON and later tries to insert the JSON into a Postgres table.

Consider the following Go code:

// data is an empty GIF
var data = []byte{
    0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00,
    0x01, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
    0xff, 0xff, 0xff, 0x21, 0xf9, 0x04, 0x01, 0x00,
    0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00,
    0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x01, 0x44,
    0x00, 0x3b,
}

func main() {
    v, _ := json.Marshal(struct {
        Resp interface{} `json:"resp,omitempty"`
    }{
        Resp: string(data),
    })
    fmt.Printf("%s\n", v)
    // Output (truncated for readability):
    // {"resp":"GIF89a\u0001\u0000\u0001\u0000\ufffd\···\u0001D\u0000;"}
}

Above, json.Marshal produces a perfectly valid JSON. But if I try to insert it into a Postgres table as jsonb, the insert fails with “unsupported Unicode escape sequence”:

= CREATE TABLE logs (data jsonb);
= INSERT INTO logs VALUES ('{"resp":"GIF89a\u0001\u0000\u0001\u0000\ufffd\···\u0001D\u0000;"}');

ERROR:  unsupported Unicode escape sequence
LINE 1: insert into logs values ('{"resp":"GIF89a\u0001\u0000\u0001\...
                                 ^
DETAIL:  \u0000 cannot be converted to text.
CONTEXT:  JSON data, line 1: {"resp":...

Because in my code, there were only a couple of places where I didn’t control the actual data, that went into a span, the way I’ve chosen to handle that was by introducing a wrapper type, that implements json.Marshaller. The wrapper checks the value is a valid UTF-8 sequence and doesn’t contain NULL character before it marshals the value into a JSON string. If the value is not a valid UTF-8, the marshaller sees it as a binary data and base64-encodes it.

// RawText handles invalid UTF-8 and NULL-bytes, encoding them as base64-string.
// Because we have to make sure the resulting JSON will be compatible with Postgres's jsonb,
// we must use RawText when we don't control the data, e.g. when log the error from an external API.
// Refer to https://www.postgresql.org/docs/10/datatype-json.html
type RawText []byte

func (v RawText) MarshalEasyJSON(w *Writer) {
    if utf8.Valid(v) && !bytes.ContainsRune(v, '\u0000') {
        // "valid" text is marshalled as string
        w.String(string(v))
    } else {
        // "invalid" text is marshalled as binary data
        w.Raw(json.Marshal([]byte(v)))
    }
}

Note, the code above is a marshaller for github.com/mailru/easyjson, which we use in the project.

Here is how it looks in practice:

func main() {
    v, _ := json.Marshal(struct {
        Resp1 interface{} `json:"resp1,omitempty"`
        Resp2 interface{} `json:"resp2,omitempty"`
    }{
        Resp1: RawText(bin),             // wrap the bin data into RawText
        Rest2: RawText("normal string"), // wrap (copy) a string into RawText
    })
    fmt.Printf("%s\n", v)
    // Output:
    // {
    // "resp1":"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
    // "resp2":"normal string"
    // }
}

2020, what a year

2020 was (technically, still is) an extraordinary year. For many people, this was the worst, the weirdest, the most depressing year; the year we wish was a bad dream. With all the respect for the terrible things, lots of people went through, for me, it was a year to remember.

In 2020 I learned how to work from home. It’s still not super fun, but it’s possible. As I look at that now, there are obvious benefits: I can start and end my day much more freely; I currently don’t feel much pressure when willing to go for a walk or a run at 11:00, or when I finish my workday at 16:00 (I tend to start around 8:30 while at home). But that being said, I still don’t consider staying full-remote and I want to go back to the office, co-working, or a random coffee bar.

In 2020 I bought a bicycle. After three years in Berlin, I’m with “all of those people” now :) Can’t say I’m super excited about the purchase. Mostly because I live in the walking distance from the office. With the bicycle, my “commute” is only five or seven minutes shorter. I have to wait at the traffic lights a lot; plus I have to spend extra minutes to lock/unlock the bicycle.

In 2020 I walked, on average, one kilometre per day more, than in the previous year (5.5 km vs 4.2 km). A bit of surprise but, overall this year, I walked less than I did in 2019. I bet that’s because this year we, obviously, didn’t travel much, so we didn’t have a chance to spend weeks walking around a new city, neither in Spring, nor on Christmas and New Year.

In 2020 we adopted a cat. The CAT, who’s now called Gagarin! He came to us from the middle of “nowhere-Russia” as a frightened little kitten and spent the first month under our bed, avoiding walking under the light. And now, after four months, he comes to have a nap on our pillow (and on my head) at 7:00, as his way to signal that it’s time to feed him.

In 2020 I assembled my four nodes Raspberry Pi Kubernetes cluster, but I haven’t made much use of it yet. As promised to myself in December 2019, during the year I learned and touched some new Kubernetes details, and I have tons of things I still want to get my hands into to understand on a more in-depth level of details.

In 2020 I started to learn Rust. I tried learning it in 2018 and in 2019. But this year, I actually spent a month or two writing code, not only reading/watching/listening to/following the content. It feels refreshing. Programming for embedded devices and game-dev are the most attractive fields that I want to explore further with Rust.

In 2020 I had two job interviews. None of them were successful for me, although I tend to believe that I’ve passed both. I have plans to draft a note about the essential questions one must ask the HR during the first call, somewhere later.

In 2020 I did and learned, and read, and listened to, and watched, and thought about a bunch of things.

In 2021 I will do even better.

On code-reviews

This is another thought experiment, this time on the importance of my feedback in the code-reviews.

Providing you’re working on a project maintained by a set team of N people. What would happen with the codebase if, for six months, in code-reviews, you started to accept changes for which, you generally leave feedback?

“Variables, struct or function names are named poorly” — accept. “We already have a package, that solves a similar problem” — accept. “A change brings inconsistency to the codebase” — accept. “The pattern doesn’t belong here” — accept.

Go is a famously opinionated programming language. But does being opinionated help and scale outside of the language and its standard library?

Do you have the answer? Discuss this note on r/golang Reddit or share your thoughts on Twitter.

A thought experiment on Apple M1

With Apple’s new M1 Macs showing (reportedly) huge performance improvement, compared to “old”, Intel-based Macs, I wonder what would hold Qualcomm (Snapdragon CPU) and others from doing “the same” and moving into laptop/desktop territory?

Microsoft already has Surface Pro X — an ARM-based Windows computer. They also have a version of Win10 for ARM, that one can even run on Raspberry Pi (still beta quality, I believe). Could 2021 become the year of ARM on desktop?

Even more interesting, Amazon’s Graviton is showing (again, reportedly) an excellent performance, while staying at reasonably low cost. What would stop Google/Microsoft from moving into ARM-based CPU territory for their clouds?

Alternative font-variant in VS Code

I’m not a big fun of the aesthetics of VS Code, at least not on macOS. In particular, I don’t like the way how its code editor renders fonts. But today I learned!

To make the fonts look better — I’m on macOS — set an alternative font-variant, e.g. “Comic Code Ligatures Medium” instead of “Regular”, in the editor’s settings. To specify font-variant, remove spaces in between the name of the font and pass the variant after the hyphen. That is, instead of 'Comic Code Ligatures', set the following:

"editor.fontFamily": "'ComicCodeLigatures-Medium', monospace"

Full disclosure, my IDE of choice is GoLand, and it has been so since it was only a plugin for IntelliJ IDEA. Even though I can comfortably use VS Code or Vim when I need to make a small change or look something up in the code, I need the IDE to effectively work on a large codebase.

The backstory for this note is that I’m working on a small TypeScript/React application in my spare time this month. For something that small, VS Code works great fine. In fact, I’m writing this particular note in VS Code too ;)

One year in production

It’s one year since I posted the very first note here. Unbelivable! Despite my own concerns I did published random posts over the course of the previous twelve months.

For the next round I came up with some personal goals:

  1. Keep posting.
  2. Work on your grammar.
  3. Add “Archive” and “Tags” sections.
  4. Find a better approach for managing drafts.
  5. Bring back the dark theme but figure out what to do with the illustrations.
  6. Keep posting ;)

Does profefe prefers "push" over "pull"?

The main component of profefe, a system for continuous profiling, is profefe-collector — a service that receives profiling data, taken from an application and persists the data in collector’s storage (design document describes it in more details). Receiving data from an external source (for example, profefe-agent), indicates that profefe, as an observability system, prefers “push” model. Why not “pulling” the profiles directly from the application?

Both push and pull models have their benefits and drawbacks.

A collector that pulls profiling data from running applications could simplify integration into existing infrastructure because there would be no need in making changes in the applications that already exposed pprof HTTP endpoint. Making sure that every application integrated and configured profefe-agent would be a challenging job in a large organisation.

On the other hand, pull model requires pprof servers to be exposed and available for the collector, so it could fetch (pull) profiling data. That can also be challenging in the deployments, where applications are collocated on the bare-metal machines. Every application (application’s instance) would have to communicate a unique TCP port for its pprof server.

To work as a pull-system, the collector must be able to discover the pprof servers, thus it requires a mechanism for service discovery (SD), to be usable at scale. Unfortunately, there isn’t a universal SD protocol or a provider, an observability system could be built upon.

Prometheus, the best example of an open-source system, which uses pull model for data collection, have to support several different SD systems in their code base. At some point they ended up introducing their own general protocol, that expects a “middle-man-service”, which translates the data from a SD system into a list of Prometheus targets (Update, this comment from u/bbrazil does a better job explaining the state of SD in Prometheus). There is no clear way for an open-source system to be both flexible and don’t end up being a pile of “plugins”, that no one is willing to maintain or break.

From the start of profefe project, several years ago, I had the idea that translating push into pull would be easier for an end-user. That is if a small deployment already exposes a pprof server, writing an external job that pulls the profiles from the applications, annotates them with meta-data, and pushes the data into the collector, can be as easy as spawning a cronjob in a sidecar. kube-profefe solves that nicely for deployments running in Kubernetes. At some point, I hoped to come up with something similar for Nomad+Consul if the experiments ended successfully.

Translating pull into push is a similarly possible but because profefe didn’t have to support any SD mechanisms from the start, that simplified the overall code base and allowed us to focus on the collector and the API for profiles quering.

profefe-collector does uses push model. But one can deploy profefe so it reflected the use cases your organisation has.

Do you use continuous profiling? Let me know about your experience. Share your thoughts on Twitter or discuss on r/golang.

How to design a good API

A good API is designed around the use-case. A poorly designed, around the API’s implementation details.

What's in your main-dot-go? (aka Go Project boilerplate)

Sometimes I write small services in Go from scratch. And every time main.go ends up looking almost the same:

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	sigs := make(chan os.Signal, 2)
	signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)

	go func() {
		<-sigs
		signal.Stop(sigs)
		cancel()
	}()

	if err := run(ctx, os.Args[1:]); err != nil {
		log.Fatalln(err)
	}
}

type Config struct {
	HTTPAddr            string
	HTTPShutdownTimeout time.Duration
}

func run(ctx context.Context, args []string) error {
	flags := flag.NewFlagSet("", flag.ExitOnError)

	var conf Config
	flags.StringVar(&conf.HTTPAddr, "http-addr", "127.0.0.1:10080", "address to listen on")
	flags.DurationVar(&conf.HTTPShutdownTimeout, "http-shutdown-timeout", 5*time.Second, "server shutdown timeout")

	if err := flags.Parse(args); err != nil {
		return err
	}

	// TODO: define the handler, the routing, and wire the dependencies with the main context
	mux := http.NewServeMux()
	server := &http.Server{
		Addr:    conf.HTTPAddr,
		Handler: mux,
	}

	errs := make(chan error, 1)
	go func() {
		log.Printf("starting: addr %s", server.Addr)
		errs <- server.ListenAndServe()
	}()

	select {
	case <-ctx.Done():
		log.Println("exiting...")
	case err := <-errs:
		return err
	}

	// create new context because top-most one is already canceled
	ctx, cancel := context.WithTimeout(context.Background(), conf.HTTPShutdownTimeout)
	defer cancel()

	return server.Shutdown(ctx)
}

Of course, not every service requires an HTTP server, but the general idea stands.


When Go will introduce signal.NotifyContext(), the signals handling in main() function will be much smaller (we’re at go1.15rc1 as I’m writing that and the change hasn’t landed into the release yet).

I love how transparent is the flow here and how everything is scoped inside run() function. This structure forces you to eliminate global state, making unit or integration testing almost trivial — at least, in theory ;)

It might feel like too much of boilerplate code for a “small” service. In practice, though, I don’t recall any time this caused any real troubles to me. The beauty of Go is in explicitness.

Go

Waveshare ESP8266 Driver Board Pins Mapping

I’ve been playing with an e-paper ESP866 Driver Board, and a 2.7" E-Ink display from Waveshare. Arduino C++ looks manageable. One strange thing, though. In both board’s documentation and GxEPD2 library’s examples, they say the display is connected to pins as BUSY → GPIO16, RST → GPIO5, DC → GPIO4, CS → GPIO15. This mapping seems wrong.

After digging through the code examples from Waveshare’s Wiki, the correct mapping is the following:

BUSY → GPIO5, RST → GPIO2, DC → GPIO4, CS → GPIO15

That’s how the initialisation of the main GxEPD2 class for my 2.7" display looks like now:

#define ENABLE_GxEPD2_GFX 0
#include <GxEPD2_BW.h>

// mapping of Waveshare e-Paper ESP8266 Driver Board
GxEPD2_BW<GxEPD2_270, GxEPD2_270::HEIGHT> display(GxEPD2_270(/*CS=15*/ SS, /*DC=4*/ 4, /*RST=2*/ 2, /*BUSY=5*/ 5));

Sticky headers? Please don't

Sticky (or “fixed”) headers are everywhere. It feels that, nowadays, every web designer’s first attempt to site’s navigation starts with a sticky header. I hate this.

Keep reading, the note has images…

Owner of Logging Context

There’s the late-night dilemma…

Who should be in charge of logging context: a component’s owner or the component itself?

type Logger interface {
	With(...kvpairs) Logger
}

type Storage struct {
	logger Logger
}

// OPTION 1: component's owner defines the context of component's logger
func main() {
	_ = NewStorage(logger.With("component", "storage"))
}

// OPTION 2: component itself is in charge of its logging context
func NewStorage(logger Logger) (st *Storage) {
	return &Storage{
		logger: logger.With("component", "storage"),
	}
}

Fun fact: a couple months back, we ruined the team’s Friday, by debating about a similar topic in the context of (Graphite) metrics namespaces. It has become even more intricate since then :/

Update (2020-04-15)

Many people on Twitter suggest that Option 1 is an obvious choice because only application knows how to name the components. I totally agree with that.

As I wrote later, the real dilemma is not about “application and component” but about “owner of the component”. Function main, in the example above, was a silly example, that tried (and failed) to illustrate the question in a code.

Let’s try another (silly) example:

// there are buch of different handlers (maybe ten) in this application
type Handler1 struct { logger Logger }

func (h *Handler1) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// OPTION 1
	req := NewRequst(h.logger.With("component", "request"), r)
}

type Handler2 struct { logger Logger }

func (h *Handler2) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// OPTION 1, still
	req := NewRequst(h.logger.With("component", "request"), r)
}

type Request struct {
	logger Logger
}

// OPTION 2
func NewRequst(logger Logger, *r http.Request) *Request {
	return &Request{
		logger: logger.With("component", "request"),
	}
}

We want to have a consistent nomenclature across the application’s logs.

Is the choice still obvious? ;)

Do you have an opinion? Share it with me on Twitter.

Retrieve Location of macOS Device from Go

Participating in self-isolation is more fun when you have toys to play. As a fun weekend project, I wanted to look at how one accesses macOS Location Services and get the geographic location of the device from Go.

To obtain the geographic location of a device on macOS, we use Apple’s Core Location framework. The framework is part of the OS, but it requires writting Objective-C (or Swift). Thanks to Go’s cgo and because Objective-C is from the family of C languages, we can write a bridge between Objective-C and Go.

Keep reading…