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

ref(metrics): Release health service INGEST-376 #28598

Merged
merged 14 commits into from
Sep 20, 2021

Conversation

jjbayer
Copy link
Member

@jjbayer jjbayer commented Sep 15, 2021

Provide a common interface for release health queries, so we can easily switch between the "sessions" backend and the metrics backend.

Note: The service class in this PR only features a single release health related function. More functions will be ported in future PRs.

https://getsentry.atlassian.net/browse/INGEST-376

Workflow to port a function:

  1. Select a function from sentry/snuba/sessions.py and add the same signature to sentry/releasehealth/base.py.
  2. Add an implementation to sentry/releasehealth/sessions.py which calls the original function.
  3. Add an implementation to sentry/releasehealth/metrics.py which retrieves the same information from the metrics dataset.
  4. Prefix all existing calls to your function with releasehealth.
  5. Go to tests/snuba/sessions/test_sessions.py and identify the test case corresponding to your function.
  6. Inside the test case, prefix calls to your function with releasehealth.
  7. Below that test case (let's call it Xx), add a class
    class XxMetrics(ReleaseHealthMetricsTestCase, Xx):
        pass
    This will repeat the test using the metrics backend.

Note: The metrics-based test will be skipped unless you have a snuba instance with metrics enabled:

  1. Stop the snuba docker container started by devservices,
  2. Start a local snuba with special config:
    export SNUBA_SETTINGS=/path/to/settings_metrics.py
    snuba bootstrap --force
    snuba api
    where settings_metrics.py looks like this:
    from snuba.settings import *
    
    ENABLE_DEV_FEATURES = True
    DISABLED_DATASETS.remove("metrics")
    SKIPPED_MIGRATION_GROUPS.remove("metrics")

@jjbayer jjbayer requested review from untitaker and RaduW September 15, 2021 14:59
Copy link
Member

@untitaker untitaker left a comment

Choose a reason for hiding this comment

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

I think after basic review this is ready to merge and deploy. I'd rather deploy in small steps instead of later having to merge a large refactor branch

@jjbayer jjbayer merged commit 31e6dd6 into master Sep 20, 2021
@jjbayer jjbayer deleted the ref/release-health-service branch September 20, 2021 08:49
jjbayer added a commit to getsentry/snuba that referenced this pull request Sep 21, 2021
The existing API endpoint /tests/<dataset:dataset>/insert allows writing
data to the default entity of each dataset. In the case of metrics, this
means that only metrics_sets can be written.

This PR introduces an additional endpoint
/tests/entities/<entity:entity>/insert, which allows the client to
define the target entity explicitly.

This is required to run tests in getsentry/sentry#28598.
from sentry.utils.services import Service


class ReleaseHealthBackend(Service): # type: ignore
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is ReleaseHealthBackend inheriting from Service, Service has two functions that are validate and setup, that none of the ReleaseHealthBackend child classes define so I feel there is no point in this inheritance, and probably makes sense to just remove # type: ignore here
Also should the ReleaseHealthBackend class be an abstractclass?

Copy link
Member Author

@jjbayer jjbayer Sep 27, 2021

Choose a reason for hiding this comment

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

Service is a special class which, in combination with LazyServiceWrapper, lets you expose the methods of the configured implementation as locals in the module. It's widely used in our code base, which is why we chose the same pattern for this abstraction. AFAIK nobody has gotten it to play nice with mypy yet, which is why we need all the #type: ignore comments.

I tried making this an abc.ABC initially, but ran into an issue I don't remember right now.



def metric_id(org_id: int, name: str) -> int:
index = indexer.resolve(org_id, UseCase.TAG_KEY, name) # type: ignore
Copy link
Contributor

Choose a reason for hiding this comment

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

@untitaker This is what I meant by places where # type: ignore can be avoided. Seems to me that indexer is of type StringIndexer , and resolve always returns int
typing the indexer imported should be enough to eliminate a few # type: ignore

Copy link
Member

@untitaker untitaker Sep 27, 2021

Choose a reason for hiding this comment

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

indexer is a module. This is a problem of our lazy service abstraction which does too much magic for mypy to comprehend:

We're certainly not going to type the indexer. That module is under active development by another team.

Copy link
Contributor

@ahmedetefy ahmedetefy Sep 27, 2021

Choose a reason for hiding this comment

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

What I meant by typing the indexer is string_indexer: StringIndexer = indexer in this class given that this uses an instance of StringIndexer, no? and StringIndexer.resolve returns an int
Isn't that a potential way to eliminate this # type: ignore?

Copy link
Member

Choose a reason for hiding this comment

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

right but I believe we need a more generic solution to the service problem, I played around with this in hackweek and didn't come up with something satisfactory.

Copy link
Contributor

Choose a reason for hiding this comment

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

Just to be completely clear on this, I do believe some cases do require a # type: ignore but what I am advocating here is that we investigate prior to falling back on this
Imo type casting the instance of indexer imported here is a satisfactory alternative to ignoring the type

Copy link
Member

Choose a reason for hiding this comment

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

I understand, but we have investigated solutions to give service methods the correct types, I just don't agree with your suggestion as I don't think the time is well-spent in coming up with usage-specific workarounds (per usage of indexer)

return str_value # type: ignore


class MetricsReleaseHealthBackend(ReleaseHealthBackend):
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: naming this just MetricsBackend should be enough given that it inherits from ReleaseHealthBackend so probably not necessary to include ReleaseHealth in every child class

previous_end: datetime,
rollup: int,
org_id: Optional[int] = None,
) -> ReleaseHealthBackend.CurrentAndPreviousCrashFreeRates:
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are doing this kind of type inheritance ? I would just move the types outside of the class.. Surely they will be used in other areas of the code that use this function

Copy link
Member

Choose a reason for hiding this comment

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

This has been fixed in master.

if org_id is None:
org_id = self._get_org_id(project_ids)

projects_crash_free_rate_dict: ReleaseHealthBackend.CurrentAndPreviousCrashFreeRates = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here as above comment


count_query = Query(
dataset=Dataset.Metrics.value,
match=Entity("metrics_counters"),
Copy link
Contributor

Choose a reason for hiding this comment

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

Would use an enum for metrics_counter.. this is error prone
instead
EntityKey.METRICS_COUNTER.value

rollup: int,
org_id: Optional[int] = None,
) -> ReleaseHealthBackend.CurrentAndPreviousCrashFreeRates:
return get_current_and_previous_crash_free_rates( # type: ignore
Copy link
Contributor

Choose a reason for hiding this comment

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

Defining a type for get_current_and_previous_crash_free_rates will prevent us from needing to add # type: ignore

@@ -1779,6 +1789,10 @@ def test_get_current_and_previous_crash_free_rates_with_zero_sessions(self):
}


class GetCrashFreeRateTestCaseMetrics(ReleaseHealthMetricsTestCase, GetCrashFreeRateTestCase):
"""Repeat tests with metrics backend"""

Copy link
Contributor

Choose a reason for hiding this comment

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

Please Add a ToDo comment

Copy link
Member Author

Choose a reason for hiding this comment

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

Deriving from both ReleaseHealthMetricsTestCase and GetCrashFreeRateTestCase already repeats the tests, so there is nothing to do here. But I agree that that should be clearer from reading the code.

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

Successfully merging this pull request may close these issues.

3 participants