github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/cloud/spotfeed/spotfeed_test.go (about)

     1  package spotfeed
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"context"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"log"
    10  	"sync"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/aws/aws-sdk-go/aws"
    15  	"github.com/aws/aws-sdk-go/aws/request"
    16  	"github.com/aws/aws-sdk-go/service/s3"
    17  	"github.com/aws/aws-sdk-go/service/s3/s3iface"
    18  	"github.com/stretchr/testify/require"
    19  )
    20  
    21  const (
    22  	testAccountId = "123456789000"
    23  )
    24  
    25  func TestFilter(t *testing.T) {
    26  	ptr := func(t time.Time) *time.Time {
    27  		return &t
    28  	}
    29  
    30  	for _, test := range []struct {
    31  		name                 string
    32  		filters              filters
    33  		filterable           filterable
    34  		expectedFilterResult bool
    35  	}{
    36  		{
    37  			"passes_filter",
    38  			filters{
    39  				AccountId: testAccountId,
    40  				StartTime: ptr(time.Date(2020, 01, 01, 01, 00, 00, 00, time.UTC)),
    41  				EndTime:   ptr(time.Date(2020, 02, 01, 01, 00, 00, 00, time.UTC)),
    42  				Version:   1,
    43  			},
    44  			&fileMeta{
    45  				nil,
    46  				testAccountId + ".2021-02-23-04.004.8a6d6bb8",
    47  				testAccountId,
    48  				time.Date(2020, 01, 10, 04, 0, 0, 0, time.UTC),
    49  				1,
    50  				false,
    51  			},
    52  			false,
    53  		},
    54  		{
    55  			"filtered_out_by_meta",
    56  			filters{
    57  				Version: 1,
    58  			},
    59  			&fileMeta{
    60  				nil,
    61  				testAccountId + ".2021-02-23-04.004.8a6d6bb8",
    62  				testAccountId,
    63  				time.Date(2021, 02, 23, 04, 0, 0, 0, time.UTC),
    64  				4,
    65  				false,
    66  			},
    67  			true,
    68  		},
    69  		{
    70  			"filtered_out_by_start_time",
    71  			filters{
    72  				StartTime: ptr(time.Date(2020, 01, 01, 01, 00, 00, 00, time.UTC)),
    73  			},
    74  			&fileMeta{
    75  				nil,
    76  				testAccountId + ".2021-02-23-04.004.8a6d6bb8",
    77  				testAccountId,
    78  				time.Date(2019, 02, 23, 04, 0, 0, 0, time.UTC),
    79  				4,
    80  				false,
    81  			},
    82  			true,
    83  		},
    84  	} {
    85  		t.Run(test.name, func(t *testing.T) {
    86  			require.Equal(t, test.expectedFilterResult, test.filters.filter(test.filterable))
    87  		})
    88  	}
    89  }
    90  
    91  // gzipBytes takes some bytes and performs compression with compress/gzip.
    92  func gzipBytes(b []byte) ([]byte, error) {
    93  	var buffer bytes.Buffer
    94  	gw := gzip.NewWriter(&buffer)
    95  	if _, err := gw.Write(b); err != nil {
    96  		return nil, err
    97  	}
    98  	if err := gw.Close(); err != nil {
    99  		return nil, err
   100  	}
   101  	return buffer.Bytes(), nil
   102  }
   103  
   104  type mockS3Client struct {
   105  	s3iface.S3API
   106  
   107  	getObjectResults map[string]string // path (bucket/key) -> decompressed response contents
   108  
   109  	listMu                sync.Mutex
   110  	listObjectPageResults [][]string // [](result path slices)
   111  }
   112  
   113  func (m *mockS3Client) ListObjectsV2PagesWithContext(_ aws.Context, input *s3.ListObjectsV2Input, callback func(*s3.ListObjectsV2Output, bool) bool, _ ...request.Option) error {
   114  	m.listMu.Lock()
   115  	defer m.listMu.Unlock()
   116  
   117  	if len(m.listObjectPageResults) == 0 {
   118  		return fmt.Errorf("unexpected attempt to list s3 objects at path %s/%s", aws.StringValue(input.Bucket), aws.StringValue(input.Prefix))
   119  	}
   120  	var currKeys []string
   121  	currKeys, m.listObjectPageResults = m.listObjectPageResults[0], m.listObjectPageResults[1:]
   122  
   123  	resultObjects := make([]*s3.Object, len(currKeys))
   124  	for i, key := range currKeys {
   125  		resultObjects[i] = &s3.Object{
   126  			Key: aws.String(key),
   127  		}
   128  	}
   129  
   130  	callback(&s3.ListObjectsV2Output{Contents: resultObjects}, true)
   131  	return nil
   132  }
   133  
   134  func (m *mockS3Client) GetObjectWithContext(_ aws.Context, input *s3.GetObjectInput, _ ...request.Option) (*s3.GetObjectOutput, error) {
   135  	path := fmt.Sprintf("%s/%s", aws.StringValue(input.Bucket), aws.StringValue(input.Key))
   136  	if v, ok := m.getObjectResults[path]; ok {
   137  		// compress the response via gzip
   138  		gzContents, err := gzipBytes([]byte(v))
   139  		if err != nil {
   140  			return nil, fmt.Errorf("failed to compress test response at %s: %s", path, err)
   141  		}
   142  		return &s3.GetObjectOutput{
   143  			// compress the response contents via gzip
   144  			Body: ioutil.NopCloser(bytes.NewReader(gzContents)),
   145  		}, nil
   146  	} else {
   147  		return nil, fmt.Errorf("attempted to get unexpected path %s from mock s3", path)
   148  	}
   149  }
   150  
   151  func TestLoaderFetch(t *testing.T) {
   152  	ctx := context.Background()
   153  	devNull := log.New(ioutil.Discard, "", 0)
   154  	for _, test := range []struct {
   155  		name            string
   156  		loader          Loader
   157  		expectedEntries []*Entry
   158  	}{
   159  		{
   160  			"s3_no_source_files",
   161  			NewS3Loader("test-bucket", "", &mockS3Client{
   162  				listObjectPageResults: [][]string{
   163  					{}, // single empty response
   164  				},
   165  			}, devNull, testAccountId, nil, nil, 0),
   166  			[]*Entry{},
   167  		},
   168  		{
   169  			"s3_single_source_file",
   170  			NewS3Loader("test-bucket", "", &mockS3Client{
   171  				listObjectPageResults: [][]string{
   172  					{ // single populated response
   173  						testAccountId + ".2021-02-25-15.002.3eb820a5.gz",
   174  					},
   175  				},
   176  				getObjectResults: map[string]string{
   177  					"test-bucket/" + testAccountId + ".2021-02-25-15.002.3eb820a5.gz": `#Version: 1.0
   178  #Fields: Timestamp UsageType Operation InstanceID MyBidID MyMaxPrice MarketPrice Charge Version
   179  2021-02-20 18:52:50 UTC	USW2-SpotUsage:x1.16xlarge	RunInstances:SV002	i-0053c2917e2afa2f0	sir-yb3gavgp	6.669 USD	2.001 USD	0.073 USD	2
   180  2021-02-20 18:50:58 UTC	USW2-SpotUsage:x1.16xlarge	RunInstances:SV002	i-07eaa4b2bf27c4b75	sir-w13gadim	6.669 USD	2.001 USD	1.741 USD	2`,
   181  				},
   182  			}, devNull, testAccountId, nil, nil, 0),
   183  			[]*Entry{
   184  				{
   185  					AccountId:      testAccountId,
   186  					Timestamp:      time.Date(2021, 02, 20, 18, 52, 50, 0, time.UTC),
   187  					UsageType:      "USW2-SpotUsage:x1.16xlarge",
   188  					Instance:       "x1.16xlarge",
   189  					Operation:      "RunInstances:SV002",
   190  					InstanceID:     "i-0053c2917e2afa2f0",
   191  					MyBidID:        "sir-yb3gavgp",
   192  					MyMaxPriceUSD:  6.669,
   193  					MarketPriceUSD: 2.001,
   194  					ChargeUSD:      0.073,
   195  					Version:        2,
   196  				},
   197  				{
   198  					AccountId:      testAccountId,
   199  					Timestamp:      time.Date(2021, 02, 20, 18, 50, 58, 0, time.UTC),
   200  					UsageType:      "USW2-SpotUsage:x1.16xlarge",
   201  					Instance:       "x1.16xlarge",
   202  					Operation:      "RunInstances:SV002",
   203  					InstanceID:     "i-07eaa4b2bf27c4b75",
   204  					MyBidID:        "sir-w13gadim",
   205  					MyMaxPriceUSD:  6.669,
   206  					MarketPriceUSD: 2.001,
   207  					ChargeUSD:      1.741,
   208  					Version:        2,
   209  				},
   210  			},
   211  		},
   212  		{
   213  			"local_no_source_files",
   214  			NewLocalLoader(
   215  				"testdata/no_source_files",
   216  				devNull, testAccountId, nil, nil, 0),
   217  			[]*Entry{},
   218  		},
   219  		{
   220  			"local_single_source_file",
   221  			NewLocalLoader(
   222  				"testdata/single_source_file",
   223  				devNull, testAccountId, nil, nil, 0),
   224  			[]*Entry{
   225  				{
   226  					AccountId:      testAccountId,
   227  					Timestamp:      time.Date(2021, 02, 20, 18, 52, 50, 0, time.UTC),
   228  					UsageType:      "USW2-SpotUsage:x1.16xlarge",
   229  					Instance:       "x1.16xlarge",
   230  					Operation:      "RunInstances:SV002",
   231  					InstanceID:     "i-0053c2917e2afa2f0",
   232  					MyBidID:        "sir-yb3gavgp",
   233  					MyMaxPriceUSD:  6.669,
   234  					MarketPriceUSD: 2.001,
   235  					ChargeUSD:      0.073,
   236  					Version:        2,
   237  				},
   238  				{
   239  					AccountId:      testAccountId,
   240  					Timestamp:      time.Date(2021, 02, 20, 18, 50, 58, 0, time.UTC),
   241  					UsageType:      "USW2-SpotUsage:x1.16xlarge",
   242  					Instance:       "x1.16xlarge",
   243  					Operation:      "RunInstances:SV002",
   244  					InstanceID:     "i-07eaa4b2bf27c4b75",
   245  					MyBidID:        "sir-w13gadim",
   246  					MyMaxPriceUSD:  6.669,
   247  					MarketPriceUSD: 2.001,
   248  					ChargeUSD:      1.741,
   249  					Version:        2,
   250  				},
   251  				{
   252  					AccountId:      testAccountId,
   253  					Timestamp:      time.Date(2021, 02, 20, 18, 56, 14, 0, time.UTC),
   254  					UsageType:      "USW2-SpotUsage:x1.32xlarge",
   255  					Instance:       "x1.32xlarge",
   256  					Operation:      "RunInstances:SV002",
   257  					InstanceID:     "i-000e2cebfe213246e",
   258  					MyBidID:        "sir-fcg8btin",
   259  					MyMaxPriceUSD:  13.338,
   260  					MarketPriceUSD: 4.001,
   261  					ChargeUSD:      2.636,
   262  					Version:        2,
   263  				},
   264  				{
   265  					AccountId:      testAccountId,
   266  					Timestamp:      time.Date(2021, 02, 20, 18, 56, 01, 0, time.UTC),
   267  					UsageType:      "USW2-SpotUsage:x1.32xlarge",
   268  					Instance:       "x1.32xlarge",
   269  					Operation:      "RunInstances:SV002",
   270  					InstanceID:     "i-032a1a622fb441a7b",
   271  					MyBidID:        "sir-c6ag9vxn",
   272  					MyMaxPriceUSD:  13.338,
   273  					MarketPriceUSD: 4.001,
   274  					ChargeUSD:      4.001,
   275  					Version:        2,
   276  				},
   277  			},
   278  		},
   279  	} {
   280  		t.Run(test.name, func(t *testing.T) {
   281  			entries, err := test.loader.Fetch(ctx, false)
   282  			require.NoError(t, err, "unexpected error fetching local feed files")
   283  			require.Equal(t, test.expectedEntries, entries)
   284  		})
   285  	}
   286  }
   287  
   288  func TestLoaderStream(t *testing.T) {
   289  	ctx := context.Background()
   290  	devNull := log.New(ioutil.Discard, "", 0)
   291  
   292  	loader := NewS3Loader("test-bucket", "", &mockS3Client{
   293  		listObjectPageResults: [][]string{
   294  			{
   295  				// initial empty response
   296  			},
   297  			{ // single file response
   298  				testAccountId + ".2100-02-20-15.002.3eb820a5.gz",
   299  			},
   300  		},
   301  		getObjectResults: map[string]string{
   302  			"test-bucket/" + testAccountId + "2100-02-20-15.002.3eb820a5.gz": `#Version: 1.0
   303  #Fields: Timestamp UsageType Operation InstanceID MyBidID MyMaxPrice MarketPrice Charge Version
   304  2100-02-20 18:52:50 UTC	USW2-SpotUsage:x1.16xlarge	RunInstances:SV002	i-0053c2917e2afa2f0	sir-yb3gavgp	6.669 USD	2.001 USD	0.073 USD	2
   305  2100-02-20 18:50:58 UTC	USW2-SpotUsage:x1.16xlarge	RunInstances:SV002	i-07eaa4b2bf27c4b75	sir-w13gadim	6.669 USD	2.001 USD	1.741 USD	2`,
   306  		},
   307  	}, devNull, testAccountId, nil, nil, 0)
   308  
   309  	// speed up sleep duration to drain list objects slice
   310  	streamSleepDuration = time.Second
   311  
   312  	entryChan, err := loader.Stream(ctx, false)
   313  	require.NoError(t, err, "unexpected err streaming s3 entries")
   314  
   315  	// test successful drain of two entries channel
   316  	for i := 0; i < 2; i++ {
   317  		<-entryChan
   318  	}
   319  
   320  	// kill the Stream goroutine
   321  	ctx.Done()
   322  }