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  }