Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Strategies for dealing with legacy code

Avatar for Anay Anay
July 04, 2019

Strategies for dealing with legacy code

Talk from the DevDay meetup (devday.in) in Pune on 4th July 2019

Strategies:
Remove noise/ dead code
Inverted test pyramid
Characterisation tests
Record + Replay integration tests

Post rollout:
Release early, release often
Feature toggles
Runtime discovery via logs/metrics
Replaying production traffic
Track trend changes

Avatar for Anay

Anay

July 04, 2019

Other Decks in Programming

Transcript

  1. Kind of legacy projects I’ve run into • Product startup

    with a monolithic (java) codebase, serving decent traffic at 2000 req/sec. Started with <20% code coverage. 24x7 usage. • Fintech startup, python monolith, very low traffic. Most active for 12 hours. • Consulting firm, python monolith, internal users. All logic in frontend javascript with a CRUD backend.
  2. Legacy code - other definitions • Complex code without the

    necessary safety nets • source code inherited from someone else • Any code older than 5 hours.
  3. Why is it a problem? • Time to rollout new

    changes increases exponentially. • Difficult to onboard new team members. • Impossible to predict how long would something take to do. • Not solving it today only makes it worse for tomorrow.
  4. Fixing the broken windows Ratcheting - coined by Chris Stevenson

    A ratchet is defined as “a device that allows linear or rotary motion in only one direction, while preventing motion in the opposite direction.” Boy scout rule “Always leave the campground cleaner than you found it.”
  5. The Legacy Code Dilemma To refactor code, you first need

    to have sufficient tests. But to write tests, you need code to be structured properly.
  6. Read the manual • An understanding of clean code. •

    Sprout method/class. • Lean on the compiler, IDE refactorings.
  7. Remove noise: Find and destroy dead code • IDE support

    makes it easy* in some languages. • Use logs to identify endpoints which weren’t invoked in a sufficiently large time window. • Run coverage in production using tools like Scythe or coverband • Remove in smaller cycles
  8. E2E tests • Via postman/newman • Browser based tests using

    testcafe/selenium etc • Breadth over depth. Basic sanity / smoke
  9. Characterisation tests https://michaelfeathers.silvrback.com/characterization-testing public static String formatText(String text) { StringBuffer

    result = new StringBuffer(); for (int n = 0; n < text.length(); ++n) { int c = text.charAt(n); if (c == '<') { while(n < text.length() && text.charAt(n) != '/' && text.charAt(n) != '>') n++; if (n < text.length() && text.charAt(n) == '/') n+=4; else n++; } if (n < text.length()) result.append(text.charAt(n)); } return new String(result); }
  10. Characterisation tests • Use a coverage tool to validate you

    have enough tests before changing things. • These characterise what the system does in terms of behavior. • Provides a safety net against changes to characterised behavior.
  11. ApprovalTests • Benefits: ◦ Avoids the need for explicitly having

    multiple assertions. ◦ Helpful for writing assertions on untested controllers or any complex objects. • Things to watch for: ◦ Any changes to underlying fields leads to changes in the approval tests. ◦ Fields like current timestamp, auto-incrementing ids have to be excluded.
  12. Integration tests with Record and Replay @vcr.use_cassette("test_get_user_details.yml") def get_user_details(self): #fetches

    data from order, wallet and location services r = service.get_user_details('user-id-1') self.assertEqual(r.status_code, 200) data = r.json() self.assertEqual(len(data['user']["orders"]), 3) self.assertEqual(data["user"]["wallet"]['balance'], 200) self.assertEqual(data["user"]["location"], { 'ip': '4.4.4.4', 'country_code': 'US' })
  13. Integration tests with Record and Replay test_get_user_details.yml --- http_interactions: -

    request: method: get uri: http://wallet.service.com/user/balance?user_id=1 body: '' headers: {} response: status: code: 200 headers: Content-Length: - '438' body: '{...}' http_version: '1.1' - request: ...
  14. Integration tests with Record and Replay • Provide a mechanism

    to return pre-recorded HTTP responses for third party APIs based on some match criteria. • Libraries which provide this capability: vcrpy, mountebank, polly.js • Some libraries can be run as standalone replay proxies.
  15. Integration tests with Record and Replay • Benefits: ◦ Faster

    because responses aren’t fetched over the wire. ◦ non-availability of third party systems no longer affect your tests. ◦ Can validate multiple combinations of simulated responses. • Things to watch out for: ◦ Dynamic parameters in the request ◦ Authentication credentials being checked in ◦ Responses going out of sync over time.
  16. Release early, release often • Trunk based development ◦ Branch

    by abstraction • Rollout with every PR, helps identify the breaking change.
  17. Feature toggles • On-off switch. Most frameworks support based on

    environment variables, time to recovery is higher. Database is faster but it is still an on-off switch for all or none. • Frameworks to roll out based on roles, percentage rollouts, specific ip addresses or any other custom logic. E.g. togglz, launchdarkly • Lets you roll out changes gradually and measure impact. Relevant logging, metrics help in measuring implications. Change in trendline metrics.
  18. Feature toggles • Benefits: ◦ Quick rollbacks in case of

    issues. No need to deploy a different artifact to fix issues. ◦ Trunk based development is easier. • Things to watch out for: ◦ Backward compatibility of schema and user flows (equivalent to a rollback scenario) ◦ Percentage rollouts need to be hashed based on userId for a consistent experience ◦ Regression testing of functionality with ‘off’ state.
  19. Runtime discovery using logging/metrics • Add loggers/metrics. Get a better

    understanding of user defined parameters. ◦ • Enable verbose logging for a specific user id. Works great if you can’t have DEBUG logs enabled in production. •
  20. Replaying production traffic • Tools like goreplay, service proxies like

    envoy, ambassador provide mechanisms of replaying production traffic against non-prod instances. • Benefits: ◦ Doesn’t require change in infrastructure setup ◦ Can be done with percentage traffic • Things to watch out: ◦ Side effects. Non-GET requests. ◦ Comparing responses across both will have subtle differences such as timestamps etc
  21. Track changes in trends • Changes in number of error

    events per 5 min window. • Deployment annotations to identify when things changed
  22. Strategies • Remove noise/ dead code • Inverted test pyramid

    • Characterisation tests • Record + Replay integration tests • Release early, release often • Feature toggles • Runtime discovery via logs/metrics • Replaying production traffic • Track trend changes