-
Notifications
You must be signed in to change notification settings - Fork 3
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package serve | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
||
servetypes "roci.dev/diff-server/serve/types" | ||
) | ||
|
||
type ClientViewGetter struct { | ||
url string | ||
} | ||
|
||
// Get fetches a client view. It returns an error if the response from the data layer doesn't have | ||
// a lastTransactionID. | ||
func (g ClientViewGetter) Get(req servetypes.ClientViewRequest, authToken string) (servetypes.ClientViewResponse, error) { | ||
reqBody, err := json.Marshal(req) | ||
if err != nil { | ||
return servetypes.ClientViewResponse{}, fmt.Errorf("could not marshal ClientViewRequest: %w", err) | ||
} | ||
httpReq, err := http.NewRequest("POST", g.url, bytes.NewReader(reqBody)) | ||
if err != nil { | ||
return servetypes.ClientViewResponse{}, fmt.Errorf("could not create client view http request: %w", err) | ||
} | ||
httpReq.Header.Add("Authorization", authToken) | ||
httpResp, err := http.DefaultClient.Do(httpReq) | ||
if err != nil { | ||
return servetypes.ClientViewResponse{}, fmt.Errorf("error sending client view http request: %w", err) | ||
} | ||
if httpResp.StatusCode != http.StatusOK { | ||
return servetypes.ClientViewResponse{}, fmt.Errorf("client view fetch http request returned %s", httpResp.Status) | ||
} | ||
var resp servetypes.ClientViewResponse | ||
var r io.Reader = httpResp.Body | ||
defer httpResp.Body.Close() | ||
err = json.NewDecoder(r).Decode(&resp) | ||
if err != nil { | ||
return servetypes.ClientViewResponse{}, fmt.Errorf("couldnt decode client view response: %w", err) | ||
} | ||
if resp.LastTransactionID == "" { | ||
return servetypes.ClientViewResponse{}, fmt.Errorf("malformed response %v missing lastTransactionID", resp) | ||
} | ||
return resp, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package serve | ||
|
||
import ( | ||
"encoding/json" | ||
"net/http" | ||
"net/http/httptest" | ||
"reflect" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
servetypes "roci.dev/diff-server/serve/types" | ||
) | ||
|
||
func TestClientViewGetter_Get(t *testing.T) { | ||
assert := assert.New(t) | ||
|
||
type args struct { | ||
} | ||
tests := []struct { | ||
name string | ||
req servetypes.ClientViewRequest | ||
authToken string | ||
respCode int | ||
respBody string | ||
want servetypes.ClientViewResponse | ||
wantErr string | ||
}{ | ||
{ | ||
"ok", | ||
servetypes.ClientViewRequest{ClientID: "clientid"}, | ||
"authtoken", | ||
http.StatusOK, | ||
`{"clientView": "clientview", "lastTransactionID": "ltid"}`, | ||
servetypes.ClientViewResponse{ClientView: []byte("\"clientview\""), LastTransactionID: "ltid"}, | ||
"", | ||
}, | ||
{ | ||
"error", | ||
servetypes.ClientViewRequest{ClientID: "clientid"}, | ||
"authtoken", | ||
http.StatusBadRequest, | ||
``, | ||
servetypes.ClientViewResponse{}, | ||
"400", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer to test more of the error message, probably just the entire one. I know you advised erik against this in a previous review, but the error messages (at least these that the customer will see) are part of the dx of the product. It is important that they are high quality. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i do agree in this case we should match on more. but in general i dont really see how matching part of or all of an error message has anything to do with quality. i can imagine exactly matching a really crappy whole error string. or partially matching a really high quality error string. exact matching just feels a little brittle to me but nbd. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, what I mean is that they should be high quality and then we should have tests that ensure they stay that way. |
||
}, | ||
{ | ||
"missing last transaction id", | ||
servetypes.ClientViewRequest{ClientID: "clientid"}, | ||
"authtoken", | ||
http.StatusOK, | ||
`{"clientView": "foo", "lastTransactionID": ""}`, | ||
servetypes.ClientViewResponse{}, | ||
"lastTransactionID", | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
var reqBody servetypes.ClientViewRequest | ||
err := json.NewDecoder(r.Body).Decode(&reqBody) | ||
assert.NoError(err, tt.name) | ||
assert.Equal(tt.req.ClientID, reqBody.ClientID, tt.name) | ||
assert.Equal(tt.authToken, r.Header.Get("Authorization"), tt.name) | ||
w.WriteHeader(tt.respCode) | ||
w.Write([]byte(tt.respBody)) | ||
})) | ||
|
||
g := ClientViewGetter{ | ||
url: server.URL, | ||
} | ||
got, err := g.Get(tt.req, tt.authToken) | ||
if tt.wantErr == "" { | ||
assert.NoError(err) | ||
} else { | ||
assert.Error(err) | ||
assert.Regexp(tt.wantErr, err.Error(), tt.name) | ||
} | ||
if !reflect.DeepEqual(got, tt.want) { | ||
t.Errorf("ClientViewGetter.Get() case %s got %v (clientview=%s), want %v (clientview=%s)", tt.name, got, string(got.ClientView), tt.want, string(tt.want.ClientView)) | ||
} | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import ( | |
"bytes" | ||
"compress/gzip" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"log" | ||
|
@@ -25,23 +26,31 @@ import ( | |
"roci.dev/diff-server/db" | ||
"roci.dev/diff-server/kv" | ||
servetypes "roci.dev/diff-server/serve/types" | ||
nomsjson "roci.dev/diff-server/util/noms/json" | ||
) | ||
|
||
type clientViewGetter interface { | ||
Get(req servetypes.ClientViewRequest, authToken string) (servetypes.ClientViewResponse, error) | ||
} | ||
|
||
// server is a single Replicant instance. The Replicant service runs many such instances. | ||
type server struct { | ||
router *httprouter.Router | ||
db *db.DB | ||
cvg clientViewGetter | ||
mu sync.Mutex | ||
} | ||
|
||
func newServer(cs chunks.ChunkStore, urlPrefix string) (*server, error) { | ||
// cvg may be nil, in which case the server skips the client view request in pull, which is | ||
// useful if you are populating the db directly or in tests. | ||
func newServer(cs chunks.ChunkStore, urlPrefix string, cvg clientViewGetter) (*server, error) { | ||
router := httprouter.New() | ||
noms := datas.NewDatabase(cs) | ||
db, err := db.New(noms) | ||
if err != nil { | ||
return nil, err | ||
} | ||
s := &server{router: router, db: db} | ||
s := &server{router: router, db: db, cvg: cvg} | ||
s.router.POST(fmt.Sprintf("%s/handlePullRequest", urlPrefix), func(rw http.ResponseWriter, req *http.Request, ps httprouter.Params) { | ||
body := bytes.Buffer{} | ||
_, err := io.Copy(&body, req.Body) | ||
|
@@ -50,23 +59,40 @@ func newServer(cs chunks.ChunkStore, urlPrefix string) (*server, error) { | |
serverError(rw, err) | ||
return | ||
} | ||
var hsreq servetypes.PullRequest | ||
err = json.Unmarshal(body.Bytes(), &hsreq) | ||
var preq servetypes.PullRequest | ||
err = json.Unmarshal(body.Bytes(), &preq) | ||
if err != nil { | ||
serverError(rw, err) | ||
return | ||
} | ||
|
||
from, ok := hash.MaybeParse(hsreq.BaseStateID) | ||
from, ok := hash.MaybeParse(preq.BaseStateID) | ||
if !ok { | ||
clientError(rw, 400, "Invalid baseStateID") | ||
return | ||
} | ||
fromChecksum, err := kv.ChecksumFromString(hsreq.Checksum) | ||
fromChecksum, err := kv.ChecksumFromString(preq.Checksum) | ||
if err != nil { | ||
clientError(rw, 400, "Invalid checksum") | ||
} | ||
patch, err := s.db.HandlePull(from, *fromChecksum) | ||
if preq.ClientID == "" { | ||
clientError(rw, 400, "Missing ClientID") | ||
return | ||
} | ||
if cvg == nil { | ||
log.Print("WARNING: not fetching new client view: no url provided via account or --clientview") | ||
} else { | ||
var cvgError error | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defining the separate error with the special handling deviates from standard practice and makes it a bit more difficult to read. I think factoring out fetching and storing the client view might help. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah its a tradeoff i guess. the nesting of the if elses was starting to get really annoying so i went this route which is less standard but more readable to me. agree factoring fetching and storing would make it more readable. |
||
cvreq := servetypes.ClientViewRequest{ClientID: preq.ClientID} | ||
cvresp, cvgError := cvg.Get(cvreq, "") // TODO fritz pass auth token along | ||
if cvgError == nil { | ||
cvgError = storeNewClientView(db, cvresp.ClientView) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the case of failing to store the client view, something more serious has gone wrong and we should probably 500 no? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. up to us. if we think of pull as 'show me the latest client view you have from the data layer' then no. if we think of pull as 'show me exactly what the data layer has right now' then it should probably error. however it almost always makes sense to do the best with what you have in cases like this, which is why i didn't make it fatal. consider a brand new client pulling for the first time. i'd much rather that client get the latest data the server has than get nothing. |
||
} | ||
if cvgError != nil { | ||
log.Printf("WARNING: got error fetching clientview: %s", cvgError) | ||
} | ||
} | ||
|
||
patch, err := s.db.Diff(from, *fromChecksum) | ||
if err != nil { | ||
serverError(rw, err) | ||
return | ||
|
@@ -102,6 +128,24 @@ func newServer(cs chunks.ChunkStore, urlPrefix string) (*server, error) { | |
return s, nil | ||
} | ||
|
||
func storeNewClientView(db *db.DB, clientView json.RawMessage) error { | ||
v, err := nomsjson.FromJSON(bytes.NewReader(clientView), db.Noms()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i have a TODO on a piece of paper that says write a test to make sure that client view of {"key": "null"} and client view of "null" both work. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
if err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All these errors end up as For debugging purposes, I think we should return problems with the client view as |
||
return err | ||
} | ||
nm, ok := v.(types.Map) | ||
if !ok { | ||
return fmt.Errorf("clientview is not a Map, it's a %s", v.Kind().String()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
// TODO fritz yes this is inefficient, will fix up Map so we don't have to go | ||
// back and forth. But after it works. | ||
m := kv.NewMapFromNoms(db.Noms(), nm) | ||
if m == nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also it doesn't look like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suppose it could fail. if it could i would argue against crashing the server. we have talked about this before. suppose we have 1000 customers and there is one map that is bad for some reason, eg we are trying some experimental code on a developer account that goes wrong or a super old customer fired back up and we missed doing something to their map. if we crash on errors in the serving path then it is a potential DOS for all 1000 customers. our service might go down because of one bad piece of data. i have a very strong prefernce to not crash the server because it could be doing other useful things not related to the bad data, like serving good data. i have a strong preference to ERROR so that a developer comes and looks at the problem. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK I see your point, but in this case, is there a way that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, I think the map stuff is going to change anyway but yes. |
||
return errors.New("couldnt create a Map from a Noms Map") | ||
} | ||
return db.PutData(m.NomsMap(), types.String(m.Checksum().String())) | ||
} | ||
|
||
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
verbose.SetVerbose(true) | ||
log.Println("Handling request: ", r.URL.String()) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
client-view
for flag name.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit on description: