diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework2/hw2 b/homeworks/Frank.Teardrop-i-hate-nicknames/homework2/hw2 new file mode 100755 index 0000000..2138efc Binary files /dev/null and b/homeworks/Frank.Teardrop-i-hate-nicknames/homework2/hw2 differ diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework2/hw2.go b/homeworks/Frank.Teardrop-i-hate-nicknames/homework2/hw2.go new file mode 100644 index 0000000..72b8b55 --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework2/hw2.go @@ -0,0 +1,73 @@ +package main + +import( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "encoding/json" +) + +const NOT_FOUND = "key not found" + +type NameReader interface { + Read(source string, key string) string +} + +type JSONFetcher map[string]string + +func (fetcher JSONFetcher) Read(source string, key string) string { + if val, ok := fetcher.retreive(source, key); ok { + return val + } + data, err := readFromSource(source) + if err != nil { + panic(err) + } + var decoded map[string]interface{} + err = json.Unmarshal(data, &decoded) + if err != nil { + panic(err) + } + value := findByKey(decoded, key) + if (key != NOT_FOUND) { + fetcher.store(source, key, value) + } + return value +} + +func (fetcher JSONFetcher) store(source string, key string, value string) { + fetcher[source+key] = value +} + +func (fetcher JSONFetcher) retreive(source string, key string) (string, bool) { + val, ok := fetcher[source+key] + return val, ok +} + +func readFromSource(source string) ([]byte, error) { + if _, err := url.ParseRequestURI(source); err == nil { + res, err := http.Get(source) + if err != nil { + return nil, err + } + defer res.Body.Close() + return ioutil.ReadAll(res.Body) + } else { + return ioutil.ReadFile(source) + } +} + +func findByKey(decoded map[string]interface{}, key string) string { + val, ok := decoded[key] + if !ok { + return NOT_FOUND + } + return val.(string) +} + +func main() { + fetcher := make(JSONFetcher) + fmt.Println(fetcher.Read("https://jsonplaceholder.typicode.com/posts/1", "body")) + fmt.Println(fetcher.Read("test.json", "key1")) +} \ No newline at end of file diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework2/test.json b/homeworks/Frank.Teardrop-i-hate-nicknames/homework2/test.json new file mode 100644 index 0000000..e71755f --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework2/test.json @@ -0,0 +1,3 @@ +{ + "key1": "value1" +} \ No newline at end of file diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/machine.go b/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/machine.go new file mode 100644 index 0000000..3c13400 --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/machine.go @@ -0,0 +1,76 @@ +package main + +import( + "errors" + "strings" + "strconv" +) + +// used for identifying different machine objects that represent +// the "same" machine in different states +var nextMachineNum = 0 + +type Machine struct { + items []int + num int +} + +func (m *Machine) push(x int) *Machine { + return MakeMachine(append(m.items, x), m.num) +} + +func (m *Machine) peek() (int, error) { + if !m.isEmpty() { + return m.items[len(m.items)-1], nil + } else { + return 0, errors.New("Machine is empty") + } +} + +func (m *Machine) pop() (int, *Machine, error) { + if !m.isEmpty() { + top := m.items[len(m.items)-1] + nextItems := m.items[:len(m.items)-1] + return top, MakeMachine(nextItems, m.num), nil + } else { + return 0, nil, errors.New("Machine is empty") + } +} + +func (m *Machine) isEmpty() bool { + return len(m.items) == 0 +} + +func MakeMachine(items []int, num int) *Machine { + tmp := make([]int, len(items)) + copy(tmp, items) + m := &Machine{tmp, num} + return m +} + +func MakeMachineFromString(s string) *Machine { + itemsStr := strings.Split(s[1:len(s)-1], " ") + items := make([]int, 0) + for i := len(itemsStr)-1; i >= 0; i -= 1 { + item, err := strconv.Atoi(itemsStr[i]) + if err != nil { + panic("Malformed machine specification: " + s) + } + items = append(items, item) + } + nextMachineNum += 1 + return MakeMachine(items, nextMachineNum) +} + +func (m *Machine) String() string { + var sb strings.Builder + sb.WriteString("[") + for i := len(m.items)-1; i >= 0; i -= 1 { + sb.WriteString(strconv.Itoa(m.items[i])) + if (i > 0) { + sb.WriteString(" ") + } + } + sb.WriteString("]") + return sb.String() +} diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/main.go b/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/main.go new file mode 100644 index 0000000..c3393ac --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/main.go @@ -0,0 +1,34 @@ +package main + +import( + "fmt" + "os" +) + +func main() { + if len(os.Args) != 3 { + fmt.Println("Please provide machine state and order as arguments") + fmt.Println("For example: \"[1 2]\" \"{1 2}") + os.Exit(1) + } + run(os.Args[1], os.Args[2]) +} + +func run(state, order string) { + fmt.Printf("Ordering %s from %s\n", order, state) + final, ok := ProcessOrder(MakeOrder(order), MakeState(state)) + if ok { + fmt.Printf("Order completed, resulting state: %s\n", final) + } else { + fmt.Println("This order cannot be processed") + } +} + +func run_examples() { + run("[1]", "{}") + run("[1 2]", "{1 2}") + run("[1 2]", "{1 2 3}") + + run("[1], [1 2], [1 2 3 4], [1 2 3]", "{1 2 3 4}") + run("[2 1 1 1 1 3], [3 1 1 1 1 4 2]", "{1 1 1 1 2 3 4}") +} \ No newline at end of file diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/order.go b/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/order.go new file mode 100644 index 0000000..6fc9ed3 --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/order.go @@ -0,0 +1,57 @@ +package main + +import( + "strings" + "strconv" +) + +type Order []int + +// create an order except one instance of the given item +// if the order has multiple instances, only one will be removed +func (o Order) except(item int) Order { + found := false + result := make([]int, 0) + for _, orderItem := range o { + if !found && item == orderItem { + found = true + continue + } + result = append(result, orderItem) + } + return result +} + +func MakeOrder(s string) Order { + items := make([]int, 0) + numbers := strings.TrimSpace(s[1:len(s)-1]) + itemsStr := strings.Split(numbers, " ") + if len(numbers) == 0 { + return items + } + for _, itemStr := range itemsStr { + item, err := strconv.Atoi(itemStr) + if err != nil { + panic("Malformed order specification: " + s) + } + items = append(items, item) + } + return items +} + +func (o Order) String() string { + var sb strings.Builder + sb.WriteString("{") + for idx, item := range o { + sb.WriteString(strconv.Itoa(item)) + if (idx < len(o)-1) { + sb.WriteString(" ") + } + } + sb.WriteString("}") + return sb.String() +} + +func (o Order) Equals(other Order) bool { + return strings.Compare(o.String(), other.String()) == 0 +} \ No newline at end of file diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/order_test.go b/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/order_test.go new file mode 100644 index 0000000..46697bc --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/order_test.go @@ -0,0 +1,36 @@ +package main + +import( + "testing" + "fmt" +) + +func TestExceptRemoves(t *testing.T) { + order := MakeOrder("{1 2 3}") + except := order.except(1) + expected := MakeOrder("{2 3}") + if !expected.Equals(except) { + complain(t, "Did not remove correctly", except, expected) + } +} + +func TestExceptNonExistent(t *testing.T) { + order := MakeOrder("{1 2 3}") + except := order.except(5) + if !except.Equals(order) { + complain(t, "Failed when removing non-existent item", except, order) + } +} + +func TestExceptEmpty(t *testing.T) { + order := MakeOrder("{}") + except := order.except(1) + if !except.Equals(order) { + complain(t, "Failed to remove from empty order", except, order) + t.Errorf("Failed to remove from empty order") + } +} + +func complain(t *testing.T, msg string, expected, result Order) { + t.Errorf(fmt.Sprintf("Error: %s, expected: %s, result: %s", msg, expected, result)) +} diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/vending.go b/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/vending.go new file mode 100644 index 0000000..e3e042b --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/vending.go @@ -0,0 +1,90 @@ +package main + +import( + "strings" +) + +type State []*Machine + +// Process given order in a given state of vending machines +// if order can be satisfied by this state, return state of the machines +// after processing order and true +// If the order cannot be satisfied return nil and false +// An empty order can be satisfied trivially: by doing nothing and +// keeping current state +func ProcessOrder(order Order, state State) (State, bool) { + if len(order) == 0 { + return state, true + } + // take only first element if items in order should be taken + // sequentially + for _, item := range order { + nextStates := processItem(item, state) + for _, nextState := range nextStates { + result, isSolution := ProcessOrder(order.except(item), nextState) + if isSolution { + return result, true + } + } + } + + return nil, false +} + +// Process a single order item in given state of vending machines +// Since there can be multiple machines that have this item, return +// states for all possible item retrievals +func processItem(item int, state State) []State { + results := make([]State, 0) + updatedMachines := takeItem(item, state) + for _, updated := range updatedMachines { + results = append(results, replaceMachine(updated, state)) + } + return results +} + +// Try taking an item from every machine in the state. +// Return every machine that got removed item from the top +// type []*Machine signifies that this is not a state but a collection +// of machines, even though it's the same type +func takeItem(item int, state State) []*Machine { + result := make([]*Machine, 0) + for _, machine := range state { + if top, err := machine.peek(); err == nil && top == item { + _, updated, _ := machine.pop() + result = append(result, updated) + } + } + return result +} + +// Find machine in the state with the same number as m +// Return new state with machine in the state replaced +func replaceMachine(replacement *Machine, state State) State { + result := make(State, 0) + for _, machine := range state { + if replacement.num == machine.num { + result = append(result, replacement) + } else { + result = append(result, machine) + } + } + return result +} + +func MakeState(s string) State { + machines := make(State, 0) + strs := strings.Split(s, ", ") + for _, machineStr := range strs { + machines = append(machines, MakeMachineFromString(machineStr)) + } + return machines +} + +func (state State) String() string { + machineStrs := make([]string, 0) + for _, machine := range state { + machineStrs = append(machineStrs, machine.String()) + } + return strings.Join(machineStrs, ", ") +} \ No newline at end of file diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/vending_test.go b/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/vending_test.go new file mode 100644 index 0000000..0dcc2f1 --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework3/vending_test.go @@ -0,0 +1,36 @@ +package main + +import( + "testing" +) + +func TestProcessItemSingleMachine(t *testing.T) { + m1 := MakeMachineFromString("[1]") + state := []*Machine{m1} + // order := MakeOrder("{1}") + nextStates := processItem(1, state) + if len(nextStates) != 1 { + t.Errorf("Incorrect size of resulting states: %d", len(nextStates)) + } else { + updatedM1 := nextStates[0][0] + if !updatedM1.isEmpty() { + t.Errorf("machine has not been updated") + } + } +} + +func TestProcessItemSingleMachineTwoElements(t *testing.T) { + m1 := MakeMachineFromString("[1 2]") + state := []*Machine{m1} + // order := MakeOrder("{1}") + nextStates := processItem(1, state) + if len(nextStates) != 1 { + t.Errorf("Incorrect size of resulting states: %d", len(nextStates)) + } else { + updatedM1 := nextStates[0][0] + top, _ := updatedM1.peek() + if top != 2 { + t.Errorf("Incorrect top element") + } + } +} diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/.gitignore b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/.gitignore new file mode 100644 index 0000000..9130019 --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/.gitignore @@ -0,0 +1,2 @@ +task_descr +test.db diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/db.go b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/db.go new file mode 100644 index 0000000..998da40 --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/db.go @@ -0,0 +1,148 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/sqlite" +) + +type MachineRecord struct { + ID uint `gorm:"primary_key"` + Items string +} + +type OrderRecord struct { + ID uint `gorm:"primary_key"` + Items string + FetchedItems string + Status string +} + +func DbConnect() *gorm.DB { + db, err := gorm.Open("sqlite3", "test.db") + if err != nil { + fmt.Println(err.Error()) + panic("failed to connect database") + } + return db +} + +func LoadMachines(db *gorm.DB) []*Machine { + var records []MachineRecord + db.Find(&records) + machines := make([]*Machine, 0) + for _, record := range records { + machines = append(machines, recordToMachine(&record)) + } + return machines +} + +func LoadOrders(db *gorm.DB) []*Order { + var records []OrderRecord + db.Find(&records) + orders := make([]*Order, 0) + for _, record := range records { + orders = append(orders, recordToOrder(&record)) + } + return orders +} + +// SaveMachine saves given machine to db. If order.ID is 0, +// this will create a new order. Return orderID +func SaveMachine(db *gorm.DB, m *Machine) uint { + record := machineToRecord(m) + db.Save(record) + return record.ID +} + +// SaveOrder saves given order to db. If order.ID is 0, +// this will create a new order. Return orderID +func SaveOrder(db *gorm.DB, o *Order) uint { + record := orderToRecord(o) + db.Save(record) + return record.ID +} + +// UpdateAtomically updates both order and machine within a single transaction +// return non-nil error in case transaction failed +func UpdateAtomically(db *gorm.DB, o *Order, m *Machine) error { + return db.Transaction(func(tx *gorm.DB) error { + orderRecord := orderToRecord(o) + if err := tx.Save(orderRecord).Error; err != nil { + return err + } + machineRecord := machineToRecord(m) + if err := tx.Save(machineRecord).Error; err != nil { + return err + } + return nil + }) +} + +func Migrate(db *gorm.DB) { + db.AutoMigrate(&MachineRecord{}, &OrderRecord{}) +} + +func machineToRecord(m *Machine) *MachineRecord { + items, err := json.Marshal(m.GetAllItems()) + if err != nil { + log.Println("Malformed machine items: %q", items) + return nil + } + return &MachineRecord{ + ID: m.ID, + Items: string(items), + } +} + +func recordToMachine(mr *MachineRecord) *Machine { + var items []int + err := json.Unmarshal([]byte(mr.Items), &items) + if err != nil { + log.Println("Malformed data in the database: " + mr.Items) + return nil + } + return MakeMachine(mr.ID, items) +} + +func orderToRecord(o *Order) *OrderRecord { + items, err := json.Marshal(o.items) + if err != nil { + log.Println("Malformed order: %q", o.items) + return nil + } + fetchedItems, err := json.Marshal(o.fetchedItems) + if err != nil { + log.Println("Malformed order: %q", o.items) + return nil + } + return &OrderRecord{ + ID: o.ID, + Items: string(items), + FetchedItems: string(fetchedItems), + Status: o.status, + } +} + +func recordToOrder(or *OrderRecord) *Order { + var items, fetchedItems []int + err := json.Unmarshal([]byte(or.FetchedItems), &fetchedItems) + if err != nil { + log.Println("Malformed data in the database: " + or.FetchedItems) + return nil + } + err = json.Unmarshal([]byte(or.Items), &items) + if err != nil { + log.Println("Malformed data in the database: " + or.Items) + return nil + } + return &Order{ + ID: or.ID, + items: items, + fetchedItems: fetchedItems, + status: or.Status, + } +} diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/docker-compose.yml b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/docker-compose.yml new file mode 100644 index 0000000..2fb81a0 --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3.1' + +services: + db: + image: postgres + container_name: ma_postgres + restart: always + environment: + POSTGRES_PASSWORD: pass + POSTGRES_DB: ma_stock + ports: + - 8035:5432 + + adminer: + image: adminer + restart: always + ports: + - 8080:8080 \ No newline at end of file diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/go.mod b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/go.mod new file mode 100644 index 0000000..f203547 --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/go.mod @@ -0,0 +1,5 @@ +module github.com/i-hate-nicknames/ma_queued_stock + +go 1.13 + +require github.com/jinzhu/gorm v1.9.12 diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/go.sum b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/go.sum new file mode 100644 index 0000000..170bcad --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/go.sum @@ -0,0 +1,22 @@ +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= +github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw= +github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/handlers.go b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/handlers.go new file mode 100644 index 0000000..785efdd --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/handlers.go @@ -0,0 +1,71 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type NewOrderRequest struct { + Items []int +} + +type OrderRequest struct { + OrderId uint +} + +func submitOrderHandler(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + var reqData NewOrderRequest + err := decoder.Decode(&reqData) + if err != nil { + panic(err) + } + orderId := store.SubmitOrder(reqData.Items) + status, err := store.ResolveOrder(orderId) + if err != nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("Failed to create an order")) + } + w.WriteHeader(http.StatusOK) + w.Write([]byte(fmt.Sprintf("Order id: %d\nOrder status: %s", orderId, status))) +} + +func getOrderStatusHandler(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + var reqData OrderRequest + err := decoder.Decode(&reqData) + if err != nil { + panic(err) + } + var response []byte + order, ok := store.GetOrder(reqData.OrderId) + if !ok { + response = []byte("Order not found!") + } else { + response = []byte(order.status) + } + w.WriteHeader(http.StatusOK) + w.Write([]byte(fmt.Sprintf("%q", response))) +} + +func cancelOrderHandler(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + var reqData OrderRequest + err := decoder.Decode(&reqData) + if err != nil { + panic(err) + } + var response []byte + order, ok := store.GetOrder(reqData.OrderId) + if !ok { + response = []byte("Order not found!") + } else if order.status == STATUS_COMPLETED { + response = []byte("Order has beel already completed") + } else { + store.CancelOrder(reqData.OrderId) + response = []byte(order.status) + } + w.WriteHeader(http.StatusOK) + w.Write([]byte(fmt.Sprintf("%q", response))) +} diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/machine.go b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/machine.go new file mode 100644 index 0000000..3448ac8 --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/machine.go @@ -0,0 +1,128 @@ +package main + +import ( + "sync" +) + +type Machine struct { + ID uint + in, out []int + mux sync.Mutex +} + +func MakeMachine(id uint, items []int) *Machine { + in := make([]int, 0) + out := make([]int, len(items)) + copy(out, items) + return &Machine{in: in, out: out, ID: id} +} + +func (m *Machine) PutAll(items []int) { + m.mux.Lock() + defer m.mux.Unlock() + for _, item := range items { + m.put(item) + } +} + +func (m *Machine) put(item int) { + m.in = append(m.in, item) +} + +// TakeAll tries to take as many items as possible from this machine, +// return two slices (taken, remains), the first one contains all +// the item taken from this machine, the second one all the items +// in toTake that can't be taken from this machine +func (m *Machine) TakeAll(orderItems []int) ([]int, []int) { + noneTaken := false + toTake := make(map[int]int, 0) + for _, orderItem := range orderItems { + toTake[orderItem]++ + } + + for !noneTaken { + noneTaken = true + topItem, ok := m.peek() + if !ok { + break + } + + if qty, ok := toTake[topItem]; ok && qty > 0 { + m.take() + toTake[topItem]-- + noneTaken = false + } + } + + taken := make([]int, 0) + remains := make([]int, 0) + for _, orderItem := range orderItems { + if qty, ok := toTake[orderItem]; ok && qty > 0 { + toTake[orderItem]-- + remains = append(remains, orderItem) + } else { + taken = append(taken, orderItem) + } + } + return taken, remains +} + +// RestoreFrom takes the same machine's copy, and resets its items +// state to that copy. +// Do nothing if given machine is not a copy (checked by machine id) +func (m *Machine) RestoreFrom(other *Machine) { + if other.ID != m.ID { + return + } + m.in = other.in + m.out = other.out +} + +func (m *Machine) Copy() *Machine { + cp := &Machine{ID: m.ID} + copy(cp.in, m.in) + copy(cp.out, m.out) + return cp +} + +// GetAllItems gets all items in this machine in the queue order +func (m *Machine) GetAllItems() []int { + result := make([]int, 0) + for i := len(m.out) - 1; i >= 0; i-- { + result = append(result, m.out[i]) + } + for i := 0; i < len(m.in); i++ { + result = append(result, m.in[i]) + } + return result +} + +func (m *Machine) take() (int, bool) { + if len(m.out) > 0 { + topItem := m.out[len(m.out)-1] + m.out = m.out[:len(m.out)-1] + return topItem, true + } else if len(m.in) > 0 { + // put everything in m.in into m.out in reversed order + // except for the item to take + m.out = make([]int, len(m.in)) + for i := len(m.in) - 1; i > 0; i-- { + m.out = append(m.out, m.in[i]) + } + item := m.in[0] + m.in = make([]int, 0) + return item, true + } else { + return 0, false + } +} + +func (m *Machine) peek() (int, bool) { + if len(m.out) > 0 { + return m.out[len(m.out)-1], true + } else if len(m.in) > 0 { + return m.in[0], true + } else { + return 0, false + } +} diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/requests/test-order.sh b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/requests/test-order.sh new file mode 100644 index 0000000..1fee324 --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/requests/test-order.sh @@ -0,0 +1,9 @@ +#!/bin/sh +printf "Creating order [1, 2, 3]\n" +http POST :8001/submit items:="[1, 2, 3]" + +printf "Getting status\n" +http POST :8001/getStatus orderId:="1" + +printf "Cancelling\n" +http POST :8001/cancel orderId:="1" \ No newline at end of file diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/service.go b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/service.go new file mode 100644 index 0000000..35a18d2 --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/service.go @@ -0,0 +1,24 @@ +package main + +import ( + "log" + "net/http" +) + +// todo: pass store around as an argument to handlers instead of a global variable +var store *Store + +func main() { + db := DbConnect() + Migrate(db) + store = LoadStore(db) + // todo: implement reading machine settings from command line + // or better add methods to create/update machines at runtime + if len(store.machines) == 0 { + store.generateMachines() + } + http.HandleFunc("/submit", submitOrderHandler) + http.HandleFunc("/getStatus", getOrderStatusHandler) + http.HandleFunc("/cancel", cancelOrderHandler) + log.Fatal(http.ListenAndServe("localhost:8001", nil)) +} diff --git a/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/store.go b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/store.go new file mode 100644 index 0000000..b338e2b --- /dev/null +++ b/homeworks/Frank.Teardrop-i-hate-nicknames/homework4/store.go @@ -0,0 +1,177 @@ +package main + +import ( + "errors" + "log" + "sync" + + "github.com/jinzhu/gorm" +) + +const ( + STATUS_PENDING = "pending" + STATUS_CANCELLED = "cancelled" + STATUS_COMPLETED = "completed" +) + +type Store struct { + machines []*Machine + orders map[uint]*Order + mux sync.Mutex // todo: switch to RW mutext for better performance of read operations + db *gorm.DB +} + +// LoadStore loads state of the store from the database +func LoadStore(db *gorm.DB) *Store { + dbOrders := LoadOrders(db) + machines := LoadMachines(db) + orders := make(map[uint]*Order, 0) + for _, dbOrder := range dbOrders { + orders[dbOrder.ID] = dbOrder + } + return &Store{machines: machines, orders: orders, db: db} +} + +func (s *Store) generateMachines() { + ms := make([]*Machine, 0) + ms = append(ms, MakeMachine(10, []int{5, 4, 3, 2, 1})) + ms = append(ms, MakeMachine(15, []int{44, 32, 12})) + ms = append(ms, MakeMachine(25, []int{1, 2, 3})) + for _, m := range ms { + SaveMachine(s.db, m) + } + s.machines = ms +} + +type Order struct { + ID uint + items []int + fetchedItems []int + status string + mux sync.Mutex +} + +func MakeOrder(items []int) *Order { + fetched := make([]int, 0) + return &Order{items: items, status: STATUS_PENDING, fetchedItems: fetched} +} + +func (o *Order) Copy() *Order { + cp := &Order{ID: o.ID, status: o.status} + copy(cp.items, o.items) + copy(cp.fetchedItems, cp.fetchedItems) + return cp +} + +func (o *Order) RestoreFrom(other *Order) { + o.items = other.items + o.fetchedItems = other.fetchedItems + o.status = other.status +} + +func (s *Store) SubmitOrder(items []int) uint { + s.mux.Lock() + defer s.mux.Unlock() + order := MakeOrder(items) + id := SaveOrder(s.db, order) + log.Println("Created order with id", id) + s.orders[id] = order + return id +} + +func (s *Store) ResolveOrder(orderId uint) (string, error) { + order, ok := s.GetOrder(orderId) + if !ok { + return "", errors.New("Order not found!") + } + order.mux.Lock() + defer order.mux.Unlock() + // this assumes s.machines will never be updated simultaneously with this method + for _, machine := range s.machines { + // lock machine, try to take as many items as possible + // if taken any, start db transaction, save both updated order and machine + // within a transaction. If that fails, rollback the states of order and machine + machine.mux.Lock() + err := ExecOrRestore(order, machine, func() error { + var err error + taken, remains := machine.TakeAll(order.items) + order.items = remains + for _, it := range taken { + order.fetchedItems = append(order.fetchedItems, it) + } + if len(taken) > 0 { + if len(order.items) == 0 { + order.status = STATUS_COMPLETED + } else { + order.status = STATUS_PENDING + } + err = UpdateAtomically(s.db, order, machine) + } + return err + }) + if err != nil { + log.Println("Error when taking items from machine", err) + } + machine.mux.Unlock() + if len(order.items) == 0 { + break + } + } + // todo: try to resolve all other orders if we changed state of at least one machine + // todo: later we can use some scheduler structure with a separate routine, and schedule + // order retries via it. + // Probably can add some checks that will only schedule orders that can take something from + // the updated state + return order.status, nil +} + +func (s *Store) GetOrder(orderId uint) (*Order, bool) { + s.mux.Lock() + defer s.mux.Unlock() + val, ok := s.orders[orderId] + return val, ok +} + +func (s *Store) CancelOrder(orderId uint) error { + order, ok := s.GetOrder(orderId) + if !ok { + return errors.New("Order not found") + } + s.mux.Lock() + if len(s.machines) == 0 { + return errors.New("no machines to put cancelled order items") + } + s.mux.Unlock() + order.mux.Lock() + defer order.mux.Unlock() + // this assumes s.machines will never be updated simultaneously with this method + machine := s.machines[0] + machine.mux.Lock() + err := ExecOrRestore(order, machine, func() error { + machine.PutAll(order.fetchedItems) + order.fetchedItems = []int{} + order.status = STATUS_CANCELLED + if err := UpdateAtomically(s.db, order, machine); err != nil { + return err + } + return nil + }) + machine.mux.Unlock() + + return err +} + +// ExecOrRestore copies order and machine data, executes f. If f returns an error, +// the state of order and machine are rolled back +func ExecOrRestore(o *Order, m *Machine, f func() error) error { + // todo: maybe consider splitting this into two methods SafeExec for + // order and machine and just chain them together here tbh + orderCopy := o.Copy() + machineCopy := m.Copy() + if err := f(); err != nil { + o.RestoreFrom(orderCopy) + m.RestoreFrom(machineCopy) + return err + } + return nil +}