diff --git a/http/client.go b/http/client.go index e3bee63c6..b18e5a382 100644 --- a/http/client.go +++ b/http/client.go @@ -1247,7 +1247,10 @@ func (c *InternalClient) Transactions(ctx context.Context) (map[string]pilosa.Tr if err != nil { return trnsMap, errors.Wrap(err, "executing request") } - defer resp.Body.Close() + defer func() { + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + }() tmpTrnsMap := make(map[string]*pilosa.Transaction) err = json.NewDecoder(resp.Body).Decode(&tmpTrnsMap) @@ -1288,7 +1291,10 @@ func (c *InternalClient) StartTransaction(ctx context.Context, id string, timeou if err != nil { return pilosa.Transaction{}, errors.Wrap(err, "executing request") } - defer resp.Body.Close() + defer func() { + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + }() err = json.NewDecoder(resp.Body).Decode(&tr) if err != nil { return pilosa.Transaction{}, errors.Wrap(err, "decoding response") @@ -1318,7 +1324,10 @@ func (c *InternalClient) FinishTransaction(ctx context.Context, id string) (pilo if err != nil { return pilosa.Transaction{}, errors.Wrap(err, "executing request") } - defer resp.Body.Close() + defer func() { + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + }() tr := &TransactionResponse{Transaction: &pilosa.Transaction{}} err = json.NewDecoder(resp.Body).Decode(&tr) if err != nil { @@ -1351,7 +1360,10 @@ func (c *InternalClient) GetTransaction(ctx context.Context, id string) (pilosa. if err != nil { return pilosa.Transaction{}, errors.Wrap(err, "executing request") } - defer resp.Body.Close() + defer func() { + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + }() tr := &TransactionResponse{Transaction: &pilosa.Transaction{}} err = json.NewDecoder(resp.Body).Decode(&tr) if err != nil { diff --git a/http/client_test.go b/http/client_test.go index eb76e43ce..87d820150 100644 --- a/http/client_test.go +++ b/http/client_test.go @@ -1386,6 +1386,20 @@ func TestClientTransactions(t *testing.T) { pilosa.Transaction{}, trns) } + + // start transaction with blank id + if trns, err := client0.StartTransaction(context.Background(), "", time.Minute, false); err != nil { + t.Fatalf("error starting transaction: %v", err) + } else { + expDeadline = time.Now().Add(time.Minute) + if len(trns.ID) != 36 { + t.Errorf("expected generated UUID, but got '%s'", trns.ID) + } + test.CompareTransactions(t, + pilosa.Transaction{ID: trns.ID, Timeout: time.Minute, Active: true, Deadline: expDeadline}, + trns) + } + } // Client represents a test wrapper for pilosa.Client. diff --git a/http/handler.go b/http/handler.go index b5e307bf0..de17bcea9 100644 --- a/http/handler.go +++ b/http/handler.go @@ -343,6 +343,8 @@ func newRouter(handler *Handler) *mux.Router { router.HandleFunc("/version", handler.handleGetVersion).Methods("GET").Name("GetVersion") router.HandleFunc("/transactions", handler.handleGetTransactions).Methods("GET").Name("GetTransactions") router.HandleFunc("/transaction/{id}", handler.handleGetTransaction).Methods("GET").Name("GetTransaction") + router.HandleFunc("/transaction/", handler.handlePostTransaction).Methods("POST").Name("PostTransaction") + router.HandleFunc("/transaction", handler.handlePostTransaction).Methods("POST").Name("PostTransaction") router.HandleFunc("/transaction/{id}", handler.handlePostTransaction).Methods("POST").Name("PostTransaction") router.HandleFunc("/transaction/{id}/finish", handler.handlePostFinishTransaction).Methods("POST").Name("PostFinishTransaction") @@ -1124,7 +1126,10 @@ func (h *Handler) handlePostTransaction(w http.ResponseWriter, r *http.Request) return } - id := mux.Vars(r)["id"] + id, ok := mux.Vars(r)["id"] + if !ok { + id = reqTrns.ID + } trns, err := h.api.StartTransaction(id, reqTrns.Timeout, reqTrns.Exclusive, false) h.doTransactionResponse(w, err, trns) diff --git a/transaction.md b/transaction.md index 109dbfd9d..69ed09fed 100644 --- a/transaction.md +++ b/transaction.md @@ -159,11 +159,13 @@ goes through API (and is passed directly to Server). (unimplemented) #### TODO -- [x] implement api layer and cluster logic, startup, etc. +- [x] implement api layer and cluster logic, startup, etc. +- [ ] add new cluster state to explicitly reject certain requests during exclusive transaction? - [ ] implement HTTP layer including header/transaction ID - [ ] implement and use persistent transaction store rather than inmem. - [ ] update go-pilosa/gpexp to actually USE transactions - [ ] update IDK to use updated go-pilosa +- [ ] external testing with e.g. curl - ID validation. No slashes, no non-URL safe chars @@ -172,3 +174,70 @@ goes through API (and is passed directly to Server). (unimplemented) - if the Exclusive transaction is active, there should be no other transactions +### Documentation + +Before performing a backup, you must request an exclusive "transaction" with the cluster. Do this via and HTTP POST to the coordinator node at path: + +`/transaction` OR `/transaction/{id}` if you wish to specify a custom ID (any alphanum+dash). Otherwise a UUID will be generated and returned in the response. + +Use headers: + +``` +Accept: application/json +Content-Type: application/json +``` + +And body like: + +``` +{ + "timeout": "10m", + "exclusive": true +} +``` + +You may choose any timeout you like, though it's better to err on the +longer side of how long you expect the backup to take. You explicitly +finish the transaction once you're done, so the timeout exists solely +for cleanup in the case of failures. + +This will return a JSON "transaction response" object. +``` +{ +"transaction": { + "id":"5e572d95-4204-40cd-804c-92976b68dc9b", + "active":true, + "exclusive":false, + "timeout":"1m0s", + "deadline":"2020-04-17T21:54:18.69359-05:00" + }, +"error":"some message" +} +``` + +The `error` field MAY not be present if there is no error. + +You MUST check whether `active` is true. If not, you must poll the transaction endpoint with a GET request and your ID until it is true. This looks like: + +GET `/transaction/5e572d95-4204-40cd-804c-92976b68dc9b` + +with headers: + +``` +Accept: application/json +``` + +and also returns a "transaction response" object. + +Once an "active", "exclusive" transaction is returned, proceed with your backup. + +Once the backup is complete, finish the transaction with + +POST `/transaction/{id}/finish` + +with headers: + +``` +Accept: application/json +``` +