Repository: ansible/ansible
Pull request: Check if a string is possibly a template in `ansible.template.is_template`
##### SUMMARY
Move `ansible.template.Templar.is_possibly_template` to a standalone function and call it from `ansible.template.is_template`.

If a very large string is not a template, previously `is_template` would go through the entire string before reaching that conclusion. Now it will return before iterating through the entire string if there are no markers in the string.

Fixes #74336.


##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
- Feature Pull Request

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below -->
`lib/ansible/template/__init__.py`

Problem statement:
Improve performance of `ansible.template.is_template` with quick check short circuit
### Summary

`ansible.template.is_template` tries to loop until it determines if the value is indeed a template.  Which means we could loop extremely large strings, that don't even remotely look like templates, before we bail.

However, we could benefit from moving `Templar.is_possibly_template` to the global scope, and testing whether the value after `jinja_env.preprocess` is even possibly a template, before doing more computationally complex validation.

### Issue Type

Feature Idea

### Component Name

lib/ansible/template/__init__.py

### Additional Information

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)

```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct

Hints from issue discussion:
Files identified in the description:
* [`lib/ansible/template/__init__.py`](https://github.com/ansible/ansible/blob/devel/lib/ansible/template/__init__.py)

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->

Commit message: Add changelog

Patch to review:
diff --git a/changelogs/fragments/74336-is_template-quick-check.yml b/changelogs/fragments/74336-is_template-quick-check.yml
new file mode 100644
index 00000000000000..2c6b05d74ea993
--- /dev/null
+++ b/changelogs/fragments/74336-is_template-quick-check.yml
@@ -0,0 +1,4 @@
+minor_changes:
+    - >-
+        add a quick short circuit when checking if a string is a template to improve performance
+        on large strings (https://github.com/ansible/ansible/issues/74336)
diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py
index d2e39cb2f62fc4..52746c106a06f2 100644
--- a/lib/ansible/template/__init__.py
+++ b/lib/ansible/template/__init__.py
@@ -188,6 +188,25 @@ def _escape_backslashes(data, jinja_env):
     return data
 
 
+def is_possibly_template(data, jinja_env):
+    """Determines if a string looks like a template, by seeing if it
+    contains a jinja2 start delimiter. Does not guarantee that the string
+    is actually a template.
+
+    This is different than ``is_template`` which is more strict.
+    This method may return ``True`` on a string that is not templatable.
+
+    Useful when guarding passing a string for templating, but when
+    you want to allow the templating engine to make the final
+    assessment which may result in ``TemplateSyntaxError``.
+    """
+    if isinstance(data, string_types):
+        for marker in (jinja_env.block_start_string, jinja_env.variable_start_string, jinja_env.comment_start_string):
+            if marker in data:
+                return True
+    return False
+
+
 def is_template(data, jinja_env):
     """This function attempts to quickly detect whether a value is a jinja2
     template. To do so, we look for the first 2 matching jinja2 tokens for
@@ -198,6 +217,11 @@ def is_template(data, jinja_env):
     comment = False
     d2 = jinja_env.preprocess(data)
 
+    # Quick check to see if this is remotely like a template before doing
+    # more expensive investigation.
+    if not is_possibly_template(data, jinja_env):
+        return False
+
     # This wraps a lot of code, but this is due to lex returning a generator
     # so we may get an exception at any part of the loop
     try:
@@ -914,23 +938,7 @@ def is_template(self, data):
     templatable = is_template
 
     def is_possibly_template(self, data):
-        '''Determines if a string looks like a template, by seeing if it
-        contains a jinja2 start delimiter. Does not guarantee that the string
-        is actually a template.
-
-        This is different than ``is_template`` which is more strict.
-        This method may return ``True`` on a string that is not templatable.
-
-        Useful when guarding passing a string for templating, but when
-        you want to allow the templating engine to make the final
-        assessment which may result in ``TemplateSyntaxError``.
-        '''
-        env = self.environment
-        if isinstance(data, string_types):
-            for marker in (env.block_start_string, env.variable_start_string, env.comment_start_string):
-                if marker in data:
-                    return True
-        return False
+        return is_possibly_template(data, self.environment)
 
     def _convert_bare_variable(self, variable):
         '''


Code review comments:
diff_hunk: @@ -198,6 +217,11 @@ def is_template(data, jinja_env):
     comment = False
     d2 = jinja_env.preprocess(data)
 
+    # Quick check to see if this is remotely like a template before doing
+    # more expensive investigation.
+    if not is_possibly_template(data, jinja_env):
path: lib/ansible/template/__init__.py
text: @user1:
I think we should be using `d2` instead of `data` here.  Or move this before `found = None` even.  I have no idea what the performance hit is of `Environment.preprocess`.

@author:
Oh, woops. I meant to do that.