diff --git a/agt/ballotagent/ballotagent.go b/agt/ballotagent/ballotagent.go new file mode 100644 index 0000000000000000000000000000000000000000..8d23345d407b2c047d1117bcf06eb32a356d192b --- /dev/null +++ b/agt/ballotagent/ballotagent.go @@ -0,0 +1,76 @@ +package ballotagent + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "sync" + "time" + + rad "gitlab.utc.fr/gvandevi/ia04binome2a" // à remplacer par le nom du dossier actuel +) + +type BallotServerAgent struct { + sync.Mutex + id string + reqCount int + addr string + ballots map[rad.Ballot]BallotInfo +} + +func NewBallotServerAgent(addr string) *BallotServerAgent { + return &BallotServerAgent{id: addr, addr: addr} +} + +// Test de la méthode +func (rsa *BallotServerAgent) checkMethod(method string, w http.ResponseWriter, r *http.Request) bool { + if r.Method != method { + w.WriteHeader(http.StatusMethodNotAllowed) + fmt.Fprintf(w, "method %q not allowed", r.Method) + return false + } + return true +} + +func decodeRequest[Req rad.Request](r *http.Request) (req Req, err error) { + buf := new(bytes.Buffer) + buf.ReadFrom(r.Body) + err = json.Unmarshal(buf.Bytes(), &req) + return +} + +func (rsa *BallotServerAgent) doReqcount(w http.ResponseWriter, r *http.Request) { + if !rsa.checkMethod("GET", w, r) { + return + } + + w.WriteHeader(http.StatusOK) + rsa.Lock() + defer rsa.Unlock() + serial, _ := json.Marshal(rsa.reqCount) + w.Write(serial) +} + +func (rsa *BallotServerAgent) Start() { + // création du multiplexer + mux := http.NewServeMux() + mux.HandleFunc("/new_ballot", rsa.createBallot) + mux.HandleFunc("/vote", rsa.doReqcount) + mux.HandleFunc("/result", rsa.doReqcount) + + rsa.ballots = make(map[rad.Ballot]BallotInfo) + + // création du serveur http + s := &http.Server{ + Addr: rsa.addr, + Handler: mux, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20} + + // lancement du serveur + log.Println("Listening on", rsa.addr) + go log.Fatal(s.ListenAndServe()) +} diff --git a/agt/ballotagent/new_ballot.go b/agt/ballotagent/new_ballot.go new file mode 100644 index 0000000000000000000000000000000000000000..e0121b2a7c47f00407789e8298af578fa39b32b5 --- /dev/null +++ b/agt/ballotagent/new_ballot.go @@ -0,0 +1,182 @@ +package ballotagent + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + rad "gitlab.utc.fr/gvandevi/ia04binome2a" + cs "gitlab.utc.fr/gvandevi/ia04binome2a/comsoc" +) + +type BallotInfo struct { + profile cs.Profile + options [][]int + votersId []string + nbAlts int + isOpen bool + results rad.ResultResponse +} + +func (rsa *BallotServerAgent) createBallot(w http.ResponseWriter, r *http.Request) { + // mise à jour du nombre de requêtes + rsa.Lock() + defer rsa.Unlock() + rsa.reqCount++ + + // vérification de la méthode de la requête + if !rsa.checkMethod("POST", w, r) { + return + } + + // décodage de la requête + req, err := decodeRequest[rad.BallotRequest](r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, err.Error()) + return + } + // Check for valid deadline formatting + deadline, errTime := time.Parse(time.RFC3339, req.Deadline) + if errTime != nil { + w.WriteHeader(http.StatusBadRequest) + msg := fmt.Sprintf("'%s' n'utilise pas le bon format, merci d'utiliser RFC3339 ", req.Deadline) + w.Write([]byte(msg)) + return + } + // Check if the deadline is in the future + if deadline.Before(time.Now()) { + w.WriteHeader(http.StatusBadRequest) + msg := fmt.Sprintf("'%s' est déjà passé ", req.Deadline) + w.Write([]byte(msg)) + return + } + + // traitement de la requête + var resp rad.Ballot + + resp.BallotID = fmt.Sprintf("scrutin%d", len(rsa.ballots)+1) + rsa.ballots[resp] = BallotInfo{ + profile: make(cs.Profile, 0), + options: make([][]int, 0), + votersId: req.VotersID, + nbAlts: req.NbAlts, + isOpen: true, + results: rad.ResultResponse{}} + + tb := make([]cs.Alternative, 0) + for _, alt := range req.TieBreak { + tb = append(tb, cs.Alternative(alt)) + } + + switch req.Rule { + case "majority": + go rsa.handleBallot(resp, cs.MajoritySWF, cs.MajoritySCF, tb, deadline) + case "borda": + go rsa.handleBallot(resp, cs.BordaSWF, cs.BordaSCF, tb, deadline) + case "approval": + go rsa.handleBallotWithSingleOption(resp, cs.ApprovalSWF, cs.ApprovalSCF, tb, deadline) + default: + w.WriteHeader(http.StatusNotImplemented) + msg := fmt.Sprintf("Unkonwn rule '%s'", req.Rule) + w.Write([]byte(msg)) + return + } + + w.WriteHeader(http.StatusOK) + serial, _ := json.Marshal(resp) + w.Write(serial) +} + +func (rsa *BallotServerAgent) handleBallot( + ballot rad.Ballot, + swf func(cs.Profile) (cs.Count, error), + scf func(cs.Profile) ([]cs.Alternative, error), + orderedTBAlts []cs.Alternative, + deadline time.Time, +) { + time.Sleep(time.Until(deadline)) + targetBallot := rsa.ballots[ballot] + profile := targetBallot.profile + + // If profile is empty, set winner as 0 and ranking as empty list + if len(profile) == 0 { + targetBallot.results = rad.ResultResponse{Winner: 0, Ranking: make([]int, 0)} + fmt.Println(ballot.BallotID, "n'a pas reçu de votes.") + return + } + + tb := cs.TieBreakFactory(orderedTBAlts) + ballotSWF := cs.SWFFactory(swf, tb) + ballotSCF := cs.SCFFactory(scf, tb) + ranking, _ := ballotSWF(profile) + winner, err := ballotSCF(profile) + if err != nil { + fmt.Println(err) + return + } + intRanking := make([]int, 0) + for _, alt := range ranking { + intRanking = append(intRanking, int(alt)) + } + + rsa.ballots[ballot] = BallotInfo{ + profile: profile, + options: targetBallot.options, + votersId: targetBallot.votersId, + nbAlts: targetBallot.nbAlts, + isOpen: false, + results: rad.ResultResponse{Winner: int(winner), Ranking: intRanking}, + } +} + +func (rsa *BallotServerAgent) handleBallotWithSingleOption( + ballot rad.Ballot, + swf func(cs.Profile, []int) (cs.Count, error), + scf func(cs.Profile, []int) ([]cs.Alternative, error), + orderedTBAlts []cs.Alternative, + deadline time.Time, +) { + time.Sleep(time.Until(deadline)) + targetBallot := rsa.ballots[ballot] + targetBallot.isOpen = false + profile := targetBallot.profile + options := targetBallot.options + + // If profile is empty, set winner as 0 and ranking as empty list + if len(profile) == 0 { + targetBallot.results = rad.ResultResponse{Winner: 0, Ranking: make([]int, 0)} + fmt.Println(ballot.BallotID, "n'a pas reçu de votes.") + return + } + + // Only the fist element of the agents' options is filled/matters + singleOptions := make([]int, len(options)) + for i, option := range options { + singleOptions[i] = option[0] + } + + tb := cs.TieBreakFactory(orderedTBAlts) + ballotSWF := cs.SWFFactoryWithOptions(swf, tb) + ballotSCF := cs.SCFFactoryWithOptions(scf, tb) + ranking, _ := ballotSWF(profile, singleOptions) + winner, err := ballotSCF(profile, singleOptions) + if err != nil { + fmt.Println(err) + return + } + intRanking := make([]int, 0) + for _, alt := range ranking { + intRanking = append(intRanking, int(alt)) + } + + rsa.ballots[ballot] = BallotInfo{ + profile: profile, + options: targetBallot.options, + votersId: targetBallot.votersId, + nbAlts: targetBallot.nbAlts, + isOpen: false, + results: rad.ResultResponse{Winner: int(winner), Ranking: intRanking}, + } +} diff --git a/agt/voteragent/voteragent.go b/agt/voteragent/voteragent.go new file mode 100644 index 0000000000000000000000000000000000000000..f59b497f47bbecf4e4aa782648be3e66e0822c30 --- /dev/null +++ b/agt/voteragent/voteragent.go @@ -0,0 +1,60 @@ +package voteragent + +type RestClientAgent struct { + id string + url string + operator string + arg1 int + arg2 int +} + +func NewRestClientAgent(id string, url string, op string, arg1 int, arg2 int) *RestClientAgent { + return &RestClientAgent{id, url, op, arg1, arg2} +} + +//func (rca *RestClientAgent) treatResponse(r *http.Response) int { +// buf := new(bytes.Buffer) +// buf.ReadFrom(r.Body) +// +// var resp rad.Response +// json.Unmarshal(buf.Bytes(), &resp) +// +// return resp.Result +//} +// +//func (rca *RestClientAgent) doRequest() (res int, err error) { +// req := rad.Request{ +// Operator: rca.operator, +// Args: [2]int{rca.arg1, rca.arg2}, +// } +// +// // sérialisation de la requête +// url := rca.url + "/calculator" +// data, _ := json.Marshal(req) +// +// // envoi de la requête +// resp, err := http.Post(url, "application/json", bytes.NewBuffer(data)) +// +// // traitement de la réponse +// if err != nil { +// return +// } +// if resp.StatusCode != http.StatusOK { +// err = fmt.Errorf("[%d] %s", resp.StatusCode, resp.Status) +// return +// } +// res = rca.treatResponse(resp) +// +// return +//} + +//func (rca *RestClientAgent) Start() { +// log.Printf("démarrage de %s", rca.id) +// res, err := rca.doRequest() +// +// if err != nil { +// log.Fatal(rca.id, "error:", err.Error()) +// } else { +// log.Printf("[%s] %d %s %d = %d\n", rca.id, rca.arg1, rca.operator, rca.arg2, res) +// } +//} diff --git a/cmd/launch-all-rest-agents/launch-all-agents.go b/cmd/launch-all-rest-agents/launch-all-agents.go new file mode 100644 index 0000000000000000000000000000000000000000..776283d249820aa7e0d07710e97dc1037173e1cc --- /dev/null +++ b/cmd/launch-all-rest-agents/launch-all-agents.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "log" + "math/rand" + + ba "gitlab.utc.fr/gvandevi/ia04binome2a/agt/ballotagent" + va "gitlab.utc.fr/gvandevi/ia04binome2a/agt/voteragent" +) + +func main() { + const n = 100 + const url1 = ":8080" + const url2 = "http://localhost:8080" + ops := [...]string{"+", "-", "*"} + + clAgts := make([]va.RestClientAgent, 0, n) + servAgt := ba.NewBallotServerAgent(url1) + + log.Println("démarrage du serveur...") + go servAgt.Start() + + log.Println("démarrage des clients...") + for i := 0; i < n; i++ { + id := fmt.Sprintf("id%02d", i) + op := ops[rand.Intn(3)] + op1 := rand.Intn(100) + op2 := rand.Intn(100) + agt := va.NewRestClientAgent(id, url2, op, op1, op2) + clAgts = append(clAgts, *agt) + } + + for _, agt := range clAgts { + // attention, obligation de passer par cette lambda pour faire capturer la valeur de l'itération par la goroutine + func(agt va.RestClientAgent) { + go agt.Start() + }(agt) + } + + fmt.Scanln() +} diff --git a/cmd/launch-bagt/launch-bagt.go b/cmd/launch-bagt/launch-bagt.go new file mode 100644 index 0000000000000000000000000000000000000000..5c8a75d8b5e238232f3f77da4587d34b293bce2c --- /dev/null +++ b/cmd/launch-bagt/launch-bagt.go @@ -0,0 +1,13 @@ +package main + +import ( + "fmt" + + ba "gitlab.utc.fr/gvandevi/ia04binome2a/agt/ballotagent" +) + +func main() { + server := ba.NewBallotServerAgent(":8080") + server.Start() + fmt.Scanln() +} diff --git a/cmd/launch-vagt/launch-vagt.go b/cmd/launch-vagt/launch-vagt.go new file mode 100644 index 0000000000000000000000000000000000000000..11556351cfedfe33ed9dd61f642129cae0f99d16 --- /dev/null +++ b/cmd/launch-vagt/launch-vagt.go @@ -0,0 +1,13 @@ +package main + +import ( + "fmt" + + va "gitlab.utc.fr/gvandevi/ia04binome2a/agt/voteragent" +) + +func main() { + ag := va.NewRestClientAgent("id1", "http://localhost:8080", "+", 11, 1) + ag.Start() + fmt.Scanln() +} diff --git a/comsoc/TieBreak.go b/comsoc/TieBreak.go index 78f89aa64ddf60c47b88a60a654b52d3a2bf7781..814c3064a995d6ebdf7b860c8ff0afd698d8a2f0 100644 --- a/comsoc/TieBreak.go +++ b/comsoc/TieBreak.go @@ -62,6 +62,47 @@ func SWFFactory(swf func(Profile) (Count, error), tb func([]Alternative) (Altern } } +func SWFFactoryWithOptions( + swf func(Profile, []int) (Count, error), + tb func([]Alternative) (Alternative, error), +) func(Profile, []int) ([]Alternative, error) { + + return func(p Profile, o []int) ([]Alternative, error) { + //récupération du décompte + count, errSWF := swf(p, o) + if errSWF != nil { + return nil, errSWF + } + //préparation de la sortie + var sortedAlts []Alternative + + //PARCOURS DU DECOMPTE + for len(count) > 0 { + //On prend les meilleures alternatives (avant tie break) + bestAlts := maxCount(count) + //On supprime les meilleures alternatives du décompte + for alt := range bestAlts { + delete(count, Alternative(alt)) + } + //Départage + for len(bestAlts) > 0 { + bestAlt, errTB := tb(bestAlts) + if errTB != nil { + return nil, errTB + } + //ajout de la meilleure alternative post-tie break + sortedAlts = append(sortedAlts, bestAlt) + //suppression de l'alternative dans bestAlts + indice := rank(bestAlt, bestAlts) + bestAlts = append(bestAlts[:indice], bestAlts[indice+1:]...) + //suppression de l'alternativ dans le compte + delete(count, Alternative(bestAlt)) + } + } + return sortedAlts, nil + } +} + func Test_sWFFactory() { //Définition de l'Ordre strict orderedAlts := []Alternative{8, 9, 6, 1, 3, 2} @@ -94,6 +135,23 @@ func SCFFactory(scf func(p Profile) ([]Alternative, error), tb func([]Alternativ return bestAlt, errTB } } + +func SCFFactoryWithOptions( + scf func(Profile, []int) ([]Alternative, error), + tb func([]Alternative) (Alternative, error), +) func(Profile, []int) (Alternative, error) { + return func(p Profile, o []int) (Alternative, error) { + //récupération des meilleures alternatives + bestAlts, errSCF := scf(p, o) + if errSCF != nil { + return Alternative(0), errSCF + } + //récupération de la meilleure alternative + bestAlt, errTB := tb(bestAlts) + return bestAlt, errTB + } +} + func Test_sCFFactory() { //Définition de l'Ordre strict orderedAlts := []Alternative{8, 9, 6, 1, 3, 2} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..75cb7767e82625f465c891759035ff649b26f661 --- /dev/null +++ b/readme.md @@ -0,0 +1,11 @@ +# Exemple de serveur Rest en Go + +Un agent permet de faire de petites opérations arithmétiques (`+`, `-`, `*`) sur 2 entiers si on lui demande en REST. + +Techniquement le serveur n'est pas pur REST (à vous de trouver pourquoi), mais il est fonctionnel et peut tenir la charge. + +3 exécutables (indépendants) sont fournis : + +* `launch-all-rest-agents` permet de lancer une démo avec un seul exécutable +* `lanch-rsagt` permet de lancer un agent de type serveur +* `lanch-rsagt` permet de lancer un agent de type client \ No newline at end of file diff --git a/types.go b/types.go new file mode 100644 index 0000000000000000000000000000000000000000..387efff38ec8d42122cbe2a54b86cca02e2fa589 --- /dev/null +++ b/types.go @@ -0,0 +1,29 @@ +package ia04binome2a + +type BallotRequest struct { + Rule string `json:"rule"` + Deadline string `json:"deadline"` + VotersID []string `json:"voters-id"` + NbAlts int `json:"#alts"` + TieBreak []int `json:"tie-break"` +} + +type Vote struct { + AgentID string `json:"agent-id"` + BallotID string `json:"ballot-id"` + Prefs []int `json:"prefs"` + Options []int `json:"options"` +} + +type Ballot struct { + BallotID string `json:"ballot-id"` +} + +type ResultResponse struct { + Winner int `json:"winner"` + Ranking []int `json:"ranking"` +} + +type Request interface { + BallotRequest | Ballot | Vote +}