Distributed Applications with GO: Difference between revisions
(11 intermediate revisions by the same user not shown) | |||
Line 25: | Line 25: | ||
*Protocol | *Protocol | ||
=Sample App= | =Sample App= | ||
The sample app is a hybrid app using GO | The sample app is a hybrid app using GO and this can be found at https://gitlab.com/bibble235/gradebookapp | ||
<br> | <br> | ||
[[File:GO Demo Distributed App.png|500px]] | [[File:GO Demo Distributed App.png|500px]] | ||
Line 823: | Line 823: | ||
===GO Templates=== | ===GO Templates=== | ||
This seems to be a bit like pug, asp or other template but to GO. | This seems to be a bit like pug, asp or other template but to GO. | ||
====Templates==== | |||
This file defines the files to load. | |||
<syntaxhighlight lang="go"> | |||
package teacherportal | |||
import ( | |||
"html/template" | |||
) | |||
var rootTemplate *template.Template | |||
func ImportTemplates() error { | |||
var err error | |||
rootTemplate, err = template.ParseFiles( | |||
"teacherportal/students.gohtml", | |||
"teacherportal/student.gohtml", | |||
"teacherportal/grades.gohtml") | |||
if err != nil { | |||
return err | |||
} | |||
return nil | |||
} | |||
</syntaxhighlight> | |||
====Student==== | ====Student==== | ||
<syntaxhighlight lang=" | <syntaxhighlight lang="html"> | ||
<!DOCTYPE html> | <!DOCTYPE html> | ||
<html lang="en"> | <html lang="en"> | ||
Line 889: | Line 914: | ||
</html> | </html> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
====Students==== | ====Students==== | ||
<syntaxhighlight lang=" | <syntaxhighlight lang="html"> | ||
<!DOCTYPE html> | <!DOCTYPE html> | ||
<html lang="en"> | <html lang="en"> | ||
Line 924: | Line 950: | ||
</html> | </html> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
====Grades==== | ====Grades==== | ||
<syntaxhighlight lang=" | <syntaxhighlight lang="html"> | ||
<!DOCTYPE html> | <!DOCTYPE html> | ||
<html lang="en"> | <html lang="en"> | ||
Line 938: | Line 965: | ||
</html> | </html> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==== | ===Handlers=== | ||
This is like EJS templates. Basically the data is retrieved and render is called to show the server-side templates. | |||
<syntaxhighlight lang="go"> | |||
package teacherportal | |||
import ( | |||
"app/grades" | |||
"app/registry" | |||
"bytes" | |||
"encoding/json" | |||
"fmt" | |||
"log" | |||
"net/http" | |||
"strconv" | |||
"strings" | |||
) | |||
func RegisterHandlers() { | |||
http.Handle("/", http.RedirectHandler("/students", http.StatusPermanentRedirect)) | |||
h := new(studentsHandler) | |||
http.Handle("/students", h) | |||
http.Handle("/students/", h) | |||
} | |||
type studentsHandler struct{} | |||
func (sh studentsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
pathSegments := strings.Split(r.URL.Path, "/") | |||
switch len(pathSegments) { | |||
case 2: // /students | |||
sh.renderStudents(w, r) | |||
case 3: // /students/{:id} | |||
id, err := strconv.Atoi(pathSegments[2]) | |||
if err != nil { | |||
w.WriteHeader(http.StatusNotFound) | |||
return | |||
} | |||
sh.renderStudent(w, r, id) | |||
case 4: // /students/{:id}/grades | |||
id, err := strconv.Atoi(pathSegments[2]) | |||
if err != nil { | |||
w.WriteHeader(http.StatusNotFound) | |||
return | |||
} | |||
if strings.ToLower(pathSegments[3]) != "grades" { | |||
w.WriteHeader(http.StatusNotFound) | |||
return | |||
} | |||
sh.renderGrades(w, r, id) | |||
default: | |||
w.WriteHeader(http.StatusNotFound) | |||
} | |||
} | |||
func (studentsHandler) renderStudents(w http.ResponseWriter, r *http.Request) { | |||
var err error | |||
defer func() { | |||
if err != nil { | |||
w.WriteHeader(http.StatusInternalServerError) | |||
log.Println("Error retrieving students: ", err) | |||
} | |||
}() | |||
serviceURL, err := registry.GetProvider(registry.GradingService) | |||
if err != nil { | |||
return | |||
} | |||
res, err := http.Get(serviceURL + "/students") | |||
if err != nil { | |||
return | |||
} | |||
var s grades.Students | |||
err = json.NewDecoder(res.Body).Decode(&s) | |||
if err != nil { | |||
return | |||
} | |||
rootTemplate.Lookup("students.gohtml").Execute(w, s) | |||
} | |||
func (studentsHandler) renderStudent(w http.ResponseWriter, r *http.Request, id int) { | |||
var err error | |||
defer func() { | |||
if err != nil { | |||
w.WriteHeader(http.StatusInternalServerError) | |||
log.Println("Error retrieving students: ", err) | |||
return | |||
} | |||
}() | |||
serviceURL, err := registry.GetProvider(registry.GradingService) | |||
if err != nil { | |||
return | |||
} | |||
res, err := http.Get(fmt.Sprintf("%v/students/%v", serviceURL, id)) | |||
if err != nil { | |||
return | |||
} | |||
var s grades.Student | |||
err = json.NewDecoder(res.Body).Decode(&s) | |||
if err != nil { | |||
return | |||
} | |||
rootTemplate.Lookup("student.gohtml").Execute(w, s) | |||
} | |||
func (studentsHandler) renderGrades(w http.ResponseWriter, r *http.Request, id int) { | |||
if r.Method != http.MethodPost { | |||
w.WriteHeader(http.StatusMethodNotAllowed) | |||
return | |||
} | |||
defer func() { | |||
w.Header().Add("location", fmt.Sprintf("/students/%v", id)) | |||
w.WriteHeader(http.StatusTemporaryRedirect) | |||
}() | |||
title := r.FormValue("Title") | |||
gradeType := r.FormValue("Type") | |||
score, err := strconv.ParseFloat(r.FormValue("Score"), 32) | |||
if err != nil { | |||
log.Println("Failed to parse score: ", err) | |||
return | |||
} | |||
g := grades.Grade{ | |||
Title: title, | |||
Type: grades.GradeType(gradeType), | |||
Score: float32(score), | |||
} | |||
data, err := json.Marshal(g) | |||
if err != nil { | |||
log.Println("Failed to convert grade to JSON: ", g, err) | |||
} | |||
serviceURL, err := registry.GetProvider(registry.GradingService) | |||
if err != nil { | |||
log.Println("Failed to retrieve instance of Grading Service", err) | |||
return | |||
} | |||
res, err := http.Post(fmt.Sprintf("%v/students/%v/grades", serviceURL, id), "application/json", bytes.NewBuffer(data)) | |||
if err != nil { | |||
log.Println("Failed to save grade to Grading Service", err) | |||
return | |||
} | |||
if res.StatusCode != http.StatusCreated { | |||
log.Println("Failed to save grade to Grading Service. Status: ", res.StatusCode) | |||
return | |||
} | |||
} | |||
</syntaxhighlight> | |||
==Service== | |||
This package is the API responsible for starting the services. | |||
<syntaxhighlight lang="go"> | |||
package service | |||
import ( | |||
"app/registry" | |||
"context" | |||
"fmt" | |||
stlog "log" | |||
"net/http" | |||
) | |||
func Start(ctx context.Context, host, port string, reg registry.Registration, registerHandlersFunc func()) (context.Context, error) { | |||
registerHandlersFunc() | |||
ctx = startService(ctx, reg.ServiceName, host, port) | |||
err := registry.RegisterService(reg) | |||
if err != nil { | |||
return ctx, err | |||
} | |||
return ctx, nil | |||
} | |||
func startService(ctx context.Context, serviceName registry.ServiceName, host, port string) context.Context { | |||
ctx, cancel := context.WithCancel(ctx) | |||
var srv http.Server | |||
srv.Addr = ":" + port | |||
go func() { | |||
stlog.Println(srv.ListenAndServe()) | |||
cancel() | |||
}() | |||
go func() { | |||
fmt.Printf("%v started. Press any key to stop.\n", serviceName) | |||
var s string | |||
fmt.Scanln(&s) | |||
err := registry.ShutdownService(fmt.Sprintf("http://%v:%v", host, port)) | |||
if err != nil { | |||
stlog.Println(err) | |||
} | |||
srv.Shutdown(ctx) | |||
cancel() | |||
}() | |||
return ctx | |||
} | |||
</syntaxhighlight> | |||
==cmds (Where GO puts main)== | |||
Here for completeness are the four mains for the four services. | |||
===Log Service=== | |||
<syntaxhighlight lang="go"> | |||
package main | |||
import ( | |||
"app/log" | |||
"app/registry" | |||
"app/service" | |||
"context" | |||
"fmt" | |||
stlog "log" | |||
) | |||
func main() { | |||
log.Run("./app.log") | |||
host, port := "localhost", "4000" | |||
serviceAddress := fmt.Sprintf("http://%v:%v", host, port) | |||
var r registry.Registration | |||
r.ServiceName = registry.LogService | |||
r.ServiceURL = serviceAddress | |||
r.HeartbeatURL = r.ServiceURL + "/heartbeat" | |||
r.RequiredServices = make([]registry.ServiceName, 0) | |||
r.ServiceUpdateURL = r.ServiceURL + "/services" | |||
ctx, err := service.Start(context.Background(), | |||
host, | |||
port, | |||
r, | |||
log.RegisterHandlers) | |||
if err != nil { | |||
stlog.Fatal(err) | |||
} | |||
<-ctx.Done() | |||
fmt.Println("Shutting down log service") | |||
} | |||
</syntaxhighlight> | |||
===Registry Service=== | |||
Just creates and instance of the service. I waits for the context to end and report. | |||
<syntaxhighlight lang="go"> | |||
package main | |||
import ( | |||
"app/grades" | |||
"app/log" | |||
"app/registry" | |||
"app/service" | |||
"context" | |||
"fmt" | |||
stlog "log" | |||
) | |||
func main() { | |||
host, port := "localhost", "6000" | |||
serviceAddress := fmt.Sprintf("http://%v:%v", host, port) | |||
var r registry.Registration | |||
r.ServiceName = registry.GradingService | |||
r.ServiceURL = serviceAddress | |||
r.HeartbeatURL = r.ServiceURL + "/heartbeat" | |||
r.RequiredServices = make([]registry.ServiceName, 0) | |||
r.ServiceUpdateURL = r.ServiceURL + "/services" | |||
ctx, err := service.Start(context.Background(), | |||
host, | |||
port, | |||
r, | |||
grades.RegisterHandlers) | |||
if err != nil { | |||
stlog.Fatal(err) | |||
} | |||
if logProvider, err := registry.GetProvider(registry.LogService); err == nil { | |||
log.SetClientLogger(logProvider, r.ServiceName) | |||
} | |||
<-ctx.Done() | |||
fmt.Println("Shutting down grading service") | |||
} | |||
</syntaxhighlight> | |||
===Teach Portal Service=== | |||
<syntaxhighlight lang="go"> | |||
package main | |||
import ( | |||
"app/log" | |||
"app/registry" | |||
"app/service" | |||
"app/teacherportal" | |||
"context" | |||
"fmt" | |||
stlog "log" | |||
) | |||
func main() { | |||
err := teacherportal.ImportTemplates() | |||
if err != nil { | |||
stlog.Fatal(err) | |||
} | |||
host, port := "localhost", "5000" | |||
serviceAddress := fmt.Sprintf("http://%v:%v", host, port) | |||
var r registry.Registration | |||
r.ServiceName = registry.TeacherPortal | |||
r.ServiceURL = serviceAddress | |||
r.HeartbeatURL = r.ServiceURL + "/heartbeat" | |||
r.RequiredServices = []registry.ServiceName{ | |||
registry.LogService, | |||
registry.GradingService, | |||
} | |||
r.ServiceUpdateURL = r.ServiceURL + "/services" | |||
ctx, err := service.Start(context.Background(), | |||
host, | |||
port, | |||
r, | |||
teacherportal.RegisterHandlers) | |||
if err != nil { | |||
stlog.Fatal(err) | |||
} | |||
if logProvider, err := registry.GetProvider(registry.LogService); err == nil { | |||
log.SetClientLogger(logProvider, r.ServiceName) | |||
} | |||
<-ctx.Done() | |||
fmt.Println("Shutting down teacher portal") | |||
} | |||
</syntaxhighlight> | |||
===Grading Service=== | |||
<syntaxhighlight lang="go"> | <syntaxhighlight lang="go"> | ||
package main | |||
import ( | |||
"app/grades" | |||
"app/log" | |||
"app/registry" | |||
"app/service" | |||
"context" | |||
"fmt" | |||
stlog "log" | |||
) | |||
func main() { | |||
host, port := "localhost", "6000" | |||
serviceAddress := fmt.Sprintf("http://%v:%v", host, port) | |||
var r registry.Registration | |||
r.ServiceName = registry.GradingService | |||
r.ServiceURL = serviceAddress | |||
r.HeartbeatURL = r.ServiceURL + "/heartbeat" | |||
r.RequiredServices = make([]registry.ServiceName, 0) | |||
r.ServiceUpdateURL = r.ServiceURL + "/services" | |||
ctx, err := service.Start(context.Background(), | |||
host, | |||
port, | |||
r, | |||
grades.RegisterHandlers) | |||
if err != nil { | |||
stlog.Fatal(err) | |||
} | |||
if logProvider, err := registry.GetProvider(registry.LogService); err == nil { | |||
log.SetClientLogger(logProvider, r.ServiceName) | |||
} | |||
<-ctx.Done() | |||
fmt.Println("Shutting down grading service") | |||
} | |||
</syntaxhighlight> | </syntaxhighlight> | ||
Latest revision as of 07:27, 25 August 2021
Elements of a Distributed System
Characteristic
Four aspects might be
- Service Discovery
- Load Balancing
- Distributed tracing and logging
- Service Monitoring
Type of Distributed System
- Hub and Spoke (Satélite approach)
- Advantages Good for load balancing and logging
- Disadvantages Bad to single point of failure. Hub is complex due to responsibilities
- Peer to Peer where each communicate directly
- Advantages No Single point of failure. Highly decoupled
- Disadvantages Service discovery and Load Balancing hard
- Message Queue System where services get work from the queue
- Advantages Easy to scale, Persistence for disaster
- Disadvantages Single Point of failure (message queue), hard to configure
- Hybrid system (none of the above)
- This might will have advantages and disadvantage of both
Architectural Element
These are the aspect you may want to consider
- Languages
- Frameworks (Recommended Go-Kit and Go-Micro)
- Transports
- Protocol
Sample App
The sample app is a hybrid app using GO and this can be found at https://gitlab.com/bibble235/gradebookapp
This is the components to build
Introduction
I do not usually go through large portions of code but I thought it might be useful to look at the sample code and comment on the topic and the relationship with GO as a language.
Project Structure
The project structure was basically a root folder with a cmd directory holding the main.go code for each binary. From there there is one folder for each component.
Log Service
Client
- SetClientLogger - Sets the attribute for the standard log package
- Write - Writes data to server
package log
import (
"app/registry"
"bytes"
"fmt"
stlog "log"
"net/http"
)
func SetClientLogger(serviceURL string, clientService registry.ServiceName) {
stlog.SetPrefix(fmt.Sprintf("[%v] - ", clientService))
stlog.SetFlags(0)
stlog.SetOutput(&clientLogger{url: serviceURL})
}
type clientLogger struct {
url string
}
func (cl clientLogger) Write(data []byte) (int, error) {
b := bytes.NewBuffer([]byte(data))
res, err := http.Post(cl.url+"/log", "text/plain", b)
if err != nil {
return 0, err
}
if res.StatusCode != http.StatusOK {
return 0, fmt.Errorf("Failed to send log message. Service responded with %v - %v", res.StatusCode, res.Status)
}
return len(data), nil
}
Server (Endpoints)
This creates an instance of a custom log type, a handler and a function to write to the file.
- Run - Creates a custom log file using the standard log package
- Write - Writes data to the stream
- RegisterHandlers - Registers the "/log", reads the data and writes the message
package log
import (
"io/ioutil"
stlog "log"
"net/http"
"os"
)
var log *stlog.Logger
type fileLog string
func (fl fileLog) Write(data []byte) (int, error) {
f, err := os.OpenFile(string(fl), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
return 0, err
}
defer f.Close()
return f.Write(data)
}
func Run(destination string) {
log = stlog.New(fileLog(destination), "", stlog.LstdFlags)
}
func RegisterHandlers() {
http.HandleFunc("/log", func(w http.ResponseWriter, r *http.Request) {
msg, err := ioutil.ReadAll(r.Body)
if err != nil || len(msg) == 0 {
w.WriteHeader(http.StatusBadRequest)
return
}
write(string(msg))
})
}
func write(message string) {
log.Printf("%v\n", message)
}
Grades Service
This service has three aspects
- Mock data
- Grade Business Logic
- EndPoints
Mock Data
There is nothing special about this data but handy to remind yourself how to do this in GO
package grades
func init() {
students = []Student{
Student{
ID: 1,
FirstName: "Averill",
LastName: "Simen",
Grades: []Grade{
Grade{
Title: "Quiz 1",
Type: GradeQuiz,
Score: 85,
},
Grade{
Title: "Week 1 Homework",
Type: GradeHomework,
Score: 94,
},
Grade{
Title: "Quiz 2",
Type: GradeQuiz,
Score: 88,
},
},
},
Student{
ID: 2,
FirstName: "Marge",
LastName: "Garrard",
Grades: []Grade{
Grade{
Title: "Quiz 1",
Type: GradeQuiz,
Score: 100,
},
Grade{
Title: "Week 1 Homework",
Type: GradeHomework,
Score: 100,
},
Grade{
Title: "Quiz 2",
Type: GradeQuiz,
Score: 88,
},
},
},
Student{
ID: 3,
FirstName: "Sydnie",
LastName: "Barber",
Grades: []Grade{
Grade{
Title: "Quiz 1",
Type: GradeQuiz,
Score: 77,
},
Grade{
Title: "Week 1 Homework",
Type: GradeHomework,
Score: 0,
},
Grade{
Title: "Quiz 2",
Type: GradeQuiz,
Score: 65,
},
},
},
Student{
ID: 4,
FirstName: "Louie",
LastName: "Easton",
Grades: []Grade{
Grade{
Title: "Quiz 1",
Type: GradeQuiz,
Score: 88,
},
Grade{
Title: "Week 1 Homework",
Type: GradeHomework,
Score: 93,
},
Grade{
Title: "Quiz 2",
Type: GradeQuiz,
Score: 84,
},
},
},
Student{
ID: 5,
FirstName: "Kylee",
LastName: "Attwood",
Grades: []Grade{
Grade{
Title: "Quiz 1",
Type: GradeQuiz,
Score: 95,
},
Grade{
Title: "Week 1 Homework",
Type: GradeHomework,
Score: 100,
},
Grade{
Title: "Quiz 2",
Type: GradeQuiz,
Score: 97,
},
},
},
}
}
Grade Business Logic
This is just business logic and useful for examples in GO.
package grades
import (
"fmt"
"sync"
)
type Student struct {
ID int
FirstName string
LastName string
Grades []Grade
}
func (s Student) Average() float32 {
var result float32
for _, grade := range s.Grades {
result += grade.Score
}
return result / float32(len(s.Grades))
}
type Students []Student
var (
students Students
studentsMutex sync.Mutex
)
func (s Students) GetByID(id int) (*Student, error) {
for i := range s {
if s[i].ID == id {
return &s[i], nil
}
}
return nil, fmt.Errorf("Student with ID '%v' not found", id)
}
type GradeType string
const (
GradeTest = GradeType("Test")
GradeHomework = GradeType("Homework")
GradeQuiz = GradeType("Quiz")
)
type Grade struct {
Title string
Type GradeType
Score float32
}
Server (EndPoints)
This has some interest parts
- toJSON which takes and interface and encodes whatever is provider.
- Uses split of the r.URL.Path to determine which was called
- Uses mutex to ensure thread safety
package grades
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
)
func RegisterHandlers() {
handler := new(studentsHandler)
http.Handle("/students", handler)
http.Handle("/students/", handler)
}
type studentsHandler struct{}
func (sh studentsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
pathSegments := strings.Split(r.URL.Path, "/")
switch len(pathSegments) {
case 2: // /students
sh.getAll(w, r)
case 3: // /students/{:id}
id, err := strconv.Atoi(pathSegments[2])
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
sh.getOne(w, r, id)
case 4: // /students/{:id}/grades
id, err := strconv.Atoi(pathSegments[2])
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
if strings.ToLower(pathSegments[3]) != "grades" {
w.WriteHeader(http.StatusNotFound)
return
}
sh.addGrade(w, r, id)
default:
w.WriteHeader(http.StatusNotFound)
}
}
func (sh studentsHandler) getAll(w http.ResponseWriter, r *http.Request) {
studentsMutex.Lock()
defer studentsMutex.Unlock()
data, err := sh.toJSON(students)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Println(err)
return
}
w.Header().Add("content-type", "application/json")
w.Write(data)
}
func (sh studentsHandler) getOne(w http.ResponseWriter, r *http.Request, id int) {
studentsMutex.Lock()
defer studentsMutex.Unlock()
student, err := students.GetByID(id)
if err != nil {
if err != nil {
w.WriteHeader(http.StatusNotFound)
log.Println(err)
return
}
}
data, err := sh.toJSON(student)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Println(fmt.Errorf("Failed to serialize students: %q", err))
return
}
w.Header().Add("content-type", "application/json")
w.Write(data)
}
func (studentsHandler) toJSON(obj interface{}) ([]byte, error) {
var b bytes.Buffer
enc := json.NewEncoder(&b)
err := enc.Encode(obj)
if err != nil {
return nil, fmt.Errorf("Failed to serialize students: %q", err)
}
return b.Bytes(), nil
}
func (sh studentsHandler) addGrade(w http.ResponseWriter, r *http.Request, id int) {
studentsMutex.Lock()
defer studentsMutex.Unlock()
student, err := students.GetByID(id)
if err != nil {
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Println(err)
return
}
}
var g Grade
dec := json.NewDecoder(r.Body)
err = dec.Decode(&g)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
log.Println(err)
return
}
student.Grades = append(student.Grades, g)
w.WriteHeader(http.StatusCreated)
data, err := sh.toJSON(g)
if err != nil {
log.Println(err)
}
w.Header().Add("content-type", "application/json")
w.Write(data)
}
Registry
Registration
This holds the types used for this package, the patch is a struct to hold the changes in an add or delete, the Registration holds data for the registration.
package registry
type Registration struct {
ServiceName ServiceName
ServiceURL string
HeartbeatURL string
ServiceUpdateURL string
RequiredServices []ServiceName
}
type ServiceName string
const (
LogService = ServiceName("LogService")
TeacherPortal = ServiceName("TeacherPortal")
GradingService = ServiceName("GradingService")
)
type patchEntry struct {
Name ServiceName
URL string
}
type patch struct {
Added []patchEntry
Removed []patchEntry
}
Server
This is responsible for
- Adding and Removing registrations
- Notifying services or either addition or removal of registration
- Provides the heartbeat method
The heartbeat function demonstrates the use of a WaitGroup where registrations are iterated over and they are checked. Like the mutex it is essential to have the defer to ensure it is released in the event of an error.
package registry
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"sync"
"time"
)
const ServerPort = ":3000"
const ServicesURL = "http://localhost" + ServerPort + "/services"
type registry struct {
registrations []Registration
mutex *sync.RWMutex
}
func (r *registry) add(reg Registration) error {
r.mutex.Lock()
r.registrations = append(r.registrations, reg)
r.mutex.Unlock()
err := r.sendRequiredServices(reg)
r.notify(patch{
Added: []patchEntry{
patchEntry{Name: reg.ServiceName, URL: reg.ServiceURL},
},
})
return err
}
func (r *registry) remove(url string) error {
for i := range r.registrations {
if r.registrations[i].ServiceURL == url {
r.notify(patch{
Removed: []patchEntry{
patchEntry{Name: r.registrations[i].ServiceName, URL: r.registrations[i].ServiceURL},
},
})
r.mutex.Lock()
r.registrations = append(r.registrations[:i], r.registrations[i+1:]...)
r.mutex.Unlock()
return nil
}
}
return fmt.Errorf("Service at URL %v not found", url)
}
func (r registry) notify(p patch) {
r.mutex.RLock()
defer r.mutex.RUnlock()
for _, reg := range r.registrations {
go func(reg Registration) {
for _, reqService := range reg.RequiredServices {
p := patch{Added: []patchEntry{}, Removed: []patchEntry{}}
sendUpdate := false
for _, added := range p.Added {
if added.Name == reqService {
p.Added = append(p.Added, added)
sendUpdate = true
}
}
for _, removed := range p.Removed {
if removed.Name == reqService {
p.Removed = append(p.Removed, removed)
sendUpdate = true
}
}
if sendUpdate {
err := r.sendPatch(p, reg.ServiceUpdateURL)
if err != nil {
log.Println(err)
return
}
}
}
}(reg)
}
}
func (r registry) sendPatch(p patch, url string) error {
d, err := json.Marshal(p)
if err != nil {
return err
}
_, err = http.Post(url, "application/json", bytes.NewBuffer(d))
if err != nil {
return err
}
return nil
}
func (r registry) sendRequiredServices(reg Registration) error {
r.mutex.RLock()
defer r.mutex.RUnlock()
var p patch
for _, serviceReg := range r.registrations {
for _, reqService := range reg.RequiredServices {
if serviceReg.ServiceName == reqService {
p.Added = append(p.Added, patchEntry{
Name: serviceReg.ServiceName,
URL: serviceReg.ServiceURL,
})
}
}
}
err := r.sendPatch(p, reg.ServiceUpdateURL)
if err != nil {
return err
}
return nil
}
func (r *registry) heartbeat(freq time.Duration) {
for {
var wg sync.WaitGroup
for _, reg := range r.registrations {
wg.Add(1)
go func(reg Registration) {
defer wg.Done()
r.checkService(reg)
}(reg)
}
wg.Wait()
time.Sleep(freq)
}
}
func (r *registry) checkService(reg Registration) {
success := true
for attempts := 0; attempts < 3; attempts++ {
res, err := http.Get(reg.HeartbeatURL)
if err != nil {
log.Println(err)
} else if res.StatusCode == http.StatusOK {
log.Printf("Heartbeat check passed for %v", reg.ServiceName)
if !success {
r.add(reg)
}
break
}
log.Printf("Heartbeat check failed for %v", reg.ServiceName)
if success {
success = false
r.remove(reg.ServiceURL)
}
time.Sleep(3 * time.Second) // wait to try again
}
}
var reg = registry{registrations: make([]Registration, 0),
mutex: new(sync.RWMutex),
}
var once sync.Once
func SetupRegistryService() {
once.Do(func() {
go reg.heartbeat(3 * time.Second)
})
}
type RegistryService struct{}
func (s RegistryService) ServeHTTP(w http.ResponseWriter, req *http.Request) {
log.Println("Request received")
switch req.Method {
case http.MethodPost:
dec := json.NewDecoder(req.Body)
var r Registration
err := dec.Decode(&r)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
log.Printf("Adding service: %v with URL: %v", r.ServiceName, r.ServiceURL)
err = reg.add(r)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
case http.MethodDelete:
payload, err := ioutil.ReadAll(req.Body)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
url := string(payload)
log.Printf("Removing service at URL: %v", url)
err = reg.remove(url)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
Client
This tracks the providers of services.
- Update. Reads the patch which contains the changes and updates the provider list
- GetProvider gets the service by name
- RegisterService Adds a function handler for hearbeat URL Adds a http handler for Service
package registry
import (
"bytes"
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
"net/url"
"sync"
)
// Providers contains the URLs for providers that the service
// requires.
type providers struct {
services map[ServiceName][]string
mutex *sync.RWMutex
}
var prov = providers{
services: make(map[ServiceName][]string),
mutex: new(sync.RWMutex),
}
func (p *providers) Update(pat patch) {
p.mutex.Lock()
defer p.mutex.Unlock()
for _, patchEntry := range pat.Added {
if _, ok := p.services[patchEntry.Name]; !ok {
p.services[patchEntry.Name] = make([]string, 0)
}
p.services[patchEntry.Name] = append(p.services[patchEntry.Name], patchEntry.URL)
}
for _, patchEntry := range pat.Removed {
if providerURLs, ok := p.services[patchEntry.Name]; ok {
for i := range providerURLs {
if providerURLs[i] == patchEntry.URL {
p.services[patchEntry.Name] = append(providerURLs[:i], providerURLs[i+1:]...)
}
}
}
}
}
func (p providers) get(name ServiceName) (string, error) {
providers, ok := p.services[name]
if !ok {
return "", fmt.Errorf("No providers available for service %v", name)
}
idx := int(rand.Float32() * float32(len(providers)))
return providers[idx], nil
}
func GetProvider(name ServiceName) (string, error) {
return prov.get(name)
}
func RegisterService(r Registration) error {
heartbeatURL, err := url.Parse(r.HeartbeatURL)
if err != nil {
return err
}
http.HandleFunc(heartbeatURL.Path, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
serviceUpdateURL, err := url.Parse(r.ServiceUpdateURL)
if err != nil {
return err
}
http.Handle(serviceUpdateURL.Path, &serviceUpdateHandler{})
buf := new(bytes.Buffer)
enc := json.NewEncoder(buf)
err = enc.Encode(r)
if err != nil {
return err
}
res, err := http.Post(ServicesURL, "application/json", buf)
if err != nil {
return err
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("Failed to register service. Registry service responded with code %v", res.StatusCode)
}
return nil
}
func ShutdownService(serviceURL string) error {
req, err := http.NewRequest(http.MethodDelete,
ServicesURL,
bytes.NewBuffer([]byte(serviceURL)))
req.Header.Add("content-type", "text/plain")
if err != nil {
return err
}
res, err := http.DefaultClient.Do(req)
if res.StatusCode != http.StatusOK {
return fmt.Errorf("Failed to deregister service. Registry service responded with code %v", res.StatusCode)
}
return err
}
type serviceUpdateHandler struct{}
func (suh serviceUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
dec := json.NewDecoder(r.Body)
var p patch
err := dec.Decode(&p)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
prov.Update(p)
}
Teacher Portal
This uses something called GO-Templates.
GO Templates
This seems to be a bit like pug, asp or other template but to GO.
Templates
This file defines the files to load.
package teacherportal
import (
"html/template"
)
var rootTemplate *template.Template
func ImportTemplates() error {
var err error
rootTemplate, err = template.ParseFiles(
"teacherportal/students.gohtml",
"teacherportal/student.gohtml",
"teacherportal/grades.gohtml")
if err != nil {
return err
}
return nil
}
Student
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Student</title>
</head>
<body>
<h1>
<a href="/students">Grade Book</a>
- {{.LastName}}, {{.FirstName}}</h1>
{{if gt (len .Grades) 0}}
<table>
<tr>
<th>Title</th>
<th>Type</th>
<th>Score</th>
</tr>
{{range .Grades}}
<tr>
<td>{{.Title}}</td>
<td>{{.Type}}</td>
<td>{{.Score}}</td>
</tr>
{{end}}
</table>
{{else}}
<em>No grades available</em>
{{end}}
<fieldset>
<legend>Add a Grade</legend>
<form action="/students/{{.ID}}/grades" method="POST">
<table>
<tr>
<td>Title</td>
<td>
<input type="text" name="Title">
</td>
</tr>
<tr>
<td>Type</td>
<td>
<select name="Type" id="Type">
<option value="Test">Test</option>
<option value="Quiz">Quiz</option>
<option value="Homework">Homework</option>
</select>
</td>
</tr>
<tr>
<td>Score</td>
<td>
<input type="number" min="0" max="100" step="1" name="Score">
</td>
</tr>
</table>
<button type="submit">Submit</button>
</form>
</fieldset>
</body>
</html>
Students
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Students</title>
</head>
<body>
<h1>Grade Book</h1>
{{if len .}}
<table>
<tr>
<th>Name</th>
<th>Average [%]</th>
</tr>
{{range .}}
<tr>
<td>
<a href="/students/{{.ID}}">{{.LastName}}, {{.FirstName}}</a>
</td>
<td>
{{printf "%.1f%%" .Average}}
</td>
</tr>
{{end}}
</table>
{{else}}
<em>No students found</em>
{{end}}
</body>
</html>
Grades
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grades</title>
</head>
<body>
</body>
</html>
Handlers
This is like EJS templates. Basically the data is retrieved and render is called to show the server-side templates.
package teacherportal
import (
"app/grades"
"app/registry"
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
)
func RegisterHandlers() {
http.Handle("/", http.RedirectHandler("/students", http.StatusPermanentRedirect))
h := new(studentsHandler)
http.Handle("/students", h)
http.Handle("/students/", h)
}
type studentsHandler struct{}
func (sh studentsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
pathSegments := strings.Split(r.URL.Path, "/")
switch len(pathSegments) {
case 2: // /students
sh.renderStudents(w, r)
case 3: // /students/{:id}
id, err := strconv.Atoi(pathSegments[2])
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
sh.renderStudent(w, r, id)
case 4: // /students/{:id}/grades
id, err := strconv.Atoi(pathSegments[2])
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
if strings.ToLower(pathSegments[3]) != "grades" {
w.WriteHeader(http.StatusNotFound)
return
}
sh.renderGrades(w, r, id)
default:
w.WriteHeader(http.StatusNotFound)
}
}
func (studentsHandler) renderStudents(w http.ResponseWriter, r *http.Request) {
var err error
defer func() {
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Println("Error retrieving students: ", err)
}
}()
serviceURL, err := registry.GetProvider(registry.GradingService)
if err != nil {
return
}
res, err := http.Get(serviceURL + "/students")
if err != nil {
return
}
var s grades.Students
err = json.NewDecoder(res.Body).Decode(&s)
if err != nil {
return
}
rootTemplate.Lookup("students.gohtml").Execute(w, s)
}
func (studentsHandler) renderStudent(w http.ResponseWriter, r *http.Request, id int) {
var err error
defer func() {
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Println("Error retrieving students: ", err)
return
}
}()
serviceURL, err := registry.GetProvider(registry.GradingService)
if err != nil {
return
}
res, err := http.Get(fmt.Sprintf("%v/students/%v", serviceURL, id))
if err != nil {
return
}
var s grades.Student
err = json.NewDecoder(res.Body).Decode(&s)
if err != nil {
return
}
rootTemplate.Lookup("student.gohtml").Execute(w, s)
}
func (studentsHandler) renderGrades(w http.ResponseWriter, r *http.Request, id int) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
defer func() {
w.Header().Add("location", fmt.Sprintf("/students/%v", id))
w.WriteHeader(http.StatusTemporaryRedirect)
}()
title := r.FormValue("Title")
gradeType := r.FormValue("Type")
score, err := strconv.ParseFloat(r.FormValue("Score"), 32)
if err != nil {
log.Println("Failed to parse score: ", err)
return
}
g := grades.Grade{
Title: title,
Type: grades.GradeType(gradeType),
Score: float32(score),
}
data, err := json.Marshal(g)
if err != nil {
log.Println("Failed to convert grade to JSON: ", g, err)
}
serviceURL, err := registry.GetProvider(registry.GradingService)
if err != nil {
log.Println("Failed to retrieve instance of Grading Service", err)
return
}
res, err := http.Post(fmt.Sprintf("%v/students/%v/grades", serviceURL, id), "application/json", bytes.NewBuffer(data))
if err != nil {
log.Println("Failed to save grade to Grading Service", err)
return
}
if res.StatusCode != http.StatusCreated {
log.Println("Failed to save grade to Grading Service. Status: ", res.StatusCode)
return
}
}
Service
This package is the API responsible for starting the services.
package service
import (
"app/registry"
"context"
"fmt"
stlog "log"
"net/http"
)
func Start(ctx context.Context, host, port string, reg registry.Registration, registerHandlersFunc func()) (context.Context, error) {
registerHandlersFunc()
ctx = startService(ctx, reg.ServiceName, host, port)
err := registry.RegisterService(reg)
if err != nil {
return ctx, err
}
return ctx, nil
}
func startService(ctx context.Context, serviceName registry.ServiceName, host, port string) context.Context {
ctx, cancel := context.WithCancel(ctx)
var srv http.Server
srv.Addr = ":" + port
go func() {
stlog.Println(srv.ListenAndServe())
cancel()
}()
go func() {
fmt.Printf("%v started. Press any key to stop.\n", serviceName)
var s string
fmt.Scanln(&s)
err := registry.ShutdownService(fmt.Sprintf("http://%v:%v", host, port))
if err != nil {
stlog.Println(err)
}
srv.Shutdown(ctx)
cancel()
}()
return ctx
}
cmds (Where GO puts main)
Here for completeness are the four mains for the four services.
Log Service
package main
import (
"app/log"
"app/registry"
"app/service"
"context"
"fmt"
stlog "log"
)
func main() {
log.Run("./app.log")
host, port := "localhost", "4000"
serviceAddress := fmt.Sprintf("http://%v:%v", host, port)
var r registry.Registration
r.ServiceName = registry.LogService
r.ServiceURL = serviceAddress
r.HeartbeatURL = r.ServiceURL + "/heartbeat"
r.RequiredServices = make([]registry.ServiceName, 0)
r.ServiceUpdateURL = r.ServiceURL + "/services"
ctx, err := service.Start(context.Background(),
host,
port,
r,
log.RegisterHandlers)
if err != nil {
stlog.Fatal(err)
}
<-ctx.Done()
fmt.Println("Shutting down log service")
}
Registry Service
Just creates and instance of the service. I waits for the context to end and report.
package main
import (
"app/grades"
"app/log"
"app/registry"
"app/service"
"context"
"fmt"
stlog "log"
)
func main() {
host, port := "localhost", "6000"
serviceAddress := fmt.Sprintf("http://%v:%v", host, port)
var r registry.Registration
r.ServiceName = registry.GradingService
r.ServiceURL = serviceAddress
r.HeartbeatURL = r.ServiceURL + "/heartbeat"
r.RequiredServices = make([]registry.ServiceName, 0)
r.ServiceUpdateURL = r.ServiceURL + "/services"
ctx, err := service.Start(context.Background(),
host,
port,
r,
grades.RegisterHandlers)
if err != nil {
stlog.Fatal(err)
}
if logProvider, err := registry.GetProvider(registry.LogService); err == nil {
log.SetClientLogger(logProvider, r.ServiceName)
}
<-ctx.Done()
fmt.Println("Shutting down grading service")
}
Teach Portal Service
package main
import (
"app/log"
"app/registry"
"app/service"
"app/teacherportal"
"context"
"fmt"
stlog "log"
)
func main() {
err := teacherportal.ImportTemplates()
if err != nil {
stlog.Fatal(err)
}
host, port := "localhost", "5000"
serviceAddress := fmt.Sprintf("http://%v:%v", host, port)
var r registry.Registration
r.ServiceName = registry.TeacherPortal
r.ServiceURL = serviceAddress
r.HeartbeatURL = r.ServiceURL + "/heartbeat"
r.RequiredServices = []registry.ServiceName{
registry.LogService,
registry.GradingService,
}
r.ServiceUpdateURL = r.ServiceURL + "/services"
ctx, err := service.Start(context.Background(),
host,
port,
r,
teacherportal.RegisterHandlers)
if err != nil {
stlog.Fatal(err)
}
if logProvider, err := registry.GetProvider(registry.LogService); err == nil {
log.SetClientLogger(logProvider, r.ServiceName)
}
<-ctx.Done()
fmt.Println("Shutting down teacher portal")
}
Grading Service
package main
import (
"app/grades"
"app/log"
"app/registry"
"app/service"
"context"
"fmt"
stlog "log"
)
func main() {
host, port := "localhost", "6000"
serviceAddress := fmt.Sprintf("http://%v:%v", host, port)
var r registry.Registration
r.ServiceName = registry.GradingService
r.ServiceURL = serviceAddress
r.HeartbeatURL = r.ServiceURL + "/heartbeat"
r.RequiredServices = make([]registry.ServiceName, 0)
r.ServiceUpdateURL = r.ServiceURL + "/services"
ctx, err := service.Start(context.Background(),
host,
port,
r,
grades.RegisterHandlers)
if err != nil {
stlog.Fatal(err)
}
if logProvider, err := registry.GetProvider(registry.LogService); err == nil {
log.SetClientLogger(logProvider, r.ServiceName)
}
<-ctx.Done()
fmt.Println("Shutting down grading service")
}
Service Registration
- Create Web Service
- Create Register Service
- Register Web Service
- Deregister Web Service
Service Discovery
- Create Grading Service
- Request Required Service On Startup
- Notify when Service Starts
- Notify when Service Shutdown