diff --git a/http/handler.go b/http/handler.go index 03a039766..890a4a31d 100644 --- a/http/handler.go +++ b/http/handler.go @@ -32,6 +32,7 @@ import ( "reflect" "runtime/debug" "runtime/pprof" + "sort" "strconv" "strings" "sync" @@ -220,6 +221,7 @@ func (h *Handler) populateValidators() { h.validators["PostFieldAttrDiff"] = queryValidationSpecRequired() h.validators["GetNodes"] = queryValidationSpecRequired() h.validators["GetShardMax"] = queryValidationSpecRequired() + h.validators["GetTransactionList"] = queryValidationSpecRequired() h.validators["GetTransactions"] = queryValidationSpecRequired() h.validators["GetTransaction"] = queryValidationSpecRequired() h.validators["PostTransaction"] = queryValidationSpecRequired() @@ -362,6 +364,8 @@ func newRouter(handler *Handler) *mux.Router { router.HandleFunc("/schema", handler.handleGetSchema).Methods("GET").Name("GetSchema") router.HandleFunc("/schema", handler.handlePostSchema).Methods("POST").Name("PostSchema") router.HandleFunc("/status", handler.handleGetStatus).Methods("GET").Name("GetStatus") + router.HandleFunc("/transaction", handler.handleGetTransactionList).Methods("GET").Name("GetTransactionList") + router.HandleFunc("/transaction/", handler.handleGetTransactionList).Methods("GET").Name("GetTransactionList") router.HandleFunc("/transaction", handler.handlePostTransaction).Methods("POST").Name("PostTransaction") router.HandleFunc("/transaction/", handler.handlePostTransaction).Methods("POST").Name("PostTransaction") router.HandleFunc("/transaction/{id}", handler.handleGetTransaction).Methods("GET").Name("GetTransaction") @@ -1241,6 +1245,41 @@ func (h *Handler) handleDeleteField(w http.ResponseWriter, r *http.Request) { resp.write(w, err) } +func (h *Handler) handleGetTransactionList(w http.ResponseWriter, r *http.Request) { + if !validHeaderAcceptJSON(r.Header) { + http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable) + return + } + trnsMap, err := h.api.Transactions(r.Context()) + if err != nil { + switch errors.Cause(err) { + case pilosa.ErrNodeNotCoordinator: + http.Error(w, err.Error(), http.StatusBadRequest) + default: + http.Error(w, "problem getting transactions: "+err.Error(), http.StatusInternalServerError) + } + return + } + + // Convert the map of transactions to a slice. + trnsList := make([]*pilosa.Transaction, len(trnsMap)) + var i int + for _, v := range trnsMap { + trnsList[i] = v + i++ + } + + // Sort the slice by createdAt. + sort.Slice(trnsList, func(i, j int) bool { + return trnsList[i].CreatedAt.Before(trnsList[j].CreatedAt) + }) + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(trnsList); err != nil { + h.logger.Printf("encoding GetTransactionList response: %s", err) + } +} + func (h *Handler) handleGetTransactions(w http.ResponseWriter, r *http.Request) { if !validHeaderAcceptJSON(r.Header) { http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable) diff --git a/transaction.go b/transaction.go index 2af2c76df..ca0d5b39a 100644 --- a/transaction.go +++ b/transaction.go @@ -45,6 +45,10 @@ type Transaction struct { // Timeout is the minimum idle time for which this transaction should continue to exist. Timeout time.Duration `json:"timeout"` + // CreatedAt is the timestamp at which the transaction was created. This supports + // the case of listing transactions in a useful order. + CreatedAt time.Time `json:"createdAt"` + // Deadline is calculated from Timeout. TODO reset deadline each time there is activity // on the transaction. (we can't do this until there is some method of associating a // request/call with a transaction) @@ -130,12 +134,14 @@ func (tm *TransactionManager) Start(ctx context.Context, id string, timeout time active := !exclusive || (len(trnsMap) == 0) // set deadline according to timeout - deadline := time.Now().Add(timeout) + createdAt := time.Now() + deadline := createdAt.Add(timeout) trns := &Transaction{ ID: id, Active: active, Exclusive: exclusive, Timeout: timeout, + CreatedAt: createdAt, Deadline: deadline, } if err = tm.store.Put(trns); err != nil { @@ -447,12 +453,14 @@ func (trns *Transaction) MarshalJSON() ([]byte, error) { Active bool `json:"active"` Exclusive bool `json:"exclusive"` Timeout string `json:"timeout"` + CreatedAt string `json:"createdAt"` Deadline string `json:"deadline"` }{ ID: trns.ID, Active: trns.Active, Exclusive: trns.Exclusive, Timeout: trns.Timeout.String(), + CreatedAt: trns.CreatedAt.In(time.UTC).Format(time.RFC3339Nano), Deadline: trns.Deadline.In(time.UTC).Format(time.RFC3339Nano), }) } @@ -463,6 +471,7 @@ func (trns *Transaction) Copy() *Transaction { Active: trns.Active, Exclusive: trns.Exclusive, Timeout: trns.Timeout, + CreatedAt: trns.CreatedAt, Deadline: trns.Deadline, Stats: trns.Stats, } diff --git a/transaction.md b/transaction.md index 4a3bae6d4..5ed78edb5 100644 --- a/transaction.md +++ b/transaction.md @@ -28,7 +28,9 @@ presumably cluster-wide. The base transaction endpoints are `/transactions`, for listing or creating transactions, and `/transaction/[id]`, for listing, creating, finishing, or -cancelling a transaction. +cancelling a transaction. A GET to the `/transaction` endpoint (without an +`[id]`) will return a list of all transactions ordered by their creation +date: `createdAt`. A POST to `/transaction` attempts to create a transaction, assigning it an arbitrary ID that is not the ID of any existing transaction. A `GET` from @@ -214,6 +216,7 @@ This will return a JSON "transaction response" object. "active":true, "exclusive":false, "timeout":"1m0s", + "createdAt":"2020-04-17T21:53:18.69359-05:00", "deadline":"2020-04-17T21:54:18.69359-05:00" }, "error":"some message"