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