-
Notifications
You must be signed in to change notification settings - Fork 610
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
Consider using ABC #66
Comments
I was thinking it would be a good idea to use them in an uniform way - either we use them everywhere, or we don't use them (and remove their usage in the API). (I'm up for using in the API side, btw ;) ) |
My thinking was (and I thought we decided that somewhere, but can't find it) that since the APIpackage must be usable on its own as a no-op implementation, we would make the API interface the no-op implementation instead of raising exceptions (that would require a separate NoOp version of each class in the API which is a lot of boilerplate code). |
Agreed that we have to be uniform regardless of what path we take. I worry about the no-op implementations because that doesn't provide a way to let sdk authors (or consumers of sdk's) know that a particular implementation has not implemented a method. Using ABCs surfaces the warning as soon as the object is imported, which means that the missing methods will raise an error as soon as the application has started. We could potentially revisit this once we have a working system, or have a unit test that just uses the API exclusively. Some way to better understand the ramifications. |
When using ABCs, we still have to create no-op versions of every class in the API (or maybe we could autogenerate these with "reflection"). I don't think the additional warnings are worth the complications and/or boilerplate code though. As a vendor I would still think twice whether I want to risk crashing the app (even if only at startup) by deriving from an ABC that got a method added since I wrote my implementation or whether to derive from that no-op version instead, so that users get (worst-case) incomplete telemetry instead of crash-looping PODs, etc. Or put another way: If we make the base classes ABCs, if we want to be backwards compatible as far as API-implementing vendors are concerned, we must never add new methods to them. |
This is how we do it in OpenTracing. Not having this as a no-op implementation would mean creating (and maintaining) a
My same feeling.
... and this has been a recurring talk, how to not break vendors for every new (minor) version ;) |
ABC's are useful in enforcing anyone implementing the interface to at least understand the scope of what needs to be implemented up front. I would hope that a vendor wouldn't ship a crashing implementation to a user, but maybe that's me being optimistic that everyone tests their code :D. If the recommended behaviour is to extend the Default implementation anyways, then I agree that using ABCs is an unnecessary complication here. It's true that using ABCs has the disadvantage of crashing if a new abstract method is added to an interface and is not implemented, which means we have two choices when adding methods to the interface:
On the upside, this ensures that new methods in the API are auto-discoverable via crashes :) Crashing applications on upgrades of an underlying library definitely doesn't feel very python-esque though. |
The problem occurs if the user upgrades the API and the vendor hasn't tested the newer API version yet. |
Just a question here: adding a new From the discussion it kinda seems like we don't want to do a major release when this happens. I understand the issues that may come with a major release, but why not treat this kind of non backwards compatible change like any other one? |
It should be a goal to be able to make as many changes as possible without requiring a new major version. Otherwise you end up having a library that either requires users to upgrade to new major releases all the time or you need to port bug fixes back to a multitude of major versions. |
I completely agree, @Oberon00, breaking changes are to be kept as few as possible for the impact they have in users and developers. My point is just that using an I believe there is no problem with using |
There won't be a crash if there is a default implementation of the method, even if that default method does nothing (it must return something of the right type of course, if applicable). |
Summarizing a conversation with @ocelotl. Here's the problem as I understand it, written out in the form of an example: Part 1 Suppose v1 of class api.Tracer(ABC):
@abstractmethod
def start_span(self, *args) -> api.Span:
pass
class api.DefaultTracer(api.Tracer):
def start_span(self, *args) -> api.Span:
return INVALID_SPAN
class sdk.Tracer(api.Tracer):
def start_span(self, *args) -> api.Span:
... # start the span, make it active
return span An APM-vendor ships their own class alt.Tracer(api.Tracer):
def start_span(self, *args):
... # do secret APM-specific stuff
return actual_span And a third party library class XYZer:
def startup(self):
span = tracer.start_span("xyz")
self.span_cm = Tracer.use_span(span, end_on_exit=True)
self.span_cm.__enter__()
def shutdown(self):
self.span_cm.__exit__(None, None, None) Some application uses the Part 2 Later, we decide to release class api.Tracer(ABC):
@abstractmethod
def start_span(self, *args) -> api.Span:
pass
@abstractmethod
def end_span(self, span) -> bool:
"""End and deactivate `span` if it's currently active.
Returns:
Whether `span` was active at the time of the call.
"""
pass
class api.DefaultTracer(api.Tracer):
def start_span(self, *args) -> api.Span:
return INVALID_SPAN
def end_span(self, span) -> bool:
return False
class sdk.Tracer(api.Tracer):
def start_span(self, *args) -> api.Span:
... # start the span, make it active
return span
def end_span(self, span) -> bool:
ok = span == self.get_current_span()
... # end the span, pop the context stack, etc.
return ok The class XYZer:
def startup(self):
self.span = tracer.start_span()
def shutdown(self):
if not tracer.end_span():
logger.warning("Unexpected active span")
So what happens when they run the app? If If It's not clear to me that calling combination of custom and no-op methods is better than refusing to instantiate the tracer here.
It won't crash, but could produce some pretty surprising results. Especially when default classes return placeholders like
Some new API methods represent breaking changes whether or not they're Some new API methods have safe I don't think there's an obviously correct answer here, and any policy we come up with is going to rely on some value judgements:
I still like ABCs for this use case, but I agree that a telemetry library crashing a user's application because of version mismatches in transitive dependencies is an extremely bad look. |
Thanks @c24t for the summary. One of the nice advantages of using ABCs along with a Default no-op implementation is that it gives implementors the option of either implementing the interface that will crash if a method is not implemented, or extend the no-op default if that option is preferred. This would mean deferring the decision to our users rather than limiting them one way or another. Though I suppose this could also create confusion. |
It could cause confusion, yes, but that can be fixed with proper documentation 🙂 . |
We discussed this again in the SIG meeting today and decided to go ahead with ABCs (and "default" no-op implementations) in #311. This does mean some code duplication, and raises the possibility of errors as described above. Hopefully the benefits outweigh the risks. |
This change implements the Context API portion of OTEP open-telemetry#66. The CorrelationContext API and Propagation API changes will come in future PRs. We're leveraging entrypoints to support other implementations of the Context API if/when necessary. For backwards compatibility, this change uses aiocontextvars for Python versions older than 3.7. Signed-off-by: Alex Boten <aboten@lightstep.com>
This change implements the Context API portion of OTEP open-telemetry#66. The CorrelationContext API and Propagation API changes will come in future PRs. We're leveraging entrypoints to support other implementations of the Context API if/when necessary. For backwards compatibility, this change uses aiocontextvars for Python versions older than 3.7. Signed-off-by: Alex Boten <aboten@lightstep.com>
This change implements the Context API portion of OTEP #66. The CorrelationContext API and Propagation API changes will come in future PRs. We're leveraging entrypoints to support other implementations of the Context API if/when necessary. For backwards compatibility, this change uses aiocontextvars for Python versions older than 3.7. Co-authored-by: Diego Hurtado <ocelotl@users.noreply.github.com> Co-authored-by: Mauricio Vásquez <mauricio@kinvolk.io>
This change implements the Context API portion of OTEP open-telemetry#66. The CorrelationContext API and Propagation API changes will come in future PRs. We're leveraging entrypoints to support other implementations of the Context API if/when necessary. For backwards compatibility, this change uses aiocontextvars for Python versions older than 3.7. Co-authored-by: Diego Hurtado <ocelotl@users.noreply.github.com> Co-authored-by: Mauricio Vásquez <mauricio@kinvolk.io>
* Add context-base and context-asynchooks packages * add BaseScopeManager interface open-telemetry#46
Consider using abstract base classes and the various
abc.abstract*
decorators in the API package.From https://github.com/open-telemetry/opentelemetry-python/pull/61/files#r307940312.
The text was updated successfully, but these errors were encountered: