Multiple "identical" clients updating state
Applications where a user has multiple clients observing state
When an application allows a user to have multiple clients observing that state, any of which could update the state as well, I found this phrase helpful: “when a user changes a state in the application, you cannot assume the client performing the state change is the only client belonging to the user”.
To some people this may be obvious. However, I found myself running into issues when I did not account for this.
Imagine an asynchronous turn-based game i.e. digital board game. A user may have the game (client) on her phone, iPad, and Steam. Her turn may come up on her commute to which she’ll play on her phone (sends an update to the game state). When she gets home, she continues the match on her PC. She then goes to another room and continues to play on the iPad. If the game server assumes the client sending the state update is the only client belonging to the user, it’s possible that if she takes a turn on one of her devices the others won’t be updated. In fact, what’s even worse is that those other clients still believe it’s her turn and she can submit additional moves! (our protagonist would never cheat though)
For the situation above, I believe the best pattern is to broadcast state updates and include the user which it originated from. That allows the user’s other clients to update state but also realize their turn is over.
Go play more asynchronous turn-based games. They can fit to many people’s lifestyles.
CORS Boilerplate in Go
Edit: Since this writing, rs/cors has been identified as the easiest and best way to do CORS in Go.
/*
* Boilerplate for handling CORS preflighted requests.
* https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests
*/
package main
import (
"fmt"
"log"
"net/http"
"strings"
)
// Define length of time results of preflight
// can be cached
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Access-Control-Max-Age
var MaxAge = "60"
// Define all allowed methods here.
var AllowedMethods = map[string]bool{
"GET": true,
"POST": true,
}
// Define all allowed headers here.
var AllowedHeaders = map[string]bool{
"XPING": true,
}
// Define all allowed origins here.
// Can be wildcard ("*") or exactly ONE URI (e.g. "www.nhatqbui.com")
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
var AllowedOrigins = "*"
func getKeys(m map[string]bool) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
// Example method handling preflight requests and normal requests.
func handleAjaxHello(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
// Handle pre-flight here.
// Note: We begin by manually checking the headers of the preflight.
// We can actually respond with the information communicated
// in the response headers and the browser should handle
// whether the subsequent is carried out. Here, we are being
// very explicit and checking the headers ourselves.
// Pre-flight will indicate the method of the actual request.
// Check that the method is a member of the set of allowed methods.
reqMethod, ok := r.Header["Access-Control-Request-Method"]
if !ok {
// Handle unspecified methods here
// In this example, we don't do anything and return.
return
}
hasMethod := false
for _, method := range reqMethod {
if AllowedMethods[method] {
hasMethod = true
break
}
}
if !hasMethod {
// Handle incorrect methods here
// In this example, we don't do anything and return.
return
}
// Check that subsequent request will have appropriate headers.
// This one is more nuanced. You should consider:
// - should the request contain ALL allowed headers?
// - or should the request headers be allowed headers
// (but not necessarily containing all allowed headers)
// Here, we just check that the request headers are allowed headers.
// Whether it has-all-necessary-headers is not checked.
reqHeaders, ok := r.Header["Access-Control-Request-Headers"]
if !ok {
// Handle unspecified methods here.
// In this example, we don't do anything and return.
return
}
for _, headers := range reqHeaders {
for _, header := range strings.Split(headers, ",") {
if !AllowedHeaders[header] {
// Handle incorrect headers here
// In this example, we don't do anything and return.
return
}
}
}
// We arrived here, the preflight has passed our checks
// And the subsequent request will be valid.
w.Header().Set("Access-Control-Allow-Origin", AllowedOrigins)
w.Header().Set("Access-Control-Allow-Methods", strings.Join(getKeys(AllowedMethods), ","))
w.Header().Set("Access-Control-Allow-Headers", strings.Join(getKeys(AllowedHeaders), ","))
w.Header().Set("Access-Control-Max-Age", MaxAge)
} else if r.Method == "POST" {
var jsonStr = []byte(fmt.Sprintf(`{"data":"PONG"}`))
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write(jsonStr)
}
}
func main() {
http.HandleFunc("/ajax/hello", handleAjaxHello)
err := http.ListenAndServeTLS(":8080", "./certs/testing.crt", "./certs/testing.key", nil)
if err != nil {
log.Fatal("ListenAndServer: ", err)
}
}
Python Protobuf 'syntax' Error
A common error somebody may encounter using Python protobufs on common Linux distros is:
TypeError: __init__() got an unexpected keyword argument 'syntax'
This is most likely due to the fact that a lot of OSs have an outdated version of the Python protobuf package. Typically a user may acquire protobuf tools from the OS package manager like
sudo apt-get install protobuf-compiler
sudo apt-get install python-protobuf
The protobuf compiler will compile a version of the protobuf which has a syntax the python-protobuf, the package that allows you to do import google.protobuf
, doesn’t understand. Specifically, all objects will be passed a syntax
field to their __init__
function. Update Python protobuf by visiting the repo and getting the latest version.
Ducktyping + Python Protobuf
Given a compiled Python protobuf as follows:
message Person {
optional string email = 1;
}
Don’t use duck typing to see if an optional field has been set!
Though the value is known to be unset, accessing the value always returns
a default value! (Source. See optional
field description.)
import Person_pb2
person = Person()
if person.email:
print('Oh wow you totally have an email!')
print('It is: '.format(person.email))
else:
print('I got nothing')
> Oh wow you totally have an email!
> It is:
Use the compiled protobuf’s data member functions to check existance:
if person.HasField('email'):
# do stuff
REST Example
Not RESTful
REST interface: “Do something and let me know when you’re done.”
Resource: “K.”
…
Resource: “Hey, I’m done.”
REST interface: “K.”
RESTful
REST interface: “Do something.”
Resource: “K.”
REST interface: “Are you done?”
Resource: “No.”
REST interface: “Are you done?”
Resource: “No.”
REST interface: “Are you done?”
Resource: “No.”
REST interface: “Are you done?”
Resource: “No.”
…