Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add grpc sync flag store #84

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

colebaileygit
Copy link
Contributor

This PR

  • adds gRPC sync source for flagd in-process evaluator

Related Issues

Fixes #83

Notes

Follow-up Tasks

How to test

Copy link

codecov bot commented Apr 27, 2024

Codecov Report

Attention: Patch coverage is 97.12919% with 6 lines in your changes missing coverage. Please review.

Project coverage is 93.21%. Comparing base (d8e10c7) to head (e9cd2ed).
Report is 33 commits behind head on main.

Files Patch % Lines
.../flagd/resolvers/process/connector/file_watcher.py 95.52% 3 Missing ⚠️
...src/openfeature/contrib/provider/flagd/provider.py 83.33% 1 Missing ⚠️
...ure/contrib/provider/flagd/resolvers/in_process.py 94.73% 1 Missing ⚠️
.../contrib/provider/flagd/resolvers/process/flags.py 96.55% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #84      +/-   ##
==========================================
+ Coverage   90.55%   93.21%   +2.65%     
==========================================
  Files           8       15       +7     
  Lines         180      560     +380     
==========================================
+ Hits          163      522     +359     
- Misses         17       38      +21     
Flag Coverage Δ
unittests ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Signed-off-by: Cole Bailey <[email protected]>
Copy link
Member

@federicobond federicobond left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work @colebaileygit! I left a few comments and questions merely to help push this forward, without being exhaustive. I know this is still in draft.

context: test-harness
dockerfile: sync/Dockerfile.unstable
ports:
- 9091:9090
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make sure this file is excluded from published package. We can do so from the pyproject.toml

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Checking this now it also includes tests and test-harness etc. Should we simplify to only include src?

[tool.hatch.build.targets.sdist]
include = [
  "src",
]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd have to check the generated build to confirm. If you can please make this change in a separate PR.

providers/openfeature-provider-flagd/pyproject.toml Outdated Show resolved Hide resolved
from openfeature.provider.provider import AbstractProvider

from ....config import Config
from ....proto.flagd.sync.v1 import sync_pb2, sync_pb2_grpc
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For more than one level up, an absolute import is much easier to read and to update if necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was getting some mypy errors when using absolute import, need to check how to fix those:

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py:10: error:
Cannot find implementation or library stub for module named
"openfeature.contrib.provider.flagd.proto.flagd.sync.v1"  [import-not-found]
    from openfeature.contrib.provider.flagd.proto.flagd.sync.v1 import (
    ^

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't figure this out in a simple way, seems related to using pre-commit at root of a "mono-repo" but I don't see any clear resources of how to structure it better. Maybe this is a follow-up issue?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach that may work as long as the number of packages remains manageable.

@colebaileygit colebaileygit marked this pull request as ready for review May 1, 2024 14:29
@colebaileygit colebaileygit requested a review from a team as a code owner May 1, 2024 14:29
Copy link

@Kavindu-Dodan Kavindu-Dodan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overal looks good 🙌 nice work and I left few comments, check them when you can

@@ -42,6 +44,14 @@ def __init__( # noqa: PLR0913
env_or_default("FLAGD_TLS", False, cast=str_to_bool) if tls is None else tls
)
self.timeout = 5 if timeout is None else timeout
self.retry_backoff_seconds: float = (
float(env_or_default("FLAGD_RETRY_BACKOFF_SECONDS", 2.0))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we do cast=float here for consistency?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but then it gives me this:

Screenshot 2024-06-20 at 21 09 12

After fighting with mypy a bit I realised that pulling the cast outside of the function is the cleanest way to have clean typing. I would instead suggest refactoring to remove this helper function everywhere and just use the python libraries directly. This abstraction doesn't add too much if you ask me:

self.retry_backoff_seconds: float = (
    env_or_default("FLAGD_RETRY_BACKOFF_SECONDS", 2.0, cast=float)
    if retry_backoff_seconds is None
    else retry_backoff_seconds
)

vs

self.retry_backoff_seconds: float = (
    float(os.environ.get("FLAGD_RETRY_BACKOFF_SECONDS", 2.0))
    if retry_backoff_seconds is None
    else retry_backoff_seconds
)

@@ -2,7 +2,7 @@
"""Client and server classes corresponding to protobuf-defined services."""
import grpc

from flagd.evaluation.v1 import evaluation_pb2 as flagd_dot_evaluation_dot_v1_dot_evaluation__pb2
from . import evaluation_pb2 as flagd_dot_evaluation_dot_v1_dot_evaluation__pb2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes automated after grpc code generation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, I actually didn't notice that script. When I try to run it I get this file not found error, does it need to be fixed and maybe documented in the contributing guide?

Failure: could not read file schemas/protobuf/buf.gen.python.yaml: open schemas/protobuf/buf.gen.python.yaml: no such file or directory


def initialize(self, evaluation_context: EvaluationContext) -> None:
self.active = True
self.thread = threading.Thread(target=self.refresh_file, daemon=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is daemon set to true in this thread?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a name to all child threads for easier debugging. This could be FlagdFileWatcherWorkerThread

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically this:

By setting them as daemon threads, you can let them run and forget about them, and when your program quits, any daemon threads are killed automatically.

https://stackoverflow.com/questions/190010/daemon-threads-explanation

transposed = re.sub(
rf"{{\s*\"\$ref\":\s*\"{name}\"\s*}}", json.dumps(rule), transposed
)
flags = json.loads(transposed)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should include unit tests for this parsing.

flag = cls(key=key, **data)
return flag
except Exception as err:
raise ParseError from err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add unit tests for this method please.

raise ProviderNotReadyError from err

def shutdown(self) -> None:
self.active = False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shutting down and then initializing again can leave more than one thread running here, because active might not be checked until after the sleep expires.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have the same issue in both connector implementations. The current thread handling logic is a bit fragile, would it make sense to build a cancellable scheduled task manager and concentrate our efforts on making sure its implementation is robust? I'm happy to discuss how to approach it.

Copy link
Contributor Author

@colebaileygit colebaileygit Jun 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds sensible. Would this be a simple refactor/enhancement we can add directly to this PR? It might be easier for me to review and test a first reference implementation if you can help provide that. I'm definitely not an expert in python thread handling


def initialize(self, evaluation_context: EvaluationContext) -> None:
self.active = True
self.thread = threading.Thread(target=self.refresh_file, daemon=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a name to all child threads for easier debugging. This could be FlagdFileWatcherWorkerThread

@beeme1mr
Copy link
Member

Hey @colebaileygit, could you please resolve all the conversations that have been addressed? It would be great to merge this soon.

@colebaileygit
Copy link
Contributor Author

Hey @colebaileygit, could you please resolve all the conversations that have been addressed? It would be great to merge this soon.

Sorry for disappearing, had a European style vacation and getting back into things now. Will take some time to revisit the PR this week

Signed-off-by: Cole Bailey <[email protected]>
Signed-off-by: Cole Bailey <[email protected]>
@beeme1mr
Copy link
Member

beeme1mr commented Jul 1, 2024

Sorry for disappearing, had a European style vacation and getting back into things now. Will take some time to revisit the PR this week

No worries. I hope you had a relaxing vacation. A European-style vacation sure sounds nice😄 At any rate, can you resolve all the addressed threads and provide a short summary of what's remaining? It looks like it's close, and I would love to help get it across the finish line.

@colebaileygit
Copy link
Contributor Author

At any rate, can you resolve all the addressed threads and provide a short summary of what's remaining? It looks like it's close, and I would love to help get it across the finish line.

@beeme1mr I've closed some threads where I provided a best effort fix, feel free to reopen @federicobond if you think they are important!

Main points still open:

Otherwise I'd say remaining topics can be follow-up tasks that don't need to block this PR:

  • fixing grpc code generation
  • fixing env_or_default cast usage
  • fixing mypy and absolute imports
  • fixing files embedded in published package

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[flagd-process] gRPC sync as flagd data source
4 participants