Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Go implementation #5

Merged
merged 3 commits into from
Jun 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Dockerfile_app
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ RUN git clone https://github.com/pyenv/pyenv.git ~/.pyenv && \
pyenv install 3.6.5 && pyenv global 3.6.5 && \
cd && curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && python get-pip.py && rm get-pip.py

# Go のインストール
RUN sudo wget https://dl.google.com/go/go1.10.2.linux-amd64.tar.gz && \
sudo tar -C /usr/local -xzf go1.10.2.linux-amd64.tar.gz && \
sudo rm go1.10.2.linux-amd64.tar.gz
ENV PATH $PATH:/usr/local/go/bin
ENV GOROOT /usr/local/go
ENV GOPATH $HOME/.local/go
ENV PATH $PATH:$GOROOT/bin

# アプリケーション
RUN mkdir /home/ishocon/data /home/ishocon/webapp
COPY admin/ishocon2.dump.tar.bz2 /home/ishocon/data/ishocon2.dump.tar.bz2
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ ISHOCONとは `Iikanjina SHOwwin CONtest` の略で、[ISUCON](http://isucon.net

## 問題詳細
* マニュアル: [ISHOCON2マニュアル](https://github.com/showwin/ISHOCON2/blob/master/doc/manual.md)
* アプリケーションAMI: `ami-fcba6b9d`
* アプリケーションAMI: `ami-63a8601c`
* ベンチマーカーAMI: `ami-eb9c4d8a`
* インスタンスタイプ: `c4.large` (アプリ、ベンチ共に)
* 参考実装言語: Ruby, Python
* 参考実装言語: Ruby, Python, Go

* AWSではなく手元で実行したい場合には [Docker を使ってローカルで環境を整える](https://github.com/showwin/ISHOCON2/blob/master/doc/local_manual.md) をご覧ください。

Expand Down
1 change: 0 additions & 1 deletion doc/local_manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
```
$ git clone git@github.com:showwin/ISHOCON2.git
$ cd ISHOCON2
$ docker-compose build
$ docker-compose up
# app_1 と bench_1 のログに 'setup completed.' と出たら起動完了
```
Expand Down
9 changes: 9 additions & 0 deletions doc/manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ $ cd ~/webapp/python
$ uwsgi --ini app.ini
```

#### Go の場合

```
$ cd ~/webapp/go
$ go get -t -d -v ./...
$ go build -o webapp *.go
$ ./webapp
```

これでブラウザからアプリケーションが見れるようになるので、IPアドレスにアクセスしてみましょう。
HTTPS でのみアクセスできることに注意してください。ブラウザによっては証明書のエラーが表示されますが、無視してページを表示してください。

Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: '3.0'
services:
app:
image: showwin/ishocon2_app:0.9.1
image: showwin/ishocon2_app:1.1.0
command: /docker/start_app.sh
volumes:
- storage_app:/var/lib/mysql
Expand All @@ -15,7 +15,7 @@ services:
- /var/lib/mysql

bench:
image: showwin/ishocon2_bench:0.9.1
image: showwin/ishocon2_bench:1.1.0
command: /docker/start_bench.sh
volumes:
- storage_bench:/var/lib/mysql
Expand Down
4 changes: 2 additions & 2 deletions docker/start_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ sudo service nginx start
sudo service mysql start
sudo chown -R mysql:mysql /var/lib/mysql /var/run/mysqld
sudo service mysql start # 正しく起動
sudo mysql -u root -pishocon -e 'CREATE DATABASE ishocon2;' && \
sudo mysql -u root -pishocon -e "CREATE USER ishocon IDENTIFIED BY 'ishocon';" && \
sudo mysql -u root -pishocon -e 'CREATE DATABASE IF NOT EXISTS ishocon2;' && \
sudo mysql -u root -pishocon -e "CREATE USER IF NOT EXISTS ishocon IDENTIFIED BY 'ishocon';" && \
sudo mysql -u root -pishocon -e 'GRANT ALL ON *.* TO ishocon;' && \
cd ~/data && tar -jxvf ishocon2.dump.tar.bz2 && sudo mysql -u root -pishocon ishocon2 < ~/data/ishocon2.dump

Expand Down
4 changes: 2 additions & 2 deletions docker/start_bench.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
service mysql start # なぜか失敗する(調査中)
chown -R mysql:mysql /var/lib/mysql /var/run/mysqld
service mysql start # 正しく起動
mysql -u root -pishocon -e 'CREATE DATABASE ishocon2;' && \
mysql -u root -pishocon -e "CREATE USER ishocon IDENTIFIED BY 'ishocon';" && \
mysql -u root -pishocon -e 'CREATE DATABASE IF NOT EXISTS ishocon2;' && \
mysql -u root -pishocon -e "CREATE USER IF NOT EXISTS ishocon IDENTIFIED BY 'ishocon';" && \
mysql -u root -pishocon -e 'GRANT ALL ON *.* TO ishocon;' && \
cd /admin && tar -jxvf ishocon2.dump.tar.bz2 && mysql -u root -pishocon ishocon2 < /admin/ishocon2.dump

Expand Down
116 changes: 116 additions & 0 deletions webapp/go/candidate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package main

// Candidate Model
type Candidate struct {
ID int
Name string
PoliticalParty string
Sex string
}

// CandidateElectionResult type
type CandidateElectionResult struct {
ID int
Name string
PoliticalParty string
Sex string
VoteCount int
}

// PartyElectionResult type
type PartyElectionResult struct {
PoliticalParty string
VoteCount int
}

func getAllCandidate() (candidates []Candidate) {
rows, err := db.Query("SELECT * FROM candidates")
if err != nil {
panic(err.Error())
}
defer rows.Close()

for rows.Next() {
c := Candidate{}
err = rows.Scan(&c.ID, &c.Name, &c.PoliticalParty, &c.Sex)
if err != nil {
panic(err.Error())
}
candidates = append(candidates, c)
}
return
}

func getCandidate(candidateID int) (c Candidate, err error) {
row := db.QueryRow("SELECT * FROM candidates WHERE id = ?", candidateID)
err = row.Scan(&c.ID, &c.Name, &c.PoliticalParty, &c.Sex)
return
}

func getCandidateByName(name string) (c Candidate, err error) {
row := db.QueryRow("SELECT * FROM candidates WHERE name = ?", name)
err = row.Scan(&c.ID, &c.Name, &c.PoliticalParty, &c.Sex)
return
}

func getAllPartyName() (partyNames []string) {
rows, err := db.Query("SELECT political_party FROM candidates GROUP BY political_party")
if err != nil {
panic(err.Error())
}
defer rows.Close()

for rows.Next() {
var name string
err = rows.Scan(&name)
if err != nil {
panic(err.Error())
}
partyNames = append(partyNames, name)
}
return
}

func getCandidatesByPoliticalParty(party string) (candidates []Candidate) {
rows, err := db.Query("SELECT * FROM candidates WHERE political_party = ?", party)
if err != nil {
panic(err.Error())
}
defer rows.Close()

for rows.Next() {
c := Candidate{}
err = rows.Scan(&c.ID, &c.Name, &c.PoliticalParty, &c.Sex)
if err != nil {
panic(err.Error())
}
candidates = append(candidates, c)
}
return
}

func getElectionResult() (result []CandidateElectionResult) {
rows, err := db.Query(`
SELECT c.id, c.name, c.political_party, c.sex, IFNULL(v.count, 0)
FROM candidates AS c
LEFT OUTER JOIN
(SELECT candidate_id, COUNT(*) AS count
FROM votes
GROUP BY candidate_id) AS v
ON c.id = v.candidate_id
ORDER BY v.count DESC`)
if err != nil {
panic(err.Error())
}
defer rows.Close()

for rows.Next() {
r := CandidateElectionResult{}
err = rows.Scan(&r.ID, &r.Name, &r.PoliticalParty, &r.Sex, &r.VoteCount)
if err != nil {
panic(err.Error())
}
result = append(result, r)
}
return
}
189 changes: 189 additions & 0 deletions webapp/go/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package main

import (
"database/sql"
"html/template"
"net/http"
"os"
"sort"
"strconv"

"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/contrib/static"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
)

var db *sql.DB

func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}

func main() {
// database setting
user := getEnv("ISHOCON2_DB_USER", "ishocon")
pass := getEnv("ISHOCON2_DB_PASSWORD", "ishocon")
dbname := getEnv("ISHOCON2_DB_NAME", "ishocon2")
db, _ = sql.Open("mysql", user+":"+pass+"@/"+dbname)
db.SetMaxIdleConns(5)

gin.SetMode(gin.DebugMode)
r := gin.Default()
r.Use(static.Serve("/css", static.LocalFile("public/css", true)))
layout := "templates/layout.tmpl"

// session store
store := sessions.NewCookieStore([]byte("mysession"))
store.Options(sessions.Options{HttpOnly: true})
r.Use(sessions.Sessions("showwin_happy", store))

// GET /
r.GET("/", func(c *gin.Context) {
electionResults := getElectionResult()

// 上位10人と最下位のみ表示
tmp := make([]CandidateElectionResult, len(electionResults))
copy(tmp, electionResults)
candidates := tmp[:10]
candidates = append(candidates, tmp[len(tmp)-1])

partyNames := getAllPartyName()
partyResultMap := map[string]int{}
for _, name := range partyNames {
partyResultMap[name] = 0
}
for _, r := range electionResults {
partyResultMap[r.PoliticalParty] += r.VoteCount
}
partyResults := []PartyElectionResult{}
for name, count := range partyResultMap {
r := PartyElectionResult{}
r.PoliticalParty = name
r.VoteCount = count
partyResults = append(partyResults, r)
}
// 投票数でソート
sort.Slice(partyResults, func(i, j int) bool { return partyResults[i].VoteCount > partyResults[j].VoteCount })

sexRatio := map[string]int{
"men": 0,
"women": 0,
}
for _, r := range electionResults {
if r.Sex == "男" {
sexRatio["men"] += r.VoteCount
} else if r.Sex == "女" {
sexRatio["women"] += r.VoteCount
}
}

funcs := template.FuncMap{"indexPlus1": func(i int) int { return i + 1 }}
r.SetHTMLTemplate(template.Must(template.New("main").Funcs(funcs).ParseFiles(layout, "templates/index.tmpl")))
c.HTML(http.StatusOK, "base", gin.H{
"candidates": candidates,
"parties": partyResults,
"sexRatio": sexRatio,
})
})

// GET /candidates/:candidateID(int)
r.GET("/candidates/:candidateID", func(c *gin.Context) {
candidateID, _ := strconv.Atoi(c.Param("candidateID"))
candidate, err := getCandidate(candidateID)
if err != nil {
c.Redirect(http.StatusFound, "/")
}
votes := getVoteCountByCandidateID(candidateID)
candidateIDs := []int{candidateID}
keywords := getVoiceOfSupporter(candidateIDs)

r.SetHTMLTemplate(template.Must(template.ParseFiles(layout, "templates/candidate.tmpl")))
c.HTML(http.StatusOK, "base", gin.H{
"candidate": candidate,
"votes": votes,
"keywords": keywords,
})
})

// GET /political_parties/:name(string)
r.GET("/political_parties/:name", func(c *gin.Context) {
partyName := c.Param("name")
var votes int
electionResults := getElectionResult()
for _, r := range electionResults {
if r.PoliticalParty == partyName {
votes += r.VoteCount
}
}

candidates := getCandidatesByPoliticalParty(partyName)
candidateIDs := []int{}
for _, c := range candidates {
candidateIDs = append(candidateIDs, c.ID)
}
keywords := getVoiceOfSupporter(candidateIDs)

r.SetHTMLTemplate(template.Must(template.ParseFiles(layout, "templates/political_party.tmpl")))
c.HTML(http.StatusOK, "base", gin.H{
"politicalParty": partyName,
"votes": votes,
"candidates": candidates,
"keywords": keywords,
})
})

// GET /vote
r.GET("/vote", func(c *gin.Context) {
candidates := getAllCandidate()

r.SetHTMLTemplate(template.Must(template.ParseFiles(layout, "templates/vote.tmpl")))
c.HTML(http.StatusOK, "base", gin.H{
"candidates": candidates,
"message": "",
})
})

// POST /vote
r.POST("/vote", func(c *gin.Context) {
user, userErr := getUser(c.PostForm("name"), c.PostForm("address"), c.PostForm("mynumber"))
candidate, cndErr := getCandidateByName(c.PostForm("candidate"))
votedCount := getUserVotedCount(user.ID)
candidates := getAllCandidate()
voteCount, _ := strconv.Atoi(c.PostForm("vote_count"))

var message string
r.SetHTMLTemplate(template.Must(template.ParseFiles(layout, "templates/vote.tmpl")))
if userErr != nil {
message = "個人情報に誤りがあります"
} else if user.Votes < voteCount+votedCount {
message = "投票数が上限を超えています"
} else if c.PostForm("candidate") == "" {
message = "候補者を記入してください"
} else if cndErr != nil {
message = "候補者を正しく記入してください"
} else if c.PostForm("keyword") == "" {
message = "投票理由を記入してください"
} else {
for i := 1; i <= voteCount; i++ {
createVote(user.ID, candidate.ID, c.PostForm("keyword"))
}
message = "投票に成功しました"
}
c.HTML(http.StatusOK, "base", gin.H{
"candidates": candidates,
"message": message,
})
})

r.GET("/initialize", func(c *gin.Context) {
db.Exec("DELETE FROM votes")

c.String(http.StatusOK, "Finish")
})

r.Run(":8080")
}
Loading