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

Refactor exceptions phase 3 #585

Merged
merged 32 commits into from Dec 12, 2016
Merged

Refactor exceptions phase 3 #585

merged 32 commits into from Dec 12, 2016

Conversation

saneshark
Copy link
Contributor

This PR will make a lot of sweeping changes to Common::Exceptions, particularly with BackendServiceExceptions.

  1. All backends should try to use the raise_error middleware in some form, it will make errors consistent.
  2. The error returned will be a very generic 'Operation Failed' exception unless a unique mapping is provided in the common.exceptions.yml file
  3. All other exceptions will eventually be modified such that title, detail, and other attributes cannot be overridden with options. The unique minor code will map to that error unless it is a model validation error or the exception accepts arguments that are used to build the detail message.

The goal is to bring continuity across all errors rendered as part of vets.gov and have unique minor codes dilineating these errors.

More info and documentation to follow.

@saneshark
Copy link
Contributor Author

@patrickvinograd The changes I've made to BackendServiceException will make it so that the DSL you're currently relying on in multi_client will no longer work. Can we work together to come up with a good alternative?

@austin-martinez @markolson @carlmjohnson I'd like to get your feedback on this as well. Particularly, to learn what, if any, errors you return as part of a response. I would characterize anything that is not a 200 level response as an error. How are you rendering these and how can we change it so that it starts using Common::Exceptions.

@saneshark saneshark changed the title Refactor exceptions phase 3 [WIP] - Refactor exceptions phase 3 Dec 1, 2016
@austin-martinez
Copy link
Contributor

For claim status/EVSS, some errors are returned to vets-api as a 200 response with errors in the JSON body. We are currently subclassing StandardError and raising the error in lib/evss/error_middleware. This would be a good candidate to switching to a Common::Exception. Timeouts/breakers exceptions are rescued by the client and data is pulled from the DB if present, the error is not re-raised. All other connection/application errors are not caught specifically by any of our controllers/clients and are handled in the ApplicationController

@saneshark saneshark mentioned this pull request Dec 1, 2016
@saneshark
Copy link
Contributor Author

saneshark commented Dec 1, 2016

@austin-martinez I'm looking at your error middleware you guys use, and I see what you mean now. Unless the payload contains a success it is an error, I wonder if maybe we can have a middleware that coerces the env body and env headers -- essentially normalizes errors and success to be more http spec like, and then use the raise_error middleware I've done here.

I can think of some alternatives but I like this approach the best. Curious to hear your thoughts on making this change. In either case it can be done as a separate PR.

@patrickvinograd
Copy link
Contributor

The ArcGIS REST API has the same thing - sometimes returns a 200 with an "error" JSON object. I don't know that you're going to be able to catch all the variants in one middleware so part of each backend service integration is dealing with those - and then we just need some contract about how to raise them up the chain - either coerce the response env body/headers or just raising a certain type of exception that is caught/coerced into a BackendServiceException.

@saneshark
Copy link
Contributor Author

saneshark commented Dec 1, 2016

@patrickvinograd that's true, but i think we could have an intermediary middleware, one that occurs before the raise_error middleware that coerces it into a more "standardized" format that raise_error can work with.

I've tried to make raise_error be dynamic enough to handle anything we throw at it, i can easily add something to it where it also checks for status[200] and success == false

The main thing though is, raise_error is designed for errors we present to an api consumer. If the errors are caught and other errors raised that would be more or equally representative to a user as to what happens that's fine... but so long as they conform to JSONAPI and have unique code for each distinct message returned.

@saneshark
Copy link
Contributor Author

@aub there is more of this to come, but i think this touches enough places to warrant its own individual code review and I'll continue doing more of the same for the internal error messages.

Based on the comment thread, I think eventually, I will look to replace the functionality I have now for the raise_error middleware such that it need only accept argument for the Code prefix, and everything else would be handled in a normalization middleware that is used before this one.

@saneshark saneshark changed the title [WIP] - Refactor exceptions phase 3 Refactor exceptions phase 3 Dec 2, 2016
Copy link
Contributor

@aub aub left a comment

Choose a reason for hiding this comment

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

Ok, here's a first pass. Overall looks really solid. I'm with you on the concern over how to get it deployed and am happy to discuss sometime.

client.post_create_message_reply_with_attachment(params[:id], create_message_params)
else
client.post_create_message_reply(params[:id], message_params)
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh man do I hate this rubocop rule. You cannot convince me that this change makes the code look better. How about roll this back and add the removal of the rubocop rule to the commit? Here's the .rubocop.yml code I have in provider screening:

# This makes you do things like this:
# variable = if test
#   'abc-123'
# else
#   'def-456'
# end
#
# I think this is harder to read than assigning the variable within the conditional.
Style/ConditionalAssignment:
  Enabled: false

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd argue the former is more readable, depends what you're used to 😄 I think it's more Ruby-ish (Lisp/Smalltalk influences) and avoids duplication.

Copy link
Contributor

Choose a reason for hiding this comment

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

The thing that bugs me the most is the crazy indentation, which is also required because of another rubocop rule. The actual assignment of the variable is less of a problem, but I'd argue that assigning the variable outside of the if like this makes it jarring to read because we're used to the if beginning on the left side of the line. Ultimately, I just can't think of a good reason for rubocop to enforce this style.

code(env)
else
Rails.logger.warn "The following exception should be added to locales: \n #{response_values(env)}"
'backend_service_exception'
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we'll catch this happening... almost seems like it should raise a different error type under these circumstances so that we know there's an error type we're not handling. Looks like as it is, it'll default to 'backend_service_exception', which is fine. But I bet we want some way to ensure we're seeing these.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a way to use Raven when you don't have an actual exception? Just a warning.

Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like you can pass it an exception without raising it, so I think you could do something like this:

exc = UnhandledBackendServiceExceptionException(blah, blah)
Raven.capture_exception(exc)

if response_values[:status]&.between?(400, 999)
@response_values = response_values
else
raise NotImplementedError, 'only invoke from raise_error middleware'
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not following how this error message lines up with the if test. If the status code isn't in the 400-999 range, does it imply that this was invoked from the wrong place?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea, this is kind of a dumb heuristic. It would be a violation of OOP to reference the caller, don't want to do that. Maybe I can make the error message be something else. The idea here is though, that one shouldn't mess with raising this exception outside of the middleware, but I can't think of a good way of enforcing that. Maybe just being deligent in code review. Not sure it's even necessary as long as its done correctly -- maybe i'll remove this check entirely.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I think I'd put back the comment about not invoking it directly and remove the test.

# then 400 explicitly it will default to 400. IT WILL NOT DEFAULT to whatever
# was provided by the backend service, because the backend service response
# might not always be rele
def _status
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the underscored method names? Doesn't it suffice that these are private?

Copy link
Contributor Author

@saneshark saneshark Dec 5, 2016

Choose a reason for hiding this comment

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

Yea, I have seen the underscore pattern used before when there is a semantic difference in meaning to what the class is designed to do with something having the same name. I could just remove the underscore and keep them private. My syntax highlighting in atom also makes it think as though "source" is a reserved word. Which is mildly annoying.

status: env.status.to_i,
detail: env[:body][detail_key],
code: code(env),
source: env[:body][source_key]
Copy link
Contributor

Choose a reason for hiding this comment

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

I bet this is fine for now, but these lines using `env[:body][{source_key,detail_key,code_key}]``` do make an assumption that the response body contains these things as easily accessible attributes. Is this always the case? I think something was mentioned about some services that return a 200 and then have the error status buried in a JSON response. Might be worth a thought as to how we'd handle that case here. Presumably you'd have to either subclass the middleware or pass it procs that can pull out the keys based on the env.

Copy link
Contributor Author

@saneshark saneshark Dec 6, 2016

Choose a reason for hiding this comment

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

No, and you're right. I think a better strategy would be to have preliminary middleware before the raise_error that normalizes these and then the raise_error middleware just accepts argument for the prefix, 'Rx' or 'SM' I thought about introspecting somehow to get this as well, but couldn't think of a good way to do it.

MESSAGE
Rails.logger.warn message
exception = UnmappedBackendServiceException.new(message)
Raven.capture_exception(exception) if ENV['SENTRY_DSN'].present?
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this use capture_message instead?
https://docs.sentry.io/clients/ruby/usage/#reporting-messages

@saneshark
Copy link
Contributor Author

saneshark commented Dec 9, 2016

Ideally this PR will be merged to dev and staging + production will cherry pick all the other PRs that have been merged with the exception of this one. I will make sure to squash and merge to make this easier.

That way I can test it out on dev to make sure there are not any issues before we push to prod. Thoughts? CC: @patrickvinograd @jkassemi @aub

I'll write up a test plan for this as well prior to merging.

@aub
Copy link
Contributor

aub commented Dec 9, 2016

Yikes, that seems like it would be pretty difficult to manage. So what I'd need to do at deploy time is create a new branch, find the list of commits in master but not in production, then cherry-pick all of those commits into the new branch, except for this one?

And aside from that complexity, I'm concerned about the fact that the code in dev won't be consistent with what goes to production, which could cause problems.

I think we should take a step back and think about what our goal is here. If this code needs to sit in a non-production, deployed environment for a while, how long would that last? What would we be testing by doing so? Is there a simpler way to achieve the same?

@saneshark
Copy link
Contributor Author

saneshark commented Dec 9, 2016

Hmmm, good point.

I think there will be situations that will arise where we need Staging or Dev just to test critical patches to production that need to go out. And that everything else would happen during an EOW iteration of some sort. I think this would be something that would be a good candidate for EOW. Perhaps it would get pushed early in the week to dev or staging and by the EOW if everything checks out then we're good to merge. But I think having a dev or staging setup just for the daily patches then is ideal.

In Agile parlance, I would characterize these changes as part of an Epic, and each of the individual parts as User Stories. I think a collection of User Stories deserve their own iteration / release schedule and QA process independent of daily critical patches we make to production.

It seems right now, we're just merging stuff and then addressing issues that arise with critical patches after the fact because we don't really have a formal QA process or iteration -> deployment strategy in place.

Realistically though, I should be able to spend a few hours playing around with this locally testing, and maybe we can just merge straight to production. Worst case scenario I'm around to push a quick patch or fix, or we can roll it back. Thoughts?

@aub
Copy link
Contributor

aub commented Dec 12, 2016

Ok, I think this is good to get merged. Plan is to drop it into dev/staging today, then do some extensive testing tonight and tomorrow before the deploy.

@saneshark saneshark merged commit 1019187 into master Dec 12, 2016
@saneshark saneshark deleted the refactor_exceptions_phase_3 branch December 12, 2016 21:18
@annaswims annaswims mentioned this pull request Nov 15, 2018
5 tasks
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.

None yet

5 participants