github.com/smugmug/godynamo@v0.0.0-20151122084750-7913028f6623/endpoints/batch_get_item/batch_get_item.go (about) 1 // Support for the DynamoDB BatchGetItem endpoint. 2 // This package offers support for request sizes that exceed AWS limits. 3 // 4 // example use: 5 // 6 // tests/batch_get_item-livestest.go 7 // 8 package batch_get_item 9 10 import ( 11 "encoding/json" 12 "errors" 13 "fmt" 14 "github.com/smugmug/godynamo/authreq" 15 "github.com/smugmug/godynamo/aws_const" 16 "github.com/smugmug/godynamo/conf" 17 ep "github.com/smugmug/godynamo/endpoint" 18 "github.com/smugmug/godynamo/types/attributestoget" 19 "github.com/smugmug/godynamo/types/attributevalue" 20 "github.com/smugmug/godynamo/types/capacity" 21 "github.com/smugmug/godynamo/types/expressionattributenames" 22 "github.com/smugmug/godynamo/types/item" 23 "net/http" 24 ) 25 26 const ( 27 ENDPOINT_NAME = "BatchGetItem" 28 JSON_ENDPOINT_NAME = ENDPOINT_NAME + "JSON" 29 BATCHGET_ENDPOINT = aws_const.ENDPOINT_PREFIX + ENDPOINT_NAME 30 // actual limit is 1024kb 31 QUERY_LIM_BYTES = 1048576 32 QUERY_LIM = 100 33 RECURSE_LIM = 50 34 ) 35 36 // RequestInstance indicates what Keys to retrieve for a Table. 37 type RequestInstance struct { 38 AttributesToGet attributestoget.AttributesToGet `json:",omitempty"` 39 ConsistentRead bool `json:",omitempty"` 40 ExpressionAttributeNames expressionattributenames.ExpressionAttributeNames `json:",omitempty"` 41 Keys []item.Item 42 ProjectionExpression string `json:",omitempty"` 43 } 44 45 func NewRequestInstance() *RequestInstance { 46 r := new(RequestInstance) 47 r.AttributesToGet = attributestoget.NewAttributesToGet() 48 r.ExpressionAttributeNames = expressionattributenames.NewExpressionAttributeNames() 49 r.Keys = make([]item.Item, 0) 50 return r 51 } 52 53 // Table2Requests maps Table names to Key and Attribute data to retrieve. 54 type Table2Requests map[string]*RequestInstance 55 56 type BatchGetItem struct { 57 RequestItems Table2Requests 58 ReturnConsumedCapacity string `json:",omitempty"` 59 } 60 61 func NewBatchGetItem() *BatchGetItem { 62 b := new(BatchGetItem) 63 b.RequestItems = make(Table2Requests) 64 return b 65 } 66 67 type Request BatchGetItem 68 69 type Response struct { 70 ConsumedCapacity []capacity.ConsumedCapacity `json:",omitempty"` 71 Responses map[string][]item.Item 72 UnprocessedKeys Table2Requests 73 } 74 75 func NewResponse() *Response { 76 r := new(Response) 77 r.ConsumedCapacity = make([]capacity.ConsumedCapacity, 0) 78 r.Responses = make(map[string][]item.Item) 79 r.UnprocessedKeys = make(Table2Requests) 80 return r 81 } 82 83 type ResponseItemsJSON struct { 84 ConsumedCapacity []capacity.ConsumedCapacity `json:",omitempty"` 85 Responses map[string][]interface{} 86 UnprocessedKeys Table2Requests 87 } 88 89 func NewResponseItemsJSON() *ResponseItemsJSON { 90 r := new(ResponseItemsJSON) 91 r.ConsumedCapacity = make([]capacity.ConsumedCapacity, 0) 92 r.Responses = make(map[string][]interface{}) 93 r.UnprocessedKeys = make(Table2Requests) 94 return r 95 } 96 97 // ToResponseItemsJSON will try to convert the Response to a ResponsesItemsJSON, 98 // where the interface value for Item represents a structure that can be 99 // marshaled into basic JSON. 100 func (resp *Response) ToResponseItemsJSON() (*ResponseItemsJSON, error) { 101 if resp == nil { 102 return nil, errors.New("batch_get_item.ToResponseItemsJson: receiver is nil") 103 } 104 resp_json := NewResponseItemsJSON() 105 for tn, rs := range resp.Responses { 106 l := len(rs) 107 resp_json.Responses[tn] = make([]interface{}, l) 108 for i, resp_item := range rs { 109 a := attributevalue.AttributeValueMap(resp_item) 110 c, cerr := a.ToInterface() 111 if cerr != nil { 112 return nil, cerr 113 } 114 resp_json.Responses[tn][i] = c 115 } 116 } 117 resp_json.ConsumedCapacity = resp.ConsumedCapacity 118 resp_json.UnprocessedKeys = resp.UnprocessedKeys 119 return resp_json, nil 120 } 121 122 // Split supports the ability to have BatchGetItem structs whose size 123 // excceds the stated AWS limits. This function splits an arbitrarily-sized 124 // BatchGetItems into a list of BatchGetItem structs that are limited 125 // to the upper bound stated by AWS. 126 func Split(b *BatchGetItem) ([]BatchGetItem, error) { 127 if b == nil { 128 return nil, errors.New("batch_get_item.Split: receiver is nil") 129 } 130 bs := make([]BatchGetItem, 0) 131 bi := NewBatchGetItem() 132 i := 0 133 for tn := range b.RequestItems { 134 for _, ri := range b.RequestItems[tn].Keys { 135 if i == QUERY_LIM { 136 bi.ReturnConsumedCapacity = b.ReturnConsumedCapacity 137 bs = append(bs, *bi) 138 bi = NewBatchGetItem() 139 i = 0 140 } 141 if _, tn_in_bi := bi.RequestItems[tn]; !tn_in_bi { 142 bi.RequestItems[tn] = NewRequestInstance() 143 bi.RequestItems[tn].AttributesToGet = 144 make(attributestoget.AttributesToGet, 145 len(b.RequestItems[tn].AttributesToGet)) 146 copy(bi.RequestItems[tn].AttributesToGet, 147 b.RequestItems[tn].AttributesToGet) 148 bi.RequestItems[tn].ConsistentRead = b.RequestItems[tn].ConsistentRead 149 } 150 bi.RequestItems[tn].Keys = append(bi.RequestItems[tn].Keys, ri) 151 i++ 152 } 153 } 154 bi.ReturnConsumedCapacity = b.ReturnConsumedCapacity 155 bs = append(bs, *bi) 156 return bs, nil 157 } 158 159 // DoBatchGet is an endpoint request handler for BatchGetItem that supports arbitrarily-sized 160 // BatchGetItem struct instances. 161 // These are split in a list of conforming BatchGetItem instances 162 // via `Split` and the concurrently dispatched to DynamoDB, 163 // with the resulting responses stitched together. May break your provisioning. 164 func (b *BatchGetItem) DoBatchGetWithConf(c *conf.AWS_Conf) ([]byte, int, error) { 165 if b == nil { 166 return nil, 0, errors.New("batch_get_item.DoBatchGetWithConf: receiver is nil") 167 } 168 if !conf.IsValid(c) { 169 return nil, 0, errors.New("batch_get_item.DoBatchGetWithConf: c is not valid") 170 } 171 bs, split_err := Split(b) 172 if split_err != nil { 173 e := fmt.Sprintf("batch_get_item.DoBatchGet: split failed: %s", split_err.Error()) 174 return nil, 0, errors.New(e) 175 } 176 resps := make(chan ep.Endpoint_Response, len(bs)) 177 for _, bi := range bs { 178 go func(bi_ BatchGetItem) { 179 body, code, err := bi_.RetryBatchGetWithConf(0, c) 180 resps <- ep.Endpoint_Response{Body: body, Code: code, Err: err} 181 }(bi) 182 } 183 combined_resp := NewResponse() 184 for i := 0; i < len(bs); i++ { 185 resp := <-resps 186 if resp.Err != nil { 187 return nil, 0, resp.Err 188 } else if resp.Code != http.StatusOK { 189 e := fmt.Sprintf("batch_get_item.DoBatchGetWithConf (%d): code %d", 190 i, resp.Code) 191 return nil, resp.Code, errors.New(e) 192 } else { 193 var r Response 194 um_err := json.Unmarshal(resp.Body, &r) 195 if um_err != nil { 196 e := fmt.Sprintf("batch_get_item.DoBatchGetWithConf (%d): %s on \n%s", 197 i, um_err.Error(), string(resp.Body)) 198 return nil, 0, errors.New(e) 199 } 200 // merge the responses from this call and the recursive one 201 _ = combineResponseMetadata(combined_resp, &r) 202 _ = combineResponses(combined_resp, &r) 203 } 204 } 205 body, marshal_err := json.Marshal(*combined_resp) 206 if marshal_err != nil { 207 return nil, 0, marshal_err 208 } 209 return body, http.StatusOK, nil 210 } 211 212 // DoBatchGet calls DoBatchGetWithConf using the global conf. 213 func (b *BatchGetItem) DoBatchGet() ([]byte, int, error) { 214 if b == nil { 215 return nil, 0, errors.New("batch_get_item.DoBatchGet: receiver is nil") 216 } 217 return b.DoBatchGetWithConf(&conf.Vals) 218 } 219 220 // unprocessedKeys2BatchGetItems will take a response from DynamoDB that indicates some Keys 221 // require resubmitting, and turns these into a BatchGetItem struct instance. 222 func unprocessedKeys2BatchGetItems(req *BatchGetItem, resp *Response) (*BatchGetItem, error) { 223 if req == nil || resp == nil { 224 return nil, errors.New("batch_get_item.unprocessedKeys2BatchGetItems: one of req or resp is nil") 225 } 226 b := NewBatchGetItem() 227 b.ReturnConsumedCapacity = req.ReturnConsumedCapacity 228 for tn := range resp.UnprocessedKeys { 229 if _, tn_in_b := b.RequestItems[tn]; !tn_in_b { 230 b.RequestItems[tn] = NewRequestInstance() 231 b.RequestItems[tn].AttributesToGet = make( 232 attributestoget.AttributesToGet, 233 len(resp.UnprocessedKeys[tn].AttributesToGet)) 234 copy(b.RequestItems[tn].AttributesToGet, 235 resp.UnprocessedKeys[tn].AttributesToGet) 236 b.RequestItems[tn].ConsistentRead = 237 resp.UnprocessedKeys[tn].ConsistentRead 238 for _, item_src := range resp.UnprocessedKeys[tn].Keys { 239 item_cp := item.NewItem() 240 for k, v := range item_src { 241 v_cp := attributevalue.NewAttributeValue() 242 cp_err := v.Copy(v_cp) 243 if cp_err != nil { 244 return nil, cp_err 245 } 246 item_cp[k] = v_cp 247 } 248 b.RequestItems[tn].Keys = append(b.RequestItems[tn].Keys, item_cp) 249 } 250 } 251 } 252 return b, nil 253 } 254 255 // Add ConsumedCapacity from "this" Response to "all", the eventual stitched Response. 256 func combineResponseMetadata(all, this *Response) error { 257 if all == nil || this == nil { 258 return errors.New("batch_get_item.combineResponseMetadata: all or this is nil") 259 } 260 combinedConsumedCapacity := make([]capacity.ConsumedCapacity, 0) 261 for _, this_cc := range this.ConsumedCapacity { 262 var cc capacity.ConsumedCapacity 263 cc.TableName = this_cc.TableName 264 cc.CapacityUnits = this_cc.CapacityUnits 265 for _, all_cc := range all.ConsumedCapacity { 266 if all_cc.TableName == this_cc.TableName { 267 cc.CapacityUnits += all_cc.CapacityUnits 268 } 269 } 270 combinedConsumedCapacity = append(combinedConsumedCapacity, cc) 271 } 272 all.ConsumedCapacity = combinedConsumedCapacity 273 return nil 274 } 275 276 // Add actual response data from "this" Response to "all", the eventual stitched Response. 277 func combineResponses(all, this *Response) error { 278 if all == nil || this == nil { 279 return errors.New("batch_get_item.combineResponses: all or this is nil") 280 } 281 for tn := range this.Responses { 282 if _, tn_in_all := all.Responses[tn]; !tn_in_all { 283 all.Responses[tn] = make([]item.Item, 0) 284 } 285 for _, item_src := range this.Responses[tn] { 286 item_cp := item.NewItem() 287 for k, v := range item_src { 288 v_cp := attributevalue.NewAttributeValue() 289 cp_err := v.Copy(v_cp) 290 if cp_err != nil { 291 return cp_err 292 } 293 item_cp[k] = v_cp 294 } 295 all.Responses[tn] = append(all.Responses[tn], item_cp) 296 } 297 } 298 return nil 299 } 300 301 // RetryBatchGetWithConf will attempt to fully complete a conforming BatchGetItem request. 302 // Callers for this method should be of len QUERY_LIM or less (see DoBatchGets()). 303 // This is different than EndpointReq in that it will extract UnprocessedKeys and 304 // form new BatchGetItem's based on those, and combine any results. 305 func (b *BatchGetItem) RetryBatchGetWithConf(depth int, c *conf.AWS_Conf) ([]byte, int, error) { 306 if b == nil { 307 return nil, 0, errors.New("batch_get_item.RetryBatchGetWithConf: receiver is nil") 308 } 309 if depth > RECURSE_LIM { 310 e := fmt.Sprintf("batch_get_item.RetryBatchGetWithConf: recursion depth exceeded") 311 return nil, 0, errors.New(e) 312 } 313 body, code, err := b.EndpointReqWithConf(c) 314 if err != nil || code != http.StatusOK { 315 return body, code, err 316 } 317 // we'll need an actual Response object 318 var resp Response 319 um_err := json.Unmarshal([]byte(body), &resp) 320 if um_err != nil { 321 e := fmt.Sprintf("batch_get_item.RetryBatchGetWithConf: %s", um_err.Error()) 322 return nil, 0, errors.New(e) 323 } 324 // if there are unprocessed items remaining from this call... 325 if len(resp.UnprocessedKeys) > 0 { 326 // make a new BatchGetItem object based on the unprocessed items 327 n_req, n_req_err := unprocessedKeys2BatchGetItems(b, &resp) 328 if n_req_err != nil { 329 e := fmt.Sprintf("batch_get_item.RetryBatchGet: %s", n_req_err.Error()) 330 return nil, 0, errors.New(e) 331 } 332 // call this function on the new object 333 n_body, n_code, n_err := n_req.RetryBatchGetWithConf(depth+1, c) 334 if n_err != nil || n_code != http.StatusOK { 335 return nil, n_code, n_err 336 } 337 // get the response as an object 338 var n_resp Response 339 um_err := json.Unmarshal(n_body, &n_resp) 340 if um_err != nil { 341 e := fmt.Sprintf("batch_get_item.RetryBatchGet: %s", um_err.Error()) 342 return nil, 0, errors.New(e) 343 } 344 // merge the responses from this call and the recursive one 345 _ = combineResponseMetadata(&resp, &n_resp) 346 _ = combineResponses(&resp, &n_resp) 347 // make a response string again out of the merged responses 348 resp_json, resp_json_err := json.Marshal(resp) 349 if resp_json_err != nil { 350 e := fmt.Sprintf("batch_get_item.RetryBatchGet: %s", resp_json_err.Error()) 351 return nil, 0, errors.New(e) 352 } 353 body = resp_json 354 } 355 return body, code, err 356 } 357 358 // RetryBatchGet is just a wrapper for RetryBatchGetWithConf using the global conf. 359 func (b *BatchGetItem) RetryBatchGet(depth int) ([]byte, int, error) { 360 if b == nil { 361 return nil, 0, errors.New("batch_get_item.RetryBatchGet: receiver is nil") 362 } 363 return b.RetryBatchGetWithConf(depth, &conf.Vals) 364 } 365 366 // These implementations of EndpointReq use a parameterized conf. 367 368 func (batch_get_item *BatchGetItem) EndpointReqWithConf(c *conf.AWS_Conf) ([]byte, int, error) { 369 if batch_get_item == nil { 370 return nil, 0, errors.New("batch_get_item.(BatchGetItem)EndpointReqWithConf: receiver is nil") 371 } 372 if !conf.IsValid(c) { 373 return nil, 0, errors.New("batch_get_item.EndpointReqWithConf: c is not valid") 374 } 375 // returns resp_body,code,err 376 reqJSON, json_err := json.Marshal(batch_get_item) 377 if json_err != nil { 378 return nil, 0, json_err 379 } 380 return authreq.RetryReqJSON_V4WithConf(reqJSON, BATCHGET_ENDPOINT, c) 381 } 382 383 func (req *Request) EndpointReqWithConf(c *conf.AWS_Conf) ([]byte, int, error) { 384 if req == nil { 385 return nil, 0, errors.New("batch_get_item.(Request)EndpointReqWithConf: receiver is nil") 386 } 387 batch_get_item := BatchGetItem(*req) 388 return batch_get_item.EndpointReqWithConf(c) 389 } 390 391 // These implementations of EndpointReq use the global conf. 392 393 func (batch_get_item *BatchGetItem) EndpointReq(c *conf.AWS_Conf) ([]byte, int, error) { 394 if batch_get_item == nil { 395 return nil, 0, errors.New("batch_get_item.(BatchGetItem)EndpointReq: receiver is nil") 396 } 397 return batch_get_item.EndpointReqWithConf(&conf.Vals) 398 } 399 400 func (req *Request) EndpointReq() ([]byte, int, error) { 401 if req == nil { 402 return nil, 0, errors.New("batch_get_item.(Request)EndpointReq: receiver is nil") 403 } 404 batch_get_item := BatchGetItem(*req) 405 return batch_get_item.EndpointReqWithConf(&conf.Vals) 406 }