github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/mempool/herocache/dns_cache_test.go (about)

     1  package herocache_test
     2  
     3  import (
     4  	"net"
     5  	"sync"
     6  	"testing"
     7  
     8  	"github.com/stretchr/testify/require"
     9  
    10  	"github.com/onflow/flow-go/module/mempool/herocache"
    11  	"github.com/onflow/flow-go/module/metrics"
    12  	"github.com/onflow/flow-go/utils/unittest"
    13  	"github.com/onflow/flow-go/utils/unittest/network"
    14  )
    15  
    16  // TestDNSCache_HappyPath checks the correctness storing and retrieving from dns cache.
    17  func TestDNSCache_HappyPath(t *testing.T) {
    18  	total := 700             // total entries to store (i.e., 700 ip domains and 700 txt records)
    19  	sizeLimit := uint32(500) // cache size limit (i.e., 500 ip domains and 500 txt records)
    20  
    21  	ipFixtures := network.IpLookupFixture(total)
    22  	txtFixtures := network.TxtLookupFixture(total)
    23  
    24  	cache := herocache.NewDNSCache(sizeLimit, unittest.Logger(), metrics.NewNoopCollector(), metrics.NewNoopCollector())
    25  
    26  	// cache must be initially empty
    27  	ips, txts := cache.Size()
    28  	require.Equal(t, uint(0), ips)
    29  	require.Equal(t, uint(0), txts)
    30  
    31  	// adding 700 txt records and 700 ip domains to cache
    32  	testAddToCache(t, cache, ipFixtures, txtFixtures)
    33  
    34  	// cache must be full up to its limit
    35  	ips, txts = cache.Size()
    36  	require.Equal(t, uint(sizeLimit), ips)
    37  	require.Equal(t, uint(sizeLimit), txts)
    38  
    39  	// only 500 txt records and 500 ip domains must be retrievable
    40  	testRetrievalMatchCount(t, cache, ipFixtures, txtFixtures, int(sizeLimit))
    41  }
    42  
    43  // TestDNSCache_Update checks the correctness of updating dns records.
    44  func TestDNSCache_Update(t *testing.T) {
    45  	ipFixture := []net.IPAddr{network.NetIPAddrFixture()}
    46  	ipDomain := "ip-domain"
    47  
    48  	txtFixture := []string{network.TxtIPFixture()}
    49  	txtDomain := "txt-domain"
    50  
    51  	cache := herocache.NewDNSCache(10,
    52  		unittest.Logger(),
    53  		metrics.NewNoopCollector(),
    54  		metrics.NewNoopCollector())
    55  
    56  	// adding records to dns cache
    57  	require.True(t, cache.PutIpDomain(ipDomain, ipFixture, int64(0)))
    58  	require.True(t, cache.PutTxtRecord(txtDomain, txtFixture, int64(0)))
    59  
    60  	// locking both txt and ip records before updating
    61  	// to later check an update unlocks them.
    62  	locked, err := cache.LockIPDomain(ipDomain)
    63  	require.NoError(t, err)
    64  	require.True(t, locked)
    65  	locked, err = cache.LockTxtRecord(txtDomain)
    66  	require.NoError(t, err)
    67  	require.True(t, locked)
    68  
    69  	// updating ip record
    70  	updatedIpFixture := []net.IPAddr{network.NetIPAddrFixture()}
    71  	updatedIpTimestamp := int64(10)
    72  	require.NoError(t, cache.UpdateIPDomain(ipDomain, updatedIpFixture, updatedIpTimestamp))
    73  
    74  	// updating txt record
    75  	updatedTxtFixture := []string{network.TxtIPFixture()}
    76  	updatedTxtTimestamp := int64(12)
    77  	require.NoError(t, cache.UpdateTxtRecord(txtDomain, updatedTxtFixture, updatedTxtTimestamp))
    78  
    79  	// sanity checking of the updated records
    80  	// updated ip record
    81  	ipRecord, ok := cache.GetDomainIp(ipDomain)
    82  	require.True(t, ok)
    83  	require.Equal(t, ipRecord.Addresses, updatedIpFixture)
    84  	require.Equal(t, ipRecord.Domain, ipDomain)
    85  	require.False(t, ipRecord.Locked) // an update must unlock it.
    86  	require.Equal(t, ipRecord.Timestamp, updatedIpTimestamp)
    87  
    88  	// updated txt record
    89  	txtRecord, ok := cache.GetTxtRecord(txtDomain)
    90  	require.True(t, ok)
    91  	require.Equal(t, txtRecord.Records, updatedTxtFixture)
    92  	require.Equal(t, txtRecord.Txt, txtDomain)
    93  	require.False(t, txtRecord.Locked) // an update must unlock it.
    94  	require.Equal(t, txtRecord.Timestamp, updatedTxtTimestamp)
    95  }
    96  
    97  // TestDNSCache_Lock evaluates that locking a txt (or ip) record can be done successfully once, and
    98  // attempts to lock and already locked record fail.
    99  // It also evaluates that a locked record can be retrieved successfully.
   100  func TestDNSCache_Lock(t *testing.T) {
   101  	ipFixture := []net.IPAddr{network.NetIPAddrFixture()}
   102  	ipDomain := "ip-domain"
   103  
   104  	txtFixture := []string{network.TxtIPFixture()}
   105  	txtDomain := "txt-domain"
   106  
   107  	cache := herocache.NewDNSCache(
   108  		10,
   109  		unittest.Logger(),
   110  		metrics.NewNoopCollector(),
   111  		metrics.NewNoopCollector())
   112  
   113  	// adding records to dns cache
   114  	require.True(t, cache.PutIpDomain(ipDomain, ipFixture, int64(0)))
   115  	require.True(t, cache.PutTxtRecord(txtDomain, txtFixture, int64(0)))
   116  
   117  	// locks ip record
   118  	locked, err := cache.LockIPDomain(ipDomain)
   119  	require.NoError(t, err)
   120  	require.True(t, locked) // first locking attempt must go through
   121  	locked, err = cache.LockIPDomain(ipDomain)
   122  	require.NoError(t, err)
   123  	require.False(t, locked) // other locking attempts must fail
   124  
   125  	// locks txt record
   126  	locked, err = cache.LockTxtRecord(txtDomain)
   127  	require.NoError(t, err)
   128  	require.True(t, locked) // first locking attempt must go through
   129  	locked, err = cache.LockTxtRecord(txtDomain)
   130  	require.NoError(t, err)
   131  	require.False(t, locked) // other locking attempts must fail
   132  
   133  	// locked ip record must be retrievable
   134  	ipRecord, ok := cache.GetDomainIp(ipDomain)
   135  	require.True(t, ok)
   136  	require.Equal(t, ipRecord.Addresses, ipFixture)
   137  	require.Equal(t, ipRecord.Domain, ipDomain)
   138  	require.True(t, ipRecord.Locked)
   139  	require.Equal(t, ipRecord.Timestamp, int64(0))
   140  
   141  	// locked txt record must be retrievable
   142  	txtRecord, ok := cache.GetTxtRecord(txtDomain)
   143  	require.True(t, ok)
   144  	require.Equal(t, txtRecord.Records, txtFixture)
   145  	require.Equal(t, txtRecord.Txt, txtDomain)
   146  	require.True(t, txtRecord.Locked)
   147  	require.Equal(t, txtRecord.Timestamp, int64(0))
   148  }
   149  
   150  // TestDNSCache_LRU checks the correctness of cache against LRU ejection.
   151  func TestDNSCache_LRU(t *testing.T) {
   152  	total := 700             // total entries to store (i.e., 700 ip and 700 txt domains)
   153  	sizeLimit := uint32(500) // cache size limit (i.e., 500 ip and 500 txt domains)
   154  
   155  	ipFixtures := network.IpLookupListFixture(total)
   156  	txtFixtures := network.TxtLookupListFixture(total)
   157  
   158  	cache := herocache.NewDNSCache(sizeLimit, unittest.Logger(), metrics.NewNoopCollector(), metrics.NewNoopCollector())
   159  
   160  	// cache must be initially empty
   161  	ips, txts := cache.Size()
   162  	require.Equal(t, uint(0), ips)
   163  	require.Equal(t, uint(0), txts)
   164  
   165  	// adding 700 txt and 700 ip domains to cache
   166  	for _, fixture := range ipFixtures {
   167  		require.True(t, cache.PutIpDomain(fixture.Domain, fixture.Result, fixture.TimeStamp))
   168  	}
   169  
   170  	for _, fixture := range txtFixtures {
   171  		require.True(t, cache.PutTxtRecord(fixture.Txt, fixture.Records, fixture.TimeStamp))
   172  	}
   173  
   174  	// cache must be full up to its limit
   175  	ips, txts = cache.Size()
   176  	require.Equal(t, uint(sizeLimit), ips)
   177  	require.Equal(t, uint(sizeLimit), txts)
   178  
   179  	// only last 500 ip domains and txt records must be retained in the DNS cache
   180  	for i := 0; i < total; i++ {
   181  		if i < total-int(sizeLimit) {
   182  			// old dns entries must be ejected
   183  			// ip
   184  			ipRecord, ok := cache.GetDomainIp(ipFixtures[i].Domain)
   185  			require.False(t, ok)
   186  			require.Nil(t, ipRecord)
   187  			// txt records
   188  			txt, ok := cache.GetTxtRecord(txtFixtures[i].Txt)
   189  			require.False(t, ok)
   190  			require.Nil(t, txt)
   191  
   192  			continue
   193  		}
   194  
   195  		// new dns entries must be persisted
   196  		// ip
   197  		ipRecord, ok := cache.GetDomainIp(ipFixtures[i].Domain)
   198  		require.True(t, ok)
   199  		require.Equal(t, ipFixtures[i].Result, ipRecord.Addresses)
   200  		require.Equal(t, ipFixtures[i].TimeStamp, ipRecord.Timestamp)
   201  		// txt records
   202  		txtRecord, ok := cache.GetTxtRecord(txtFixtures[i].Txt)
   203  		require.True(t, ok)
   204  		require.Equal(t, txtFixtures[i].Records, txtRecord.Records)
   205  		require.Equal(t, txtFixtures[i].TimeStamp, txtRecord.Timestamp)
   206  	}
   207  }
   208  
   209  // testAddToCache is a test helper that adds ip and txt records to the cache.
   210  func testAddToCache(t *testing.T,
   211  	cache *herocache.DNSCache,
   212  	ipTestCases map[string]*network.IpLookupTestCase,
   213  	txtTestCases map[string]*network.TxtLookupTestCase) {
   214  
   215  	wg := sync.WaitGroup{}
   216  	wg.Add(len(ipTestCases) + len(txtTestCases))
   217  
   218  	for _, fixture := range ipTestCases {
   219  		require.True(t, cache.PutIpDomain(fixture.Domain, fixture.Result, fixture.TimeStamp))
   220  	}
   221  
   222  	for _, fixture := range txtTestCases {
   223  		require.True(t, cache.PutTxtRecord(fixture.Txt, fixture.Records, fixture.TimeStamp))
   224  	}
   225  }
   226  
   227  // TestDNSCache_Remove checks the correctness of cache against removal.
   228  func TestDNSCache_Remove(t *testing.T) {
   229  	total := 30              // total entries to store (i.e., 700 ip domains and 700 txt records)
   230  	sizeLimit := uint32(500) // cache size limit (i.e., 500 ip domains and 500 txt records)
   231  
   232  	ipFixtures := network.IpLookupListFixture(total)
   233  	txtFixtures := network.TxtLookupListFixture(total)
   234  
   235  	cache := herocache.NewDNSCache(sizeLimit, unittest.Logger(), metrics.NewNoopCollector(), metrics.NewNoopCollector())
   236  
   237  	// cache must be initially empty
   238  	ips, txts := cache.Size()
   239  	require.Equal(t, uint(0), ips)
   240  	require.Equal(t, uint(0), txts)
   241  
   242  	// adding 700 txt records and 700 ip domains to cache
   243  	for _, fixture := range ipFixtures {
   244  		require.True(t, cache.PutIpDomain(fixture.Domain, fixture.Result, fixture.TimeStamp))
   245  	}
   246  
   247  	for _, fixture := range txtFixtures {
   248  		require.True(t, cache.PutTxtRecord(fixture.Txt, fixture.Records, fixture.TimeStamp))
   249  	}
   250  
   251  	// cache must be full up to its limit
   252  	ips, txts = cache.Size()
   253  	require.Equal(t, uint(total), ips)
   254  	require.Equal(t, uint(total), txts)
   255  
   256  	// removes a single ip domains and txt records
   257  	require.True(t, cache.RemoveIp(ipFixtures[0].Domain))
   258  	require.True(t, cache.RemoveTxt(txtFixtures[0].Txt))
   259  	// repeated attempts on removing already removed entries must return false.
   260  	require.False(t, cache.RemoveIp(ipFixtures[0].Domain))
   261  	require.False(t, cache.RemoveTxt(txtFixtures[0].Txt))
   262  
   263  	// size must be updated post removal
   264  	ips, txts = cache.Size()
   265  	require.Equal(t, uint(total-1), ips)
   266  	require.Equal(t, uint(total-1), txts)
   267  
   268  	// only last 500 ip domains and txt records must be retained in the DNS cache
   269  	for i := 0; i < total; i++ {
   270  		if i == 0 {
   271  			// removed entries must no longer exist.
   272  			// ip
   273  			ipRecord, ok := cache.GetDomainIp(ipFixtures[i].Domain)
   274  			require.False(t, ok)
   275  			require.Nil(t, ipRecord)
   276  			// txt records
   277  			txtRecord, ok := cache.GetTxtRecord(txtFixtures[i].Txt)
   278  			require.False(t, ok)
   279  			require.Nil(t, txtRecord)
   280  
   281  			continue
   282  		}
   283  
   284  		// other entries must be existing.
   285  		// ip
   286  		ipRecord, ok := cache.GetDomainIp(ipFixtures[i].Domain)
   287  		require.True(t, ok)
   288  		require.Equal(t, ipFixtures[i].Result, ipRecord.Addresses)
   289  		require.Equal(t, ipFixtures[i].TimeStamp, ipRecord.Timestamp)
   290  		// txt records
   291  		txtRecord, ok := cache.GetTxtRecord(txtFixtures[i].Txt)
   292  		require.True(t, ok)
   293  		require.Equal(t, txtFixtures[i].Records, txtRecord.Records)
   294  		require.Equal(t, txtFixtures[i].TimeStamp, txtRecord.Timestamp)
   295  	}
   296  }
   297  
   298  // testMatchCount is a test helper that checks specified number of txt and ip domains are retrievable from the cache.
   299  // The `count` parameter specifies number of expected matches from txt and ip domains, separately.
   300  func testRetrievalMatchCount(t *testing.T,
   301  	cache *herocache.DNSCache,
   302  	ipTestCases map[string]*network.IpLookupTestCase,
   303  	txtTestCases map[string]*network.TxtLookupTestCase,
   304  	count int) {
   305  
   306  	// checking ip domains
   307  	actualCount := 0
   308  	for _, tc := range ipTestCases {
   309  		ipRecord, ok := cache.GetDomainIp(tc.Domain)
   310  		if !ok {
   311  			continue
   312  		}
   313  		require.True(t, ok)
   314  
   315  		require.Equal(t, tc.TimeStamp, ipRecord.Timestamp)
   316  		require.Equal(t, tc.Result, ipRecord.Addresses)
   317  		actualCount++
   318  	}
   319  	require.Equal(t, count, actualCount)
   320  
   321  	// checking txt records
   322  	actualCount = 0
   323  	for _, tc := range txtTestCases {
   324  		txtRecord, ok := cache.GetTxtRecord(tc.Txt)
   325  		if !ok {
   326  			continue
   327  		}
   328  		require.True(t, ok)
   329  
   330  		require.Equal(t, tc.TimeStamp, txtRecord.Timestamp)
   331  		require.Equal(t, tc.Records, txtRecord.Records)
   332  		actualCount++
   333  	}
   334  	require.Equal(t, count, actualCount)
   335  
   336  }