2016/11/12

GDG Berlin Golang "Movember Gophers" に行ってきた

昨日ドイツのベルリンについたばかりで、少し時差ボケってたんですが、なんとなくGDG Berlin Golang "Movember Gophers"に参加できました。

ちなみに「Movember」というのが「Moustache」と「November」の組み合わせなというわけで、イベントページのゴーファー君も立派な口ひげしてますね。

僕、カナダ人ですが、実はプログラミングや開発に興味を持ち始めたのが日本にきてからなので、こんな感じに日本の外で勉強会に参加するのって初めてでした。世界中の勉強会で何が普通なのか全くわからないんですが、とりあえずピザとビールが決まりみたいです。

ただ昨日は折り畳めて食べる巨大なピザでした・・・

あと、日本ではいつもトークを聞いてから、懇親会を行うんですが、このイベントではピザとビールが最初から出してあって、みんなが集めるの待ってるうちに飲む感じでした。トークとトークの間にみんながビールを取りに冷蔵庫にダッシュしたところ面白かったですww

さて、ピザとビールの件をほといて、トークの内容の話をしましょう。


最初の発表者が@matryer というイギリスの方でした。GoでTDDをする話をしてくれました (スライドはこちらです)。

TTDを行うときに便利なTIPを色々紹介してくれました。例えば:

  • Silk というマークダウンで書かれたドキュメントからHTTPのテストを実行できるパッケージ
  • go test -cover コマンドでテストのカバレージを確認する方法
  • 特別な理由がなければ、テストヘルパーを使わないで標準testパッケージをそのまま使おう (Go作者の意見)
  • 外部dependencyを避けるため、実際のオブジェクトじゃなくてモックしたオブジェクトを利用する (つまりinterfaceでtypeを柔軟にする)
  • interfaceを元にモックのstructを自動生成ツールの紹介
  • あるパッケージを外部からテストしたいとき、テスト専用のパッケージを作ってもいい(テストの場合同じディレクトリに複数パッケージをおいても大丈夫)


2番目の発表がに@konradreicheによるConsumer Driven Contract Testing in Goでした。

マイクロサービスのインテグレーションテストで出てくる問題を解決しようとしているPactの紹介でした。(概要から日本語で説明する自信ないので、クックパッドの記事を参考にするといいでしょう)

Pact for Goが実際にTape.tvでどういうふうに使われているかも説明してくれました。Box Officeというチケット購入システム(Ruby)とBouncerという認証用API(Go)の違うチームによって管理されているマイクロサービスがあるそうですが、共通のAPIをPactで定義することで管理のところを一部自動化できてて安心らしいです。

ただConsumer Driven Contractを使えるまでのプロジェクトのセットアップやチームが新しい仕組みになれるまでの時間の面で少し大変らしいです。


最後に@fortytw2watneyというRubyのvcrの移植の紹介をしました。HTTPリクエストをキャプチャーし、次回に実行した際にHTTPリスポンスを再利用することで、ネットワーク障害の影響でこける確率を低くしたり、テストを早くしたりするとても便利そうなパッケージです。

2016/11/02

Minna no Go Gengo: A Summary / Review in English (chapter 3)

Here's a continuation of my summary of the Japanese Go programming book: Minna no Go Gengo. This is chapter 3.

For anyone who has missed my other summaries, here are chapter 1 and chapter 2.

How to Make practical applications

Author: Fujiwara Shunichiro (aka @fujiwara)

3.1 Opening

First of all what does the author mean by "practical applications"?
A practical application...

  • makes it easy to look up what kind of operations it performs
  • has good performance
  • can support different inputs and outputs
  • is easy for humans to use
  • is easy to maintain

The two github repos below are referenced frequently throughout the chapter as real-world practical applications written. Both are applications created by the author.

3.2 Version Control

Many Go programs can be shipped as a single binary, so in comparison to interpreted languages, the deployment process is generally much simpler. However since we're dealing with binaries it's a good idea to make it easy to programmatically obtain the version number of the binary so users can check if they have the latest version. Using the flag package to capture whether the program was invoked with -v or --version flags is common, but instead of hardcoding the version into the source code, the author recommends making use of git tags to store the version number and then passing it to the code using the build argument ldflags. A Makefile could for example do something like this:


#!/bin/sh

GIT_VER=`git describe --tags`
go build -ldflags "-X main.version=${GIT_VER}"


The Makefile for fluent-agent-hydra seems to make use of this very technique.

3.3 Efficient Use of I/O

This section demonstrates why and how bufio should be used when dealing with I/O operations.

The first point the author makes is how useful bufio.Reader.Peek can be when you run into a situation where you want to validate data coming in from STDIN, but don't want to read everything in the buffer quite just yet. An example of this kind of scenario is in the application stretcher which expects to receive a valid JSON string via STDIN. Although it's possible to read the entirety of the input into memory and then check whether it is valid JSON or not, it's more efficient to pass the input in io.Reader directly to encoding/json.Decoder. This is where bufio.Reader.Peek comes in. A call to Peak() can be used to check if the first character looks like the beginning of a JSON array ("[") , and if not we can simply return an error without bothering to read the rest of STDIN.

Another important point the author brings up is the difference between buffering in Go as opposed to in interpreted languages such as Ruby, Perl and Python. Interpreted languages generally handle the buffering of text output automatically at run time when their enclosing program is handed to a pipe, thereby reducing the number of costly system calls. Go, on the other hand, doesn't automatically buffer anything.

For example, try inspecting the following program using strace -e trace=write ./filename | cat


package main

import (
  "fmt"
  "os"
  "strings"
)

func main() {
  for i := 0; i < 100; i++ {
    fmt.Fprintln(os.Stdout, strings.Repeat("x", 100))
  }
}


Checking the output of strace on the above program reveals that a total of 100 system calls are recorded, indicating that no buffering has taken place. The bufio package can help us improve this example.


package main

import (
  "bufio"
  "fmt"
  "os"
  "strings"
)

func main() {
  b := bufio.NewWriter(os.Stdout)
  for i := 0; i < 100; i++ {
    fmt.Fprintln(b, strings.Repeat("x", 100))
  }
  b.Flush()
}


By wrapping os.Stdout with a *bufio.Writer we can delay the system calls until Flush() is called. The default buffer size is 4096 bytes, but it can be increased as necessary. Inspecting our new and improved program in strace will show that the number of system calls has been reduced to 2 whether we pass our program to a pipe or not.

3.4 Handling random numbers

This section mostly just explains the difference between math/rand (pseudo random number generator) and crypto/rand (cryptographically secure pseudo random number generator) and shows how they can be used. I think this topic is pretty well covered in English.

3.5 Human readable numbers

The package recommended for converting file sizes and time stamps to human readable format is: go-humanize. Again the documentation for this package is in English, so probably I don't need to summarize. Just if you need to convert numbers to a more readable format, use this package rather than wasting time trying to do it yourself.

3.6 Executing external commands through Go

Generally speaking executing other programs through Go incurs a penalty in terms of starting up other processes in the background and sending data to external commands, so performance-wise, it's often preferable to implement a lot of things in pure Go. However there are of course instances where it is better to delegate the work to an existing program. This section mostly just models how to use the os/exec package to call external programs. One thing I didn't know is that if you call sh through os/exec you can use redirects and other shell sigils (>, ||, &&, etc) as normal.


exec.Command("sh", "-c", "some_command || handle_error").Output()


3.7 Timing out

While a lot of existing packages like net/http handle timeouts for you, sometimes you might want to implement a timeout yourself. This section demonstrates how you can use the time package and channels to implement a timeout yourself.


// A 10 second timer
timer := time.NewTimer(10 * time.Second)
// a channel to receive the result
done := make(chan error)

go func() {
  // call the function you want to run asynchronously in a goroutine
  done <- doSomething() // a function that returns an error
}

// use select to wait for a response from multiple channels
select {
case <-timer.C:
  return fmt.Errorf("timeout reached")
case err <-done:
  if err != nil {
    return err
  }
}


3.8 Working with signals

Go's default handling of signals is documented in os/signal. This section demonstrates how you might change the default behaviour in your own programs. A few examples of why you might want to do this is for example you have a server application and want to finish processing all incoming requests before closing or you want to make sure your program finishes writing everything currently in the buffer and properly closes open files before exiting itself. The example in the book is relatively close to the example from the os/signal docs for Notify(), so a read through that will give you the gist.

3.9 Stopping goroutines

It's easy to start a goroutine, but stopping one goroutine from inside another can be a bit tricky. There are two main ways to do this: using channels, or using the context package provided by go 1.7 and later.

The example code on page 80 shows how to use channels to stop a goroutine. The code demonstrates a program that implements concurrent workers which can process data from a queue.


package main

import (
  "fmt"
  "sync"
)

var wg sync.WaitGroup

func main() {
  queue := make(chan string)
  for i := 0; i < 2; i++ { //make two workers (goroutines)
    wg.Add(1)
    go fetchURL(queue)
  }

  queue <- "http://www.example.com"
  queue <- "http://www.example.net"
  queue <- "http://www.example.net/foo"
  queue <- "http://www.example.net/bar"

  close(queue) // tell the goroutines to terminate
  wg.Wait()    // wait for all goroutines to terminate
}

func fetchURL(queue chan string) {
  for {
    url, more := <-queue // more will be false when this closes
    if more {
      // process the url
      fmt.Println("fetching", url)
      // ...
    } else {
      fmt.Println("worker exit")
      wg.Done()
      return
    }
  }
}


When you call close() on a channel from the sending side, the second variable passed to the receiving side (the variable more evaluates to false. The goroutine is then able to close itself (by calling return) when there is no more data to receive.

The context package can be used to achieve much the same thing. The advantage that context brings to the table is a function called context.WithTimeout() which lets you handle timing out and cancellation in one fell swoop. Go Concurrency Patterns: Context from the official golang blog covers its usage extensively.

That wraps up the topics introduced in this chapter. A lot of the content was new to me, so hope to make use of some of the techniques in the future.

2016/10/17

Using Packer and Terraform on Digital Ocean

I'm in the middle of an international move, which means that it's kind of slow to connect to the VPS I had been using, since they are based in Digital Ocean's Singapore region. I have a couple chef recipes to help me automate, so moving my servers over to a region closer to me shouldn't be too painful, but even just installing rbenv and getting Chef up and running can be a bit of a pain. What better time to teach myself how to use Packer and Terraform?

So this is the agenda for today:

  • Use Packer to setup users and install nginx openresty on a snapshot using Chef as the provisioner
  • Create a Terraform configuration that will use the snapshot created above to startup a Droplet

Packer

First, make a directory to contain your packer configuration files and enter the directory:


crimson@dixneuf 14:07 ~/ $ mkdir packer 
crimson@dixneuf 14:07 ~/ $ cd packer 


Next, create a json file to hold the configuration settings. You can name it whatever you like, so best to name it something memorable to remember what it is later. I called mine bustermachine.json (because I like to name my VPS after Top wo Nerae 2, why not? ¯\_(ツ)_/¯). This is the basic configuration:


{
  "builders": [{
    "type": "digitalocean",
    "api_token": "YOUR-API-TOKEN",
    "region": "fra1", 
    "size": "512mb",
    "image": "centos-7-2-x64",
    "droplet_name": "bustermachine",
    "snapshot_name": "bustermachine-img-{{timestamp}}"
  }]
}


This will create a snapshot using the centos 7.2 image in the Frankfurt 1 region. The droplet size is set at 512MB, but the size can be scaled up when creating a new droplet from this image, so making the smallest size can't hurt.
The snapshot name must be unique and is what will appear in the DigitalOcean console, so it's a good idea to set it to something memorable + a timestamp. You can find additional configuration options in Packer's official documentation.

The configuration above will build an empty server with nothing running, so let's do some provisioning with chef-solo:


  "provisioners": [{
    "type": "chef-solo",
    "cookbook_paths": ["cookbooks"],
    "data_bags_path": "data_bags",
    "run_list": [ "recipe[local-accounts]", "recipe[nginx]" ]
  }]


The cookbooks_paths and data_bags_path are relative to the working directory (our ~/packer folder), but you can also define an absolute path to an existing chef repository on your local machine. What kind of recipes you want to run is up to you, but I'm just going to run one that sets up a user account and one that installs installs openresty nginx.

OK. Let's build it.


crimson@dixneuf 14:14 ~/packer  $ packer build bustermachine.json
digitalocean output will be in this color.

==> digitalocean: Creating temporary ssh key for droplet...
==> digitalocean: Creating droplet...
==> digitalocean: Waiting for droplet to become active...
==> digitalocean: Waiting for SSH to become available...
==> digitalocean: Connected to SSH!
==> digitalocean: Provisioning with chef-solo
    digitalocean: Installing Chef...
    digitalocean: % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
    digitalocean: Dload  Upload   Total   Spent    Left  Speed
    digitalocean: 0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
    sudo: sorry, you must have a tty to run sudo
    digitalocean:  25 20058   25  5000    0     0   4172      0  0:00:04  0:00:01  0:00:03  4177
    digitalocean: curl: (23) Failed writing body (1855 != 2759)
==> digitalocean: Destroying droplet...
==> digitalocean: Deleting temporary ssh key...
Build 'digitalocean' errored: Error installing Chef: Install script exited with non-zero exit status 1

==> Some builds didn't complete successfully and had errors:
--> digitalocean: Error installing Chef: Install script exited with non-zero exit status 1

==> Builds finished but no artifacts were created.


Uh oh. Since I'm getting the message sudo: sorry, you must have a tty to run sudo, it looks like CentOS' default sudo permissions are getting in the way of Chef's installation. We can work around this by defining "ssh_pty": true in the builder portion of our Packer configuration.

Now the configuration file looks like this. Now that we've got that taken care of, let's try building again:


crimson@dixneuf 14:20 ~/digitalocean/packer  $ packer build bustermachine.json
digitalocean output will be in this color.

==> digitalocean: Creating temporary ssh key for droplet...
==> digitalocean: Creating droplet...
==> digitalocean: Waiting for droplet to become active...
==> digitalocean: Waiting for SSH to become available...
==> digitalocean: Connected to SSH!
==> digitalocean: Provisioning with chef-solo
    digitalocean: Installing Chef...

....

    digitalocean: Running handlers:
    digitalocean: Running handlers complete
    digitalocean: Chef Client finished, 14/14 resources updated in 31 seconds
==> digitalocean: Gracefully shutting down droplet...
==> digitalocean: Creating snapshot: bustermachine-img-1476642546
==> digitalocean: Waiting for snapshot to complete...
==> digitalocean: Error waiting for snapshot to complete: 
Timeout while waiting to for droplet to become 'active'
==> digitalocean: Destroying droplet...
==> digitalocean: Deleting temporary ssh key...
Build 'digitalocean' errored: Error waiting for snapshot to complete: 
Timeout while waiting to for droplet to become 'active'

==> Some builds didn't complete successfully and had errors:
--> digitalocean: Error waiting for snapshot to complete: 
Timeout while waiting to for droplet to become 'active'

==> Builds finished but no artifacts were created.


Ok this time there was a timeout... but a look at Packer's Github issues shows that this problem is a known bug that will be fixed in the next version of Packer (I'm on version 0.10.2). Even though Packer timed out, the Digital Ocean console shows that our snapshot was created ok.

Since we've successfully created our first snapshot, let's move on to creating a Droplet in Terraform.

Terraform

First make a directory to hold our configuration files and from where we will execute all our terraform commands:


crimson@dixneuf 14:07 ~/ $ mkdir terraform
crimson@dixneuf 14:07 ~/ $ cd terraform


Before we continue, though, the next step requires that we know the ID of the snapshot we just created, which is different from the slug name that appears in the console. We can use the Digital Ocean API to look that up:


curl -X GET -H "Content-Type: application/json" -H "Authorization: Bearer " / 
"https://api.digitalocean.com/v2/snapshots"
{
  snapshots: [
    {
      id: 20321411,
      name: "bustermachine-img-1476642546",
      regions: ["fra1"],
      created_at: "2016-10-16T18:31:29Z",
      resource_id: 29342757,
      resource_type: "droplet",
      min_disk_size: 20,
      size_gigabytes: 1.35
    }
  ],
  links: { },
  meta: { total: 1 }
}


In my case it's id: 20321411, so we'll have to use that ID in our Terraform config file. Let's make that file now and name it config.tf:

variable "do_token" {}

# Configure the DigitalOcean Provider
provider "digitalocean" {
    token = "${var.do_token}"
}

This first part just lets Terraform know that we intend to use Digital Ocean as our provider, but we have to pass it our API token. We can do this in two ways. One way is to create a file called terraform.tfvars to contain our variables, or we can pass the variable using the command line when we call terraform plan later on (terraform plan -var 'do_token=foo'). I recommend checking out the documentation.
Next we need to define the resources we intend to create. Here's my config for creating a Droplet named vingtsept using the snapshot ID I obtained earlier (20321411) in the image definition:

# Create a web server
resource "digitalocean_droplet" "vingtsept" {
    image = "20321411"
    name = "vingtsept"
    region = "fra1"
    size = "1gb"
    ssh_keys = [4055393]
}

Note that, although our snapshot was created from a 512MB Droplet, we can create a larger 1GB Droplet from it (but making a smaller Droplet from a bigger sized snapshot is not possible).
I already had an ssh key registered on Digital Ocean so I set the ssh key id (also obtainable via the Digital Ocean API), but if you need to upload a new one you can also use Terraform to do it:

resource "digitalocean_ssh_key" "default" {
    name = "dixneuf"
    public_key = "${file("/Users/crimson/.ssh/id_rsa.pub")}"
}

Now that we've finished creating our config file, let's try running terraform plan to check our configuration:


crimson@dixneuf 15:28 ~/terraform  $ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but
will not be persisted to local or remote state storage.

The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed. Cyan entries are data sources to be read.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

+ digitalocean_droplet.vingtsept
    image:                "20321411"
    ipv4_address:         ""
    ipv4_address_private: ""
    ipv6_address:         ""
    ipv6_address_private: ""
    locked:               ""
    name:                 "vingtsept"
    region:               "fra1"
    size:                 "1gb"
    ssh_keys.#:           "1"
    ssh_keys.0:           "4055393"
    status:               ""

Plan: 1 to add, 0 to change, 0 to destroy.


Looks ok, so let's create the Droplet already.


crimson@dixneuf 15:28 ~/terraform  $ terraform apply
digitalocean_droplet.vingtsept: Creating...
  image:                "" => "20321411"
  ipv4_address:         "" => ""
  ipv4_address_private: "" => ""
  ipv6_address:         "" => ""
  ipv6_address_private: "" => ""
  locked:               "" => ""
  name:                 "" => "vingtsept"
  region:               "" => "fra1"
  size:                 "" => "1gb"
  ssh_keys.#:           "" => "1"
  ssh_keys.0:           "" => "4055393"
  status:               "" => ""
digitalocean_droplet.vingtsept: Still creating... (10s elapsed)
digitalocean_droplet.vingtsept: Still creating... (20s elapsed)
digitalocean_droplet.vingtsept: Still creating... (30s elapsed)
digitalocean_droplet.vingtsept: Still creating... (40s elapsed)
digitalocean_droplet.vingtsept: Creation complete

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path: terraform.tfstate


And there it is. The Droplet is up and running. Makes for incredibly painless server setup if you ask me!

2016/10/05

Minna no Go Gengo: A Summary / Review in English (chapter 2)

It took me way longer than I expected to sit down and write this, but now that I have a bit of downtime before my next flight, I'd like to continue my summary of Minna no Go Gengo.

How to build multi-platform tools for your workplace
Author: @mattn

This chapter encourages readers to build multi-platform tools (Windows, Mac, Linux, etc) to support different devices coworkers use and gives some guidelines on how this can be done effectively in Go. Here is a bit of a break down of what each of the sections are and what kind of info they cover:

2.1 Why build internal tools in Go

Go makes it possible to statically build a runnable module for various OSes, so there is no need to ask users to install the Go runtime on their machines. Thanks to this, there is no worry that a different runtime implementation on a different OS will behave differently. Distributing a single binary file is all that is needed to let others use a Go program. Both of these things make Go a really good choice for internal tooling.

2.2 Implicit rules to follow

Rule one is use path/filepath to interact with the filesystem and not the path package. These two packages might be confusing to new users of Go: while path/filepath is pretty explanatory, path is a package meant for resolving relative paths in a http or ftp context. Because the path package does not recognize "\" as a path separator even on Windows, accessing a url like http://localhost:8080/data/..\main.go on a web server that makes use of the path package to locate static files could be used to expose the raw contents of other files on the filesystem

Rule 2 is to use defer to clean up resources. This is pretty well documented elsewhere, so I don't think I really need to elaborate.

The next recommendation is of particular concert to anyone who deals with Japanese or languages containing multibyte characters. Anyone interacting with programs that make use of the ANSI API in Windows to produce output will have to make use of an appropriate encoding package like golang.org/x/text/encoding/japanese to convert the input from ShiftJIS to UTF-8.

2.3 Using TUI on Windows

Linux based Text-based User Interfaces use a lot of escape sequences, many of which don't display properly in Windows. In Go you can use a library called termbox to make the process of making multi-platform TUI applications easier. Another recommended program is one of the author's own tools: go-colorable which can be used which can help produce coloured text in log output, etc.

2.4 Handling OS Specific Processes

Use runtime.GOOS to determine the OS from within a program.

This section also covers build constraints, but this topic is already well covered in English in the documentation, so I won't go into detail here.

2.5 Rely on existing tools instead of trying too hard

While it is technically possible to daemonize processes in Go by using the syscall package to call fork(2), the multithreaded nature of Go makes this a bit tricky. So it is generally recommended to use external tools to handle the daemonizing of a Go program. For Linux for example check out daemonize, supervisord and upstart and for Windows check out nssm

For Unix a regular user can't listen on port 80 or 433 so a lot of unix servers are configured to start as root and use setuid(2) to demote the permissions. However it's not recommended that you use setuid(2) in Go because it only affects the current thread. Instead use nginx or another server to reverse proxy requests from 80 or 433 to another port that Go can listen on.

2.6 Go likes its single binaries

Go makes deployment as easy as placing a single binary file on a server, but in the case of larger programs like web applications (for example) sometimes templates, pictures and other files are necessary. Try using go-bindata to pack static files as assets in a binary so that you don't have to sacrifice ease of deployment.

2.7 Making Windows applications

This section covers how to toggle whether or not your Go program displays a command prompt or not using the -ldflags="-H windowsgui" with go build and also how to link resource files (like the application's icon) using IDI_MYAPP ICON "myapp.ico"

And here are some recommended packages for building multi-platform compatible GUIs:

2.8 Configuration files

The first part of this section covers different file formats like INI, JSON, YAML, TOML and covers their strengths and weaknesses.

Aside from file format, file location on each platform can also be a source of confusion when configuring applications. On UNIX systems the standard was to place each file in the home directory like $HOME/.myapp originally, but more recently the XDG Base Directory Specification recommends that config files be placed under $HOME/.config/.

Similarly on Windows it's no problem if you use %USERPROFILE%\.config\, but the author mentioned he often places config files under %APPDATA%\my-app\.


Well that's the gist of it. I haven't really built software for Windows before mostly just because it seemed like too much trouble, but this chapter sure made it look like Go is making that whole process much easier for those of us who are used to developing for Linux.

For anyone who missed my (much briefer) summary of chapter 1, you can find it here.

2016/09/13

Minna no Go Gengo: A Summary / Review in English (chapter 1)

My copy of Minna no Go Gengo, the Golang book with the cover everyone loves, came in the mail today!

Just last week, I was talking to some gophers based in Germany and when I mentioned that a coworker of mine recently published a chapter in a book on Go, one of the guys immediately asked "Is it the one with the gophers and robot on the cover?". Since the book only exists in Japanese I was surprised that he knew about it, but I guess I shouldn't be too suprised. The cover is just too good not to share, am I right?

So I thought i'd write a quick review / summary of each chapter in case there are some gophers out there who are interested in knowing what's in the book. I've just barely begun reading, but as the title, Minna no Go Gengo (Everyone's Golang), suggests, it covers a number of topics for a range of different skill levels from how to get started for absolute beginners to more advanced topics like reflection. Each chapter is written by a different author, all of whom are well-known OSS contributors here in Japan.

The first chapter is the most beginner friendly, but also contains some stellar tips about how to write Go code in a Go-like way.

How to start writing Go code for team development.

Author: Matsuki Masayuki (aka @Songmu)

This chapter starts out with the essentials: how to install Go, an introduction to some of the core command-line tools used in Go development as well as suggestions for some useful third party tools like ghq, peco and glide). It's super concise and does a great job of covering the essentials without being too verbose.

For anyone who's written Go code before, the meat of the chapter, though is in the style guide which highlights some differences between writing programs using scripting languages like Ruby and Perl vs writing in Go. For example:

  • Avoid using regexp: use the strings package wherever possible instead. Why? They can be really slow, sometimes even slower than Perl regexp... which is pretty bad for a pre-compiled program.
  • Avoid maps. Because Go is a strongly typed language it is better to use structs. Also maps are not thread-safe. If you need to use a map alongside concurrency embed one in a struct alongside a sync.RWMutex.
  • Don't overuse concurrency. While Go is great for concurrency overusing it not only makes programs harder to read, but also increases the likelihood of race conditions.
  • Use the -ldflags and -tag options to embed useful information in a binary when using go build.
  • runtime.NumGoRoutine and runtime.ReadMemStats are useful monitoring metrics for web servers and other long running programs. golang-stats-api-handler is a useful library that provides an api interface to the go runtime package.

Hardly an exhaustive list, as this chapter is packed with useful info for people who are transitioning to Go from other languages and gives a good introduction to how to get into a Go mindset. I am looking forward to reading and writing up on the remaining chapters.

2016/08/07

Using OpenResty's access_by_lua and the satisfy any directive

So recently I learned that OpenResty's access_by_lua_block and the satisfy any directive don't play nice together. To be honest I didn't have a very compelling reason to use an access_by_lua_block to begin with. Ideally I would have used a set_by_lua_block, but subrequests using ngx.location.capture are disabled by it since it is a blocking function.

Still I felt a bit conflicted about whether I should be using access_by_lua or rewrite_by_lua, since technically all I really wanted to do was set a variable (to print in the access logs) and am neither authenticating or rewriting. Using either one seems like a hacky workaround.

As it turns out, rewrite_by_lua is the much safer option if you use any authentication directives. Take for example this situation:

  • I need to set a variable using an external microservice and decide to do so with ngx.location.capture
  • It's a private API with complex authentication rules (ie a combination of IP blocking and basic auth)

So something like this:


server {
  set $my_special_variable "0"; # fallback value if no response from microservice
  access_by_lua_block {    
    local res = ngx.location.capture("/microservice")
    if res then
      ngx.var.my_special_variable = res.body
    end
  }

  location ~ /private/api/endpoint {
    satisfy any;
    allow [some ip address];
    deny all;

    auth basic "unauthorized";
    auth_basic_user_file /etc/nginx/.htpasswd;

    proxy_pass http://my_backend_server;
  }
}
.

Unfortunately the access_by_lua_block counts as a satisfied condition for the satisfy any; directive and suddenly my API is opened up to the world: neither the correct IP or basic auth is required to access it anymore. Everyone can access it. Huge security risk to say the least.

So it still feels bit hacky since the subrequest to the microservice isn't involved doing any uri rewriting to speak of, but rewrite_by_lua_block is definitely the better option in this situation. Glad I caught this technicality early on.

2016/07/31

Testing Out Google's Natural Language API

Since today is the last day of the Google Natural Language API's free public beta I thought I'd give it a little spin. One of the applications for the API listed on google's promotional page was analyzing product reviews... which reminded me that late last year I made a Slack webhook that extracts Google Play and App Store reviews and happen to still have a stockpile of those laying around in a database of mine, so what better sample data to use for this experiment?

Apple and Google provide rating data (1 to 5 stars) so I know what percentages of the users gave unfavourable reviews, but it would be a good idea to try to narrow down some of the things users are complaining about. Perhaps that's something we can tackle with this API? Let's try it.

My sample data is stored in a database with the following structure:


sqlite> .schema
CREATE TABLE review (
  id INT(11) NOT NULL,
  title VARCHAR(255) NULL,
  content TEXT,
  rating TINYINT(3) NOT NULL DEFAULT 0,
  device_type TINYINT(3) NOT NULL,
  device_name VARCHAR(255) NULL,
  author_name VARCHAR(255) NULL,
  author_uri VARCHAR(255) NULL,
  created DATETIME NULL,
  updated DATETIME NOT NULL,
  acquired DATETIME NOT NULL,
  PRIMARY KEY (id, device_type)
);
CREATE INDEX updated_idx on review(updated);
CREATE INDEX rating_idx on review(rating);
.

At this point I don't really care about platform (although I certainly could further break down my user samples by device type if I was so inclined), so I'm just going to collect the comments from users who gave a distinctly bad rating (of 1 or 2 stars) to feed to the API with a simple query like this:


SELECT content FROM review WHERE rating < 3;
.

If I throw the main body of the review text at Google's API and see if it'll come up with some salient keywords (and how often they are brought up) perhaps it'll give us a better clue what it is the users are complaining about.

So first we need to authenticate with the API.

Creating a service key file for authentication is straight forward enough, so I'll just link to the documentation here and once you have one of those all you need to do is use the gcloud command to authenticate and print an access-token.


$ gcloud auth activate-service-account --key-file=kinmedai-cb03d32572c2.json
Activated service account credentials for: [user@projectname.iam.gserviceaccount.com]
$ gcloud auth print-access-token
[[output omitted]]
.

Now that I'm ready to access the API, I decided to create a script in Go to do the dirty work for me. So the first step is to use the sample json payload and response body data from the getting started docs to generate structs in Go. Writing structs by hand is a pain, so I used JSON to Go to generate the bulk of it and then tweaked it a bit like so:

The request structure:


type EntityRequest struct {
  EncodingType string                `json:"encodingType"`
  Document     EntityRequestDocument `json:"document"`
}

type EntityRequestDocument struct {
  TypeName string `json:"type"`
  Content  string `json:"content"`
  Language string `json:"language"`
}
.

And the response structure:


type EntityResponse struct {
  Entities []DetectedEntity `json:"entities"`
  Language string           `json:"language"`
}

type DetectedEntity struct {
  Name       string          `json:"name"`
  EntityType string          `json:"type"`
  Salience   float64         `json:"salience"`
  Mentions   []EntityMention `json:"mentions"`
  Metadata   struct {
    WikipediaUrl string `json:wikipedia_url"`
  } `json:"metadata"`
}

type EntityMention struct {
  Text struct {
    Content     string `json:"content"`
    BeginOffset string `json:"beginOffset"`
  } `json:"text"`
}
.

Now that I know what kind of data I'll be dealing with I can start building my request.


func createEntityRequests() []*EntityRequest {
  dbh := getDBH()
  rows, err := dbh.Query(`SELECT content FROM review WHERE rating < 3`)
  if err != nil {
    log.Fatal(err)
  }

  var entities []*EntityRequest

  for rows.Next() {
    var comment string
    err = rows.Scan(&comment)
    if err != nil {
      log.Fatal(err)
    }

    // Google Play lets users submit ratings with no comments (stars only ratings) so skip those
    if len(comment) == 0 {
      continue
    }

    entityRequest := &EntityRequest{
      EncodingType: "UTF8",
      Document: EntityRequestDocument{
        TypeName: "PLAIN_TEXT",
        Content:  comment,
        Language: "JA",
      },
    }
    entities = append(entities, entityRequest)
  }

  return entities
}
.

Next I'll need to create a function that posts to the entities analysis API. Again, the quickstart docs summarize this process very clearly, but it's a basic HTTP post request with a json payload and the access token we got from gcloud set in the Authorization header.

I'm planning on passing the token directly from standard input so I can pipe my script with gcloud, but more on that later. First the request:


func postEntity(accessToken string, entityRequest *EntityRequest) []byte {
  jsonEntity, _ := json.Marshal(entityRequest)
  req, err := http.NewRequest("POST", ENTITIES_URL, bytes.NewBuffer(jsonEntity))
  if err != nil {
    log.Fatal(err)
  }
  req.Header.Set("Content-Type", "application/json")
  req.Header.Set("Authorization", "Bearer "+accessToken)

  client := &http.Client{}
  res, err := client.Do(req)
  if err != nil {
    log.Fatal(err)
  }
  defer res.Body.Close()

  body, err := ioutil.ReadAll(res.Body)

  if res.StatusCode != http.StatusOK || err != nil {
    log.Fatal(res.Status)
    log.Fatal(err)
  }

  return body
}
.

Now you might have noticed that this is not the most efficient way to get feedback since I am sending one request per review. At the moment I only have 220 reviews for my test data, so it's not a big deal, but if I was actually planning on using this on any regular kind of basis it could potentially be very expensive (and also slow) to do it this way. Since we don't need to associate any other of the data with the content we could potentially amalgamate several reviews into one body of text and send the data in a batch.

However at this point I'm not even 100% sure this experiment is going to produce any kind of meaningful result, so for the time being I'm going to analyze each review individually. Better to make sure it works before spending time optimizing it, right?

Anyway, let's piece the rest of the script together. I know I'm going to have to pass the script my API access token as well as the path to the db (I'm using sqlite3), so my interface is going to look something like this:


$ gcloud auth print-access-token | go run kinmedai.go -d /path/to/sqlitedbname.db
.

So for my main block I'm going to grab my two parameters via stdin and flags and build the request payloads. Then I'll post each payload to the API and parse any entities from the http response body into the detectedEntities map that'll be used to count how many times a specific term was referenced:


func main() {
  flag.Parse()

  var accessToken string
  fmt.Scan(&accessToken)

  entityRequests := createEntityRequests()
  detectedEntities := make(map[string]int)

  for i := 0; i < len(entityRequests); i++ {
    var entityResponse EntityResponse
    body := postEntity(accessToken, entityRequests[i])
    json.Unmarshal(body, &entityResponse)

    for j := 0; j < len(entityResponse.Entities); j++ {
      entity := entityResponse.Entities[j]
      detectedEntities[entity.Name] = detectedEntities[entity.Name] + 1
    }
  }

  for k, v := range detectedEntities {
    fmt.Printf("%s: %d\n", k, v)
  }
}
.

Yikes! That's a lot of for loops! But as I said I'm still in the experimental phase so I'm not going to worry about how fast this runs just yet.

And the output looks something like this (did I mention I was parsing Japanese reviews?):


Wi-Fi: 2
GOOGLE: 1
TT: 1
DL: 1
2MWXZH4T: 1
アンインストール: 2
ぼく友: 1
ガラポン: 1
2.7.2: 1
GYH7AFSY: 1
某都: 1
Fuck this shit I: 1
掛布: 1
星飛雄馬: 1
ガチャ: 12
ガチャゲー: 1
C8YENNZG: 1
やめた: 3
ゴミゲー: 1
めちゃ運: 1
っ・ω: 1
ゴールド: 1
ガチャ.: 1
間違いない(* ̄ー ̄: 1
WB5Z2JQX: 1
甲子園: 3
平安: 1
パワプロ: 1
.

So it looks like the results are a bit hit and miss. Things like invitation codes, emoji, etc that probably shouldn't be actual keywords are showing up as entities. However it looks like at least 15 (if you count "ガチャ", "ガチャ.", "ガチャゲー" and "ガラポン" as the same result) out of our sample of 220 users are complaining about the gacha system in this particular app.

I can't exactly say this is a groundbreaking discovery or anything. Having read most of the reviews for the app over the last few months (since my webhook delivers new reviews to my team's slack daily) I pretty much knew that many of the complaints hinged around users not getting the drops they wanted, but I guess this helps quantify it a little better?

Either way it looks like Google's entity search is mostly based around pinpointing terms that can be found on Wikipedia... whereas for an app like the one I run it'd be more useful to do a keyword search for game specific terminology if the goal is to pinpoint features the users might be frustrated about.

But now that I've seen what this API is capable of I'm already starting to think of better applications for this technology (like analyzing followers' tweets to see what other games and manga they are talking about).

I skipped over some details (such as connecting to the db and other things that don't really pertain to using the API), but I've uploaded the script to a gist so feel free to reference it in its entirety in case I lost you at any point.

2014/12/25

ペッパー開発体験ワークショップ行ってみた

この間、今年のqiitaのアドベントカレンダーに「Pepper」カレンダーのをみて、「ペッパーってそもそもまだ販売されてないし、買えたとしても滅茶苦茶高いでしょ?」って思って、実際誰がペッパーの開発しているのか見たくてクリックしてみたのですが。なんと秋葉原で無料で開発体験ができるスペースがあること知りました。(今年はもうほぼ終了と思いますが、来年もやるようです)

丁度今仕事休みで暇ですし、無料ですから行ってみました!

ペッパーくんといいツーショット撮ってもらいましたね!


※以下のブログは主に「ペッパー体験ワークショップに興味あるけど、よくわからない」って人向けです。開発方法について特になにも書いてないです。


ワークショップの名前自体に「開発」って単語が入っていますが、実際「基本編」って書いてあるセッションは少なくとも非エンジニアも全然参加できます。Aldebaranさんのソフト(Choreographe)を使って、ドラッグアンドドロップ操作などでペッパーくんのプログラム作れますので、興味ある人はぜひ友達など連れてみてください。
プログラミングがやりたい方はむしろ「上級編」や「ハッカソン」などに参加したほうが良さそうです。

ChoreographeのUIがこういう感じです:

体験スペースではノートパソコンはテーブルごとに用意してあるので、特になにも持っていかなくてもOKなのですが、自分のノートパソコンの持ち込む場合、事前にChoreographeをインストールすることができます。
ダウンロード方法が大変わかりづらいのですが、Aldebaranのコミュニティーサイトでアカウント作ってからこのページに「Software」のタブが現れます。

アカウント作成が面倒な場合、体験スペースにインストーラーが入っているUSBもありますので、早めに行ってUSBからインストールしてもOKだと思います。

公式サイトにワークショップの内容についてあまり書いてないので、ワークショップの内容をざっくり紹介させてください:


【12/15】Pepper開発体験ワークショップ(SDK基本編 #1)

こういったものやりました:

  • ポーズとジェスチャー&timelineを使ってタイミングを合わせる
  • 音声合成機能をつかって喋らせる
  • 発話認識とSwitch Caseを使った反応
  • タッチパネル認識

ダンスのデモも見せてもらいました:


【12/23】Pepper開発体験ワークショップ(SDK基本編 #2)

こういったものやりました:

  • タブレットに画像や動画を表示する
  • Move toやMove around機能で体を動かせる
  • 顔追跡
  • 喋りながらジェスチャーをつける

基本的に時間あまりないので、項目ごとにとても簡単なプログラムを作る余裕しかないですが、まあまあ楽しいです。
最後にペッパーくんにこんなのやってもらいました:

機会あれば、ぜひ皆さんも行ってみてください

2014/12/08

Riot API: 画像のバッチダウンロード

この記事はRiot APIのAdvent Calendar 2014の8日目の記事です。

前回はプロフィールアイコンのurlをCDNから取得するところまでデモしましたが、最新の画像をバッチで一気にダウンロード、使いやすくなるようにファイルをローカルで保存するのをやってみます。


1)チャンピオンのリストを取得

バージョン取得などは前回とほぼ同じので、説明を飛ばします:

#!/usr/bin/env python

import os
import requests
import shutil

DD_URL = 'http://ddragon.leagueoflegends.com'

res = requests.get(DD_URL + '/realms/na.json')
res.raise_for_status()

version = res.json()['n']['champion']

champion_info_url= DD_URL + '/cdn/' + version + '/data/en_US/champion.json'
res = requests.get(champion_info_url)
res.raise_for_status()

data = res.json()['data']
 

ブラウザでチャンピオンjsonデーターを確認するとわかり安いと思いますが、全チャンピオンの名前や基本パラメーターが入っているhash tableを'data'に取得できました。

2)保存先のディレクトリーを用意する

今後ダウンロードをする画像を保存先ディレクトリがあるかどうか確認して、なければつくりましょう。


output_dir = os.path.dirname(os.path.realpath(__file__)) + '/champion'

try:
    os.stat(output_dir)
except:
    os.mkdir(output_dir)
 

3)キャラクターをループで回して、画像を保存しておく

DataDragonではチャンピオンの名前をファイル名として使われています:
http://ddragon.leagueoflegends.com/cdn/4.20.1/img/champion/Aatrox.png

チャンピオンの名前わかれば取得しやすいでしょうけど、たとえば、Riot APIで最近のゲーム履歴(/game/by-summoner/SUMMONER_ID/recent)取得した場合、利用したチャンピオンのkey(ID)しか返ってこないので、名前だけだとかなり不便ですね。
そのため、画像をダウンロードしたら、ファイル名をチャンピオンの名前じゃなくて、チャンピオンのkeyをつけましょう。


champion_img_base = DD_URL + "/cdn/" + version + "/img/champion/"

for name in data:
    img_url = champion_img_base + name + ".png"
    filename = data[name]['key'] + ".png"

    res = requests.get(img_url, stream=True)
    if res.status_code == 200:
        with open(output_dir + '/' + filename, 'wb') as f:
            res.raw.decode_content = True
            shutil.copyfileobj(res.raw, f)
 

以上!
ダウンロードできたかどうかを確認すると:


ls champion/
1.png 106.png 114.png 122.png 14.png 161.png 21.png 25.png 28.png 34.png 40.png 45.png 55.png 61.png 7.png 79.png 85.png 96.png
10.png 107.png 115.png 126.png 143.png 17.png 22.png 254.png 29.png 35.png 41.png 48.png 56.png 62.png 72.png 8.png 86.png 98.png
101.png 11.png 117.png 127.png 15.png 18.png 222.png 26.png 3.png 36.png 412.png 5.png 57.png 63.png 74.png 80.png 89.png 99.png
102.png 110.png 119.png 13.png 150.png 19.png 23.png 266.png 30.png 37.png 42.png 50.png 58.png 64.png 75.png 81.png 9.png
103.png 111.png 12.png 131.png 154.png 2.png 236.png 267.png 31.png 38.png 429.png 51.png 59.png 67.png 76.png 82.png 90.png
104.png 112.png 120.png 133.png 157.png 20.png 238.png 268.png 32.png 39.png 43.png 53.png 6.png 68.png 77.png 83.png 91.png
105.png 113.png 121.png 134.png 16.png 201.png 24.png 27.png 33.png 4.png 44.png 54.png 60.png 69.png 78.png 84.png 92.png
  

全部揃ってありますね!これで新しいチャンピオンが追加された場合でもスクリプトを動かすだけですぐに画像をダウンロードできそうですね。

2014/12/06

Riot API: プロフィールアイコンの取得

この記事はRiot APIのAdvent Calendarの6日目の投稿です。

Riot APIで取得できるJSONデータをみるとなんかワクワクして、LOLKingなどMobafireみたいなかっこいいサイト作りたくなりますよね?
でももちろん、ウェブサイトを作った場合に、最新のアイコンなどの画像も必要になりますので、今回はPythonをつかって自分のプロフィールアイコンのurlを取得するところまでデモしたいと思います。

Riot Gamesのアセット・リポジトリはData Dragonというサービスに通じてアクセスできます。
Riot APIと別のチームが管理しているらしいので、アップデート直後にData Dragonへの反映が遅れたり、 データーが一致しない可能性もありますが、それでも最新の画像の取得には一番便利なツールになります。

ではプロフィールアイコンの取得をやりましょう

1) バージョン取得

画像データーはバッチでアップロードされていて、一番最初にダウンロードしたい画像のバージョンを取得する必要があります。 自分のアカウントはNorth Americaサーバーにありますので、naのjsonフィードを使います。

# datadragon.py
import requests    # pip install requests

DD_URL = 'http://ddragon.leagueoflegends.com'

version_path = '/realms/na.json'
res = requests.get(DD_URL + version_path)
res.raise_for_status()      # catch non 2xx status

print(res.text)
  

プリントの結果はこんな感じです:


{"n":{"item":"4.20.1","rune":"4.17.1","mastery":"4.17.1","summoner":"4.20.1","champion":"4.20.1",            "profileicon":"4.20.1","language":"4.20.1"},"v":"4.20.1","l":"en_US","cdn":"http:\/\/ddragon.leagueoflegends.  com\/cdn","dd":"4.17.1","lg":"0.152.55","css":"0.152.55","profileiconmax":28,"store":null}
  

今回はプロフィールアイコンのバージョンが取得したいと思いますので、こういうふうにprofileiconのバージョン番号だけ取得しましょう:


version = res.json()['n']['profileicon']
  

2) 自分のプロフィールアイコンIDを取得

次に自分のプロフィールアイコンIDを取得します。今回はデータードラゴンじゃなくて、通常のRiotAPIを使いましょう。


API_URL = 'https://na.api.pvp.net/api/lol/na'
summoner_by_name_path = "/summoner/by-name/laouji";
profile_url = API_URL + "/v1.4" + summoner_by_name_path + "?api_key=" + API_KEY

res = requests.get(profile_url)
res.raise_for_status()
 

res.textの中身を確認すると:


"laouji":{"id":46048341,"name":"laouji","profileIconId":607,"summonerLevel":23,"revisionDate":1411808085000}}
 

3)画像のURLを組み合わせる

APIコールの結果に必要なprofileIconIdありましたので、画像のurlを組み合わせるのが簡単です:


def icon_url ( version, icon_id ):
    url = DD_URL + '/cdn/' + version + '/img/profileicon/' + str(icon_id) + '.png'
    return url

icon_id = res.json()['laouji']['profileIconId']
icon_url = icon_url(version, icon_id)
 

自分の場合はこれでした:

2014/08/24

OpenRestyとLapisでSupervisorctlを実行できるシンプルなウェブアプリ作成

周りのマークアップエンジニアがgit使いこなしていて、平気でブランチを切り替えたりして開発サーバー上で作業して貰っています。ただし、ブランチ切り変えになると、たまにアプリリスタートも必要で、マークアップじゃ一人で作業したいブランチを開発サーバーに反映できない場合があります。

Plack::Loader::Shotgunを使ってアプリさえ実行すれば、こういった問題をよりやすく解決できる気もしますが、一応、Nginx OpenRestyを使ってみたかったので、試しにウェブインタフェースを使ってプロセスをリスタートできるアプリを作っちゃいました。

*OpenRestyについてはOpenRestyの公式ページを参考にしてください

**フレームワークはLapisというOpenResty上で動くLuaのウェブフレームワークです。おしゃんてぃでおすすめです。

最近process管理のため主にSupervisordを使っています。周りの人がそれをrootを使って実行しガチなんですが、やっぱり開発環境といってもウェブのユーザがrootのプロセスが実行できたら怖いですし、nginx自体も普段nobodyユーザによって実行されるので、まずSupervisordをnobodyユーザとして実行しました。 そこでnobodyがアクセスできるディレクトリを作って、以下のlapis app.luaを作成しました:
--app.lua
local lapis = require("lapis")
local app_helpers = require("lapis.application")
local validate = require("lapis.validate")
local cjson = require("cjson")

local capture_errors = app_helpers.capture_errors

local app = lapis.Application()
app:enable("etlua")
app.layout = false

validate.validate_functions.alphanumeric = function(input)
     return string.match(input, "^[%w_%-]+$"), "must be alphanumeric"
end

-- たたいたコマンドのSTDOUTパージング
function app:parse_status(line)
    local parts = {}
    for word in line:gmatch("%S+") do table.insert(parts, word) end

    local status = {
        ["name"] = parts[1],
        ["status"] = parts[2],
    }
    if status["status"] == 'RUNNING' then
        status["pid"] = string.gsub(parts[4], ",", "")
        status["uptime"] = parts[#parts]
    elseif status["status"] == 'STOPPED' then
        status["uptime"] = string.format("%s %s %s", parts[3], parts[4], parts[5])
    end

    return status
end

-- トップページにstatusの結果をテーブルで表示したいので、結果をselfにいれるとテンプレートで使えるようになる
app:get("/", function(self)
    local handle = io.popen("/usr/bin/supervisorctl status" .. " 2>&1")

    self.supervisor_status = {}
    for line in handle:lines() do
        table.insert(
            self.supervisor_status,
            self.app:parse_status(line)
        )
    end

    handle:close()

    return { render = 'index' }
end)

-- 最低限のvalidationとリスタートをかける処理
app:post("/:app_name/restart/", capture_errors(function(self)
    validate.assert_valid(self.params, {
        { "app_name", exists = true, alphanumeric = true }
    })

    local app_name = self.params.app_name
    local handle = io.popen("/usr/bin/supervisorctl restart " .. app_name .. " 2>&1")

    self.message = {}
    for line in handle:lines() do
        table.insert(self.message, line)
    end

    return cjson.encode(self.message)
end))

return app
.

出来上がったものはこんな感じ:

2014/07/27

Amazon SNSを使ってSESメールのホワイトリスト管理

ご無沙汰です。

メールマガジンって色々大変ですよね?私がその大変さを実感したのが最近ばかりのことですが、今日はAmazonのSimple Notification Serviceを使ってホワイトリストの管理を簡単にできる方法を紹介させてもらいたいと思います。

SESを使ってメールを送信した時に、ユーザのメールアドレスが存在しなかったため送信完了できなかった場合(Bounce)や、ユーザが「迷惑メール」のボタンを押した(Complaint)場合、Amazonからエラーの詳細が書いてあるメールを送ってもらうのがデフォルトの設定かと思いますが、それ以外にもSNSの通信を送ってもらうこともできます。

そしてSNSにはHTTPSのインタフェースもあるので、簡単なAPIを立ち上げて、BounceとComplaintを自動的にブラックリスト化をすることが意外と簡単です。

SNS Topicの作成の仕方や送信先(endpoint)の認証の仕方が丁寧にドキュメントに書いてありますが、BounceとComplaintの場合どんなメッセージが書いてくるかというと、こんな感じです:

POST / HTTP/1.1
x-amz-sns-message-type: Notification
x-amz-sns-message-id: 22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324
x-amz-sns-topic-arn: arn:aws:sns:us-east-1:123456789012:MyTopic
x-amz-sns-subscription-arn: arn:aws:sns:us-east-1:123456789012:MyTopic:c9135db0-26c4-47ec-8998-413945fb5a96
Content-Length: 773
Content-Type: text/plain; charset=UTF-8
Host: example.com
Connection: Keep-Alive
User-Agent: Amazon Simple Notification Service Agent

{
  "Type" : "Notification",
  "MessageId" : "22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324",
  "TopicArn" : "arn:aws:sns:us-east-1:123456789012:MyTopic",
  "Message" : "{\\"notificationType\\":\\"Bounce\\",\\"bounce\\":{\\"bounceSubType\\":\\"General\\",\\"bounceType\\":\\"Permanent\\",\\"reportingMTA\\":\\"dsn; a8-41.smtp-out.amazonses.com\\",   \\"bouncedRecipients\\":[{\\"status\\":\\"5.1.1\\",\\"action\\":\\"failed\\",\\"diagnosticCode\\":\\"smtp; 554 5.1.1 <recipient@example.com>: Recipient address rejected: User unknown\\",      \\"emailAddress\\":\\"recipient@example.com\\"}],\\"timestamp\\":\\"2014-07-27T09:39:22.070Z\\",\\"feedbackId\\":\\"00000147773054e9-96ae8a22-c833-4967-874c-d56accc9fd2d-000000\\"},\\"mail\\":{\\"timestamp\\":\\"2014-07-27T09:39:18.000Z\\",\\"source\\":\\"noreply@example.come\\",\\"messageId\\":\\"0000014777304606-b5a16bd1-5a20-40ef-993b-0395f14de101-000000\\",\\"destination\\":         [\\"recipient@example.com\\"]}}",
  "Timestamp" : "2012-05-02T00:54:06.655Z",
  "SignatureVersion" : "1",
  "Signature" : "EXAMPLEw6JRNwm1LFQL4ICB0bnXrdB8ClRMTQFGBqwLpGbM78tJ4etTwC5zU7O3tS6tGpey3ejedNdOJ+1fkIp9F2/LmNVKb5aFlYq+9rk9ZiPph5YlLmWsDcyC5T+Sy9/umic5S0UQc2PEtgdpVBahwNOdMW4JPwk0kAJJztnc=",
  "SigningCertURL" : "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem",
  "UnsubscribeURL" : "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789012:MyTopic:c9135db0-26c4-47ec-8998-413945fb5a96"
  }

リクエストbodyのJSONなのですが、JSONなのにContent-Typeが「text/plain」のでご注意ください。そして、今回の話題のブラックリストに入れたいメールアドレスがMessageの項目に入っています。エスケープされているJSONなので、これもデコード必要ですね。

私はPlack::Requestからrawのリクエストbodyを取得して、デコードしてみました。


if ($request->headers->header('x-amz-sns-message-type') eq "Notification") {
        my $raw_body = $request->raw_body;
        $raw_body =~ s/\\\\/\\/g;

        my $decoder = JSON::XS->new->utf8;
        my $json = $decoder->decode($raw_body) or die "could not decode json";
        my $message = $decoder->decode($json->{Message}) or die "could not decode json";

        my @addresses;
        if ($message->{notificationType} eq "Bounce") {
            my $bounced_recipients = $message->{bounce}->{bouncedRecipients};
            @addresses = map { $_->{emailAddress} } @$bounced_recipients;

        } elsif ($message->{notificationType} eq "Complaint") {
            my $complaining_recipients = $message->{complaint}->{complainedRecipients};
            @addresses = map { $_->{emailAddress} } @$complaining_recipients;

        }

        for my $address (@addresses) {
            #対象メールアドレスをブラックリストに追加
        }
}


意外と簡単でした!これでばっちり!

2013/07/11

VimからCorona SDKのシミュレーターを起動する方法

最近アプリ作りたくて、フレームワーク色々いじってきたところです。JavaやC++とかを使うとちゃんとしたIDEがないと困るかと思いますが、Eclipseって使うとなんかだるいですよね。。。 まぁ、とにかく気軽にスクリプト言語でかけるフレームワークを探しているところCorona SDKを見つけました。Lua全くわかんないというのがありましたが、どっちかというとC++を習うほど大変じゃないですし、なんとvimでの開発環境を構築するのが簡単すぎて、ほぼ一目惚れでした。

SDKをインストールができたら、以下をvimrcに張っておくぐらいで十分:


map  :!/Applications/CoronaSDK/Corona\ Terminal -project %:p -skin iPhone<CR>


*Mac上のCorona Terminalのデファルトパスの場合です

main.luaがvimで開いた時、を押すことによってコロナのiPhoneシミュレーターを起動できるので、とても便利です。

2013/05/29

[技術メモ] CentOS 5.8でOpenSSLを更新

先月、hachioji.pmでinstagramのAPIをたたいて画像を表示する機能を作ったのですが、ローカルで問題なく動かすことができたのに、レンタルサーバーにアップしたら、IO::Socket::SSL

Client side SNI not supported for this openssl

ってエラで動かなくて、びっくりしました。(汗)
具体的になにをインストールしたらいいかわからなくて、そこでちょっと挫折して、直すのに大分さぼっちゃいましたが、次回のため、一応どう対応したかをメモっときましょう。

僕が使ってるCentOS 5.8にデファルトで入ってるOpenSSLのバージョンが0.9.8ですが、IO::Socket::SSLのドキュメントによるとOpenSSL 1.0以上が必要そうです:

Support for SNI on the client side was added somewhere in the OpenSSL 0.9.8 series, but only with 1.0 a bug was fixed when the server could not decide about its hostname. Therefore client side SNI is only supported with OpenSSL 1.0 or higher in IO::Socket::SSL.

OpenSSLを更新するにはAxivoのRepository Packageをいれるとyum installでできます。

rpm -ivh --nosignature http://rpm.axivo.com/redhat/axivo-release-5-1.noarch.rpm


それができたら、インストールするのにopenssl.x86_64が必要なので、それが入ってること確認できたら、以下のコマンドで更新:

yum remove openssl.i686
yum --enablerepo=axivo update openssl


詳しくはこちら

2013/04/21

英語圏の知らない英語、第1回:justの使い方

このブログなんですが、日本語で書き続けるべきか、母語の英語で書けばいいのか、色々検討していたのですが、やっぱり日本語で書きたいと決めました。確かに英語で書けば自分の言いたいことをもっとはっきり言えるはずですが、日本語で書くと「わかりづらい」とか「面白くない」とかを理由にして英語に切り替えれば、諦めたということになると思ったからです。
最近、Google I/Oのカンファレンスのビデオを見てて「The Myth of the Genius Programmer」というtalkを見ました。その中で「人は周りに評価されることで上手になるから、どんなに下手でも早めに自分のコードをgithubなど人が見えるところに投稿するといい。」というメッセージが載っています。
このブログってまさにそのためのもので、初心者の自分のコードを人に見せることで恥ずかしくても、とにかくプライドを捨てて投稿しています。
そして人間の言語も似てるものです。話せば話す程上手くなるので、間違いを気にせず、だんだんコミュニケーションをとればいいですよね。
でもその中で、もう一つの問題が出てしまいます。誰もこのブログ読んでませんw そのためにもうちょっとブログの範囲を広げられればいいと思って、英語や言語取得などについても少し書いてみようと思います。東京に住んでいる外国人として、和製英語で誤解が発生しているのを何回も経験したことあるので、和製英語と英語圏の使ってる英語の使い分けについてちょっと説明させていただきたいと思います。

さて、今日のword of the dayとして「just」を話題にしたいと思います。
昨日街歩いていたら、床屋の看板に「just cut 10 min」を見かけました。この文章は日本人にどうとられるか確実にわかりませんが、日本語ではジャストっていうは「丁度いい」という意味で使われているので、これはおそらく「10分でお似合いの髪型にしてあげます」的なことを伝えたかったでしょうと思います。ただ、英語の上記の文章ですと英語圏の人なら「カットのみ 10分」に取られます。なぜならば、英語では「just」というのは「ただの〜・〜だけ・〜に過ぎない」という意味になります。一般的にネガティブな意味になっているので、そんな看板をみて「その床屋さんでシャンプーはしてくれないのかな」とか思う人も出てしまうでしょう。
英語で上記の文章を書き直せば、「the perfect cut in under ten minutes」が正解ですかね。
確かに英語で「just right」「just perfect」という言い方もあります。こちらはネガティブではなくて、日本語のジャストに近い意味を持っています。おそらく日本語のジャストの由来がこのjust right(丁度いい)ですが、「いいことだらけで悪いことがまざっていない」という意味でjust(だけ)が使えるわけだと思います。
おまけによく見られる和製英語を英語圏に通じそうな英語に翻訳してみました:

和製英語 英語
ジャストカット perfect haircut
ジャストフィット close fitting / snug
ジャストタイミング perfect timing
9時ジャスト exactly 9 o'clock

※実は形容動詞のjustが「だけ」意外の意味がもう一つあります。それは「ぎりぎり」って意味です。例えば、「just in time」(ぎりぎり間に合った)、「just married」(結婚したばかり)とか様々な表現に出たりします。

2013/03/18

TDD Bootcamp Tokyo 2013

16日にTDD Boot Camp Tokyoに参加してきました。
PHPer、Rubyist、Javaer(? ...Javaやってる人って言い方なんかありますか) はやっぱり多かったですが、4番目のテーブルでC#、Scala、Objective-Cなど様々な言語も出てきました。

僕は今職場のプロジェクトでPHPを使っていますので、PHPのテストフレームワークでも勉強しようかなと思っていましたが、やっぱり終末になるとPHPは書きたくないです!
日本語のドキュメントがないせいかわからないけど、そもそも僕が使おうと思っていたPHPフレームワーク(Testify.php)があまり人気がないようです・・・

というわけで、Perlのイベントで知り合ったハッカーとペアプロをして、Test::Moreを使って参加しました。日本語キーボードに慣れてしまった僕とVimを普段利用しない彼という事情でちょっと辛かったのもありましたが、楽しかったです。
機能を実装する前にまずテストを書くという手段がなかなか面白いし、意外とやりやすかったです。

そして最後にじゃんけんで買ってGithubのTシャーツを貰いました。^^


2012/12/02

シンプルなAndroid RSSリーダーを作ってみました

もう12月ですね。ということはアドベントカレンダーの季節になりました♪

子供の時、アドベントカレンダー(チョコレートが入っているほう)でずいぶん楽しんでましたので、大人になった自分もこの季節を楽しめるようにPerl Advent Calendar(http://www.perladvent.org)のRSSリーダーを作成してみました。

RSSのXMLの構成はこんな構成

<entry>
  <title>タイトル</title>
  <id>URL</id>
  <summary>要約</summary>
  <updated>更新時間</updated>
</entry>

なので、以下のようなPOJOを作成しました。


public class Entry implements Serializable {   //後でIntentのBundleに入れるようにSerializableを実装
 
 private String title;
 private String link;
 private String summary;
 private String updated;
 
 public void setTitle(String title) {
  this.title = title;
 }
 
 public String getTitle() {
  return this.title;
 }
 
 //...長いので、linkとsummaryのゲッターとセッターを略します
 
 public void setUpdated(String updated) {
  String dateOnly = updated.substring(0, 10); 
  this.updated = dateOnly;
 }
 
 public String getUpdated() {
  return this.updated;
 }
}


SAXパーサーを使うことにしました。ハンドラークラスを使ってstartElementメソッドで適当なタグ名を見つけたらフラグを立って、中身のテキストを全部引っ張ってきて、最後にendElementメソッドでフラグを消すという訳ですが、なぜかSAXの仕様でcharactersのメソッドが何回も呼ばれることもあるらしくて、たまにテキストが2・3回読み込まれてて、困りました・・・



長い分を読み込むためにcharactersを複数回呼ぶことが必要と思うので、あまりいい解決方法ではないかと思いますけど、文字を追加してくれるbuilder.appendを呼ぶ直前にbuilder.setLength(0)でリセットすることでとりあえず解決できました・・・


builder.setLength(0);
builder.append(ch, start, length);



奇麗に表示できました。



ハンドラークラスはこんな感じになっています:


public class RSSHandler extends DefaultHandler {
 private ArrayList entries;
 private Entry currentEntry;
 private StringBuilder builder;
 
 boolean inTitle;
 boolean inLink;
 boolean inSummary;
 boolean inUpdated;
 boolean inEntry;
 
 public ArrayList getEntries() {
  return this.entries;
 }
 
 @Override
 public void startDocument() throws SAXException {
  super.startDocument();
  entries = new ArrayList();
  builder = new StringBuilder();
 }

 @Override
 public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
  super.startElement(uri, localName, name, attributes);
  if (localName.equalsIgnoreCase(ENTRY)) {
   this.currentEntry = new Entry();
   inEntry = true;
  }
  if (localName.equalsIgnoreCase(TITLE)) {
   inTitle = true;
  }
  if (localName.equalsIgnoreCase(LINK)) {
   inLink = true;
  }
  if (localName.equalsIgnoreCase(SUMMARY)) {
   inSummary = true;
  }
  if (localName.equalsIgnoreCase(UPDATED)) {
   inUpdated = true;
  }
 }
 
 @Override
 public void characters(char[] ch, int start, int length) throws SAXException {
  super.characters(ch, start, length);
  
  if (inEntry) {
   if (inTitle) {
    builder.setLength(0);
    builder.append(ch, start, length);
   }
   if (inLink) {
    builder.setLength(0);
    builder.append(ch, start, length);
   }
   if (inSummary) {
    builder.append(ch, start, length);
   }
   if (inUpdated) {
    builder.setLength(0);
    builder.append(ch, start, length);
   }
  }
 }
 
 @Override
 public void endElement(String uri, String localName, String name) throws SAXException {
  super.endElement(uri, localName, name);
  if (this.currentEntry != null) {
   if (localName.equalsIgnoreCase(TITLE)) {
    currentEntry.setTitle(builder.toString());
    inTitle = false;
   } else if (localName.equalsIgnoreCase(LINK)) {
    currentEntry.setLink(builder.toString());
    inLink = false;
   } else if (localName.equalsIgnoreCase(SUMMARY)) {
    currentEntry.setSummary(builder.toString());
    inSummary = false;
   } else if (localName.equalsIgnoreCase(UPDATED)) {
    currentEntry.setUpdated(builder.toString());
    inUpdated = false;
   } 
    
   if (localName.equalsIgnoreCase(ENTRY)) {
    entries.add(currentEntry);
    inEntry = false;
   }
  }
 }
}


そしてAsyncTaskでSAXを呼ぶことにして、出来上がったArrayListを返すことにしました。


@Override
 protected ArrayList doInBackground(Void... params) {

  try {
   URL url = new URL(xmlLocation);
   SAXParserFactory spf = SAXParserFactory.newInstance();
   SAXParser sp = spf.newSAXParser();
  
   XMLReader xr = sp.getXMLReader();
   RSSHandler handler = new RSSHandler();
   xr.setContentHandler(handler);     //ハンドラークラスを設定
   
   xr.parse(new InputSource(url.openStream()));
   
   entries = handler.getEntries();
  } catch (MalformedURLException e) {
   Log.e("LoadFeedData", "MalformedUrlException: ", e);
  } catch (Exception e) {
   Log.e("LoadFeedData", "Parsing exception", e);
  }
  return entries;
 }


後は、ArrayAdapterに渡して、そこで項目ごとにListViewにデーターを入れるだけです。

ちなみに、詳細はViewはこんな感じに表示してみました。コードはHighlighted Syntaxで読んだほうが楽なので、結局ブラウザーで読みたくて「ブラウザーで表示」みたいなボタンも用意しておきました(汗)



以前にJSONデータを読み込んでListViewで表示するようなアプリは作ったことはあったんですが、XMLは初めてですので、勉強になりました。SAX Parserの仕様をもっと深く勉強する必要があると思いますけど、一応、このRSSリーダーでアドベントカレンダーを読んでおきます。

2012/11/25

ブログを開始しました

こんにちは!laoujiと言います。26歳。
エンジニアにクラスチェンジしてからまだ1年経ってないので、どっちかというと初心者向けコンテンツになりそうですが、Techブログやらせていただきます。
プログラマーはみんなブログやっているイメージがあるので、自分もそろそろやってみようかと思いました。実際何を書けばいいかよくわかりませんが、まずブログソフトを配置すれば、何とかなるかなと思いました(汗)。
ではよろしくお願いします!