Repository: cloud-custodian/cloud-custodian

Issue:
filter using value_from takes excessively long
**Describe the bug**
We have a security-group ingress filter that uses `value_from` to pull a list of cidr values from a url.  This causes the filtering step to take roughly 15 seconds to filter 34 security groups.  If I replace this with a `value` with a list of cidr addresses, the filter step is done in milliseconds.

**To Reproduce**
The following security-group filter:
```yaml
  - name: hourly-aws-std-sg01-compliance-scan
    resource: security-group
    filters:
      - type: ingress
        Cidr:
          op: in
          value_from:
            url: https://api.cloudflare.com/client/v4/ips
            format: json
            expr: "[result.ipv4_cidrs, result.ipv6_cidrs]|[]"
```
produced the following timing (17.5 seconds to filter 34 security-groups)
```
2020-04-02 14:41:49,574: custodian.resources.securitygroup:DEBUG Using cached c7n.resources.vpc.SecurityGroup: 34
2020-04-02 14:42:07,069: custodian.resources.securitygroup:DEBUG Filtered from 34 to 1 securitygroup
2020-04-02 14:42:07,069: custodian.policy:INFO policy:hourly-aws-std-sg01-compliance-scan resource:security-group region:us-west-2 count:1 time:17.50
```

**Expected behavior**
If I replace the filter with a static list of cidrs to compare against:
```yaml
  - name: hourly-aws-std-sg01-compliance-scan
    resource: security-group
    filters:
      - type: ingress
        Cidr:
          op: in
          value:
            - "173.245.48.0/20"
            - "103.21.244.0/22"
            - "103.22.200.0/22"
            - "103.31.4.0/22"
            - "141.101.64.0/18"
            - "108.162.192.0/18"
            - "190.93.240.0/20"
            - "188.114.96.0/20"
            - "197.234.240.0/22"
            - "198.41.128.0/17"
            - "162.158.0.0/15"
            - "104.16.0.0/12"
            - "172.64.0.0/13"
            - "131.0.72.0/22"
            - "2400:cb00::/32"
            - "2606:4700::/32"
            - "2803:f800::/32"
            - "2405:b500::/32"
            - "2405:8100::/32"
            - "2a06:98c0::/29"
            - "2c0f:f248::/32"
```
it's nearly instantaneous:
```
2020-04-02 14:45:27,569: custodian.resources.securitygroup:DEBUG Using cached c7n.resources.vpc.SecurityGroup: 34
2020-04-02 14:45:27,570: custodian.resources.securitygroup:DEBUG Filtered from 34 to 1 securitygroup
2020-04-02 14:45:27,570: custodian.policy:INFO policy:hourly-aws-std-sg01-compliance-scan resource:security-group region:us-west-2 count:1 time:0.00
```

**Background (please complete the following information):**
 - OS: OSX 10.15.4
 - Python Version: python 3.7.6
 - Custodian Version: 0.8.46.1


Discussion:
Actually, the more I look at the code, it doesn't look like it's using the cache for https destinations ...

Resolving patch:
diff --git a/c7n/resolver.py b/c7n/resolver.py
--- a/c7n/resolver.py
+++ b/c7n/resolver.py
@@ -37,6 +37,11 @@ def __init__(self, session_factory, cache):
         self.cache = cache
 
     def resolve(self, uri):
+        if self.cache:
+            contents = self.cache.get(("uri-resolver", uri))
+            if contents is not None:
+                return contents
+
         if uri.startswith('s3://'):
             contents = self.get_s3_uri(uri)
         else:
@@ -46,7 +51,8 @@ def resolve(self, uri):
             with closing(urlopen(req)) as response:
                 contents = self.handle_response_encoding(response)
 
-        self.cache.save(("uri-resolver", uri), contents)
+        if self.cache:
+            self.cache.save(("uri-resolver", uri), contents)
         return contents
 
     def handle_response_encoding(self, response):
@@ -93,7 +99,7 @@ class ValuesFrom:
          url: s3://bucket/xyz/foo.json
          expr: [].AppId
 
-      values_from:
+      value_from:
          url: http://foobar.com/mydata
          format: json
          expr: Region."us-east-1"[].ImageId
@@ -129,6 +135,7 @@ def __init__(self, data, manager):
         }
         self.data = format_string_values(data, **config_args)
         self.manager = manager
+        self.cache = manager._cache
         self.resolver = URIResolver(manager.session_factory, manager._cache)
 
     def get_contents(self):
@@ -147,6 +154,21 @@ def get_contents(self):
         return contents, format
 
     def get_values(self):
+        if self.cache:
+            # use these values as a key to cache the result so if we have
+            # the same filter happening across many resources, we can reuse
+            # the results.
+            key = [self.data.get(i) for i in ('url', 'format', 'expr')]
+            contents = self.cache.get(("value-from", key))
+            if contents is not None:
+                return contents
+
+        contents = self._get_values()
+        if self.cache:
+            self.cache.save(("value-from", key), contents)
+        return contents
+
+    def _get_values(self):
         contents, format = self.get_contents()
 
         if format == 'json':


Test patch:
diff --git a/tests/test_resolver.py b/tests/test_resolver.py
--- a/tests/test_resolver.py
+++ b/tests/test_resolver.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 import csv
 import json
+import pickle
 import os
 import tempfile
 import vcr
@@ -30,12 +31,16 @@ class FakeCache:
 
     def __init__(self):
         self.state = {}
+        self.gets = 0
+        self.saves = 0
 
     def get(self, key):
-        return self.state.get(key)
+        self.gets += 1
+        return self.state.get(pickle.dumps(key))
 
     def save(self, key, data):
-        self.state[key] = data
+        self.saves += 1
+        self.state[pickle.dumps(key)] = data
 
 
 class FakeResolver:
@@ -72,7 +77,7 @@ def test_resolve_s3(self):
         uri = "s3://%s/resource.json?RequestPayer=requestor" % bname
         data = resolver.resolve(uri)
         self.assertEqual(content, data)
-        self.assertEqual(list(cache.state.keys()), [("uri-resolver", uri)])
+        self.assertEqual(list(cache.state.keys()), [pickle.dumps(("uri-resolver", uri))])
 
     def test_handle_content_encoding(self):
         session_factory = self.replay_flight_data("test_s3_resolver")
@@ -106,9 +111,9 @@ def setUp(self):
     def tearDown(self):
         os.chdir(self.old_dir)
 
-    def get_values_from(self, data, content):
+    def get_values_from(self, data, content, cache=None):
         config = Config.empty(account_id=ACCOUNT_ID)
-        mgr = Bag({"session_factory": None, "_cache": None, "config": config})
+        mgr = Bag({"session_factory": None, "_cache": cache, "config": config})
         values = ValuesFrom(data, mgr)
         values.resolver = FakeResolver(content)
         return values
@@ -181,3 +186,16 @@ def test_value_from_vars(self):
         )
         self.assertEqual(values.get_values(), ["east-resource"])
         self.assertEqual(values.data.get("url", ""), ACCOUNT_ID)
+
+    def test_value_from_caching(self):
+        cache = FakeCache()
+        values = self.get_values_from(
+            {"url": "", "expr": '["{region}"][]', "format": "json"},
+            json.dumps({"us-east-1": "east-resource"}),
+            cache=cache,
+        )
+        self.assertEqual(values.get_values(), ["east-resource"])
+        self.assertEqual(values.get_values(), ["east-resource"])
+        self.assertEqual(values.get_values(), ["east-resource"])
+        self.assertEqual(cache.saves, 1)
+        self.assertEqual(cache.gets, 3)