gorqlite/api.go

204 lines
6.8 KiB
Go

package gorqlite
/*
this file has low level stuff:
rqliteApiGet()
rqliteApiPost()
There is some code duplication between those and they should
probably be combined into one function.
nothing public here.
*/
import "bytes"
import "encoding/json"
import "errors"
import "fmt"
import "io/ioutil"
import "net/http"
import "time"
/* *****************************************************************
method: rqliteApiGet() - for api_STATUS
- lowest level interface - does not do any JSON unmarshaling
- handles retries
- handles timeouts
* *****************************************************************/
func (conn *Connection) rqliteApiGet(apiOp apiOperation) ([]byte, error) {
var responseBody []byte
trace("%s: rqliteApiGet() called", conn.ID)
// only api_STATUS now - maybe someday BACKUP
if apiOp != api_STATUS {
return responseBody, errors.New("rqliteApiGet() called for invalid api operation")
}
// just to be safe, check this
peersToTry := conn.cluster.makePeerList()
if len(peersToTry) < 1 {
return responseBody, errors.New("I don't have any cluster info")
}
trace("%s: I have a peer list %d peers long", conn.ID, len(peersToTry))
// failure log is used so that if all peers fail, we can say something
// about why each failed
failureLog := make([]string, 0)
PeerLoop:
for peerNum, peerToTry := range peersToTry {
trace("%s: attemping to contact peer %d", conn.ID, peerNum)
// docs say default GET policy is up to 10 follows automatically
url := conn.assembleURL(api_STATUS, peerToTry)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
trace("%s: got error '%s' doing http.NewRequest", conn.ID, err.Error())
failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error()))
continue PeerLoop
}
trace("%s: http.NewRequest() OK")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
client.Timeout = time.Duration(conn.timeout) * time.Second
response, err := client.Do(req)
if err != nil {
trace("%s: got error '%s' doing client.Do", conn.ID, err.Error())
failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error()))
continue PeerLoop
}
defer response.Body.Close()
trace("%s: client.Do() OK")
responseBody, err := ioutil.ReadAll(response.Body)
if err != nil {
trace("%s: got error '%s' doing ioutil.ReadAll", conn.ID, err.Error())
failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error()))
continue PeerLoop
}
trace("%s: ioutil.ReadAll() OK")
if response.Status != "200 OK" {
trace("%s: got code %s", conn.ID, response.Status)
failureLog = append(failureLog, fmt.Sprintf("%s failed, got: %s", url, response.Status))
continue PeerLoop
}
// if we got here, we succeeded
trace("%s: api call OK, returning", conn.ID)
return responseBody, nil
}
// if we got here, all peers failed. Let's build a verbose error message
var stringBuffer bytes.Buffer
stringBuffer.WriteString("tried all peers unsuccessfully. here are the results:\n")
for n, v := range failureLog {
stringBuffer.WriteString(fmt.Sprintf(" peer #%d: %s\n", n, v))
}
return responseBody, errors.New(stringBuffer.String())
}
/* *****************************************************************
method: rqliteApiPost() - for api_QUERY and api_WRITE
- lowest level interface - does not do any JSON unmarshaling
- handles 301s, etc.
- handles retries
- handles timeouts
it is called with an apiOperation type because the URL it will use varies
depending on the API operation type (api_QUERY vs. api_WRITE)
* *****************************************************************/
func (conn *Connection) rqliteApiPost(apiOp apiOperation, sqlStatements []string) ([]byte, error) {
var responseBody []byte
switch apiOp {
case api_QUERY:
trace("%s: rqliteApiGet() post called for a QUERY of %d statements", conn.ID, len(sqlStatements))
case api_WRITE:
trace("%s: rqliteApiGet() post called for a QUERY of %d statements", conn.ID, len(sqlStatements))
default:
return responseBody, errors.New("weird! called for an invalid apiOperation in rqliteApiPost()")
}
// jsonify the statements. not really needed in the
// case of api_STATUS but doesn't hurt
jStatements, err := json.Marshal(sqlStatements)
if err != nil {
return nil, err
}
// just to be safe, check this
peersToTry := conn.cluster.makePeerList()
if len(peersToTry) < 1 {
return responseBody, errors.New("I don't have any cluster info")
}
// failure log is used so that if all peers fail, we can say something
// about why each failed
failureLog := make([]string, 0)
PeerLoop:
for peerNum, peer := range peersToTry {
trace("%s: trying peer #%d", conn.ID, peerNum)
// we're doing a post, and the RFCs say that if you get a 301, it's not
// automatically followed, so we have to do that ourselves
responseStatus := "Haven't Tried Yet"
var url string
for responseStatus == "Haven't Tried Yet" || responseStatus == "301 Moved Permanently" {
url = conn.assembleURL(apiOp, peer)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jStatements))
if err != nil {
trace("%s: got error '%s' doing http.NewRequest", conn.ID, err.Error())
failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error()))
continue PeerLoop
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
response, err := client.Do(req)
if err != nil {
trace("%s: got error '%s' doing client.Do", conn.ID, err.Error())
failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error()))
continue PeerLoop
}
defer response.Body.Close()
responseBody, err = ioutil.ReadAll(response.Body)
if err != nil {
trace("%s: got error '%s' doing ioutil.ReadAll", conn.ID, err.Error())
failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error()))
continue PeerLoop
}
responseStatus = response.Status
if responseStatus == "301 Moved Permanently" {
v := response.Header["Location"]
failureLog = append(failureLog, fmt.Sprintf("%s redirected me to %s", url, v[0]))
url = v[0]
continue PeerLoop
} else if responseStatus == "200 OK" {
trace("%s: api call OK, returning", conn.ID)
return responseBody, nil
} else {
trace("%s: got error in responseStatus: %s", conn.ID, responseStatus)
failureLog = append(failureLog, fmt.Sprintf("%s failed, got: %s", url, response.Status))
continue PeerLoop
}
}
}
// if we got here, all peers failed. Let's build a verbose error message
var stringBuffer bytes.Buffer
stringBuffer.WriteString("tried all peers unsuccessfully. here are the results:\n")
for n, v := range failureLog {
stringBuffer.WriteString(fmt.Sprintf(" peer #%d: %s\n", n, v))
}
return responseBody, errors.New(stringBuffer.String())
}