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  }