Tagged: dependency Toggle Comment Threads | Keyboard Shortcuts

  • kmitov 4:19 pm on June 13, 2021 Permalink |
    Tags: dependency, , ,   

    Dependencies – one more variable adding to the “cost of the code” 

    One thing I have to explain a lot is what are the costs of software development. Why are things taking so long? Why is there any needed for maintenance and support? Why are developers spending significant amount of their time looking over the existing code base and why we can not just add the next and the next feature?

    Today I have an example of this – and these are “dependencies”.

    The goal of this article is to give people more understanding on how the “tech works.”. I’ve seen that every line of code and every dependency that we add to a project will inevitably result in further costs down the road so we should really keep free of unnecessary dependencies and features.

    Daily builds

    Many contemporary professional software projects have a daily build. This means that every day at least once the project is “built” from zero, all the tests are run and we automatically validate that the customers could use it.

    Weekly dependencies updates

    Every software project depends on libraries that implement common functionality and features. Having few dependencies is healthy for the project, but having no dependencies and implementing everything on your own is just not viable in today’s world.

    These libraries and frameworks that we depend on also regularly release new versions.

    My general rule that I follow in every project is that we check for new versions of the dependencies every Wednesday at around 08:00 in the morning. We check for new dependencies, we download them, we build the project and we run the specs/tests. If the tests fail this means that the new dependencies that we’ve downloaded have somehow changed the behavior of the project.

    Dependencies change

    Most of the time dependencies are changed in a way that does not break any of the functionality of your project. This week was not such a week. A new dependency came along and it broke a few of the projects.

    The problem came from a change in two dependencies:

    Fetching websocket-driver 0.7.5 (was 0.7.4)
    Fetching mustache-js-rails 4.2.0.1 (was 4.1.0)
    Installing mustache-js-rails 4.2.0.1 (was 4.1.0)
    Installing websocket-driver 0.7.5 (was 0.7.4) with native extensions
    

    We have installed new versions of two of the dependencies “websocket-driver” and “mustache-js-rails’

    These two dependencies broke the builds.

    Why should we keep up to date

    Now out of the blue we should resolve this problem. This takes time. Sometimes it is 5 minutes. Sometimes it could be an hour or two. If we don’t do it, it will probably result in more time at a later stage. As the change is new in ‘mustache-js-rails’ we have the chance to get in touch with the developers of the library and resolve the issue while it is fresh for them and they are still “in the context” of what they were doing.

    Given the large number of dependencies that each software project has there is a constant need to keep up to date with new recent versions of your dependencies.

    What if we don’t keep up to date?

    I have one such platform. We decided 6-7 years ago not to invest any further in it. It is still working but it is completely out of date. Any new development will cost the same as basically developing the platform as brand new. That’s the drawback of not keeping up to date. And it happens even with larger systems on a state level with the famous search for COBOL developers because a state did not invest in keeping their platform up to date for some 30+ years.

     
  • kmitov 6:44 am on March 25, 2021 Permalink |
    Tags: dependency, ,   

    A practical example for dependency injection 

    In today’s article I am sharing with our team an example I found while reading parts of the code in one of our platforms where we’ve created a form of ‘cyclic’ references with cancancan merges in Rails Controllers. I wondered for a while how we’ve managed to create it, how to avoid creating them in the future and I share this as an example with our team, but I hope the article could be useful for the whole community.

    I see this as a perfect example for the use of Dependency Injection.

    The example

    CourseSectionsAbilities is merged with ContentsAbility which is merged with ContentRefsAbilities which is then merged with CourseSectionAbilities. This means that CourseSectionsAbilities is merged twice and the second merge overrides the first merge.

    class CourseSectionsController < CommonController
    
      def current_ability
        # This is the first merge 
        # CoruseSectionsAbility is merged with ContentsAbility.
        @current_ability ||= Abilities::CourseSectionsAbility.new(current_user)
                               .merge(Abilities::ContentsAbility.new(current_user))
      end
    
    end
    
    module Abilities
      class ContentsAbility
        include CanCan::Ability
    
        def initialize user
          ...
          # This is the second merge
          # ContentsAbility is merged with ContentRefs ability
          merge ContentRefsAbility.new user
        end
      end
    end
    
    module Abilities
    
      class ContentRefsAbility
        include CanCan::Ability
    
        def initialize user
          # This is the third merge
          # ContentRefsAbility is merged with CourseSectionsAbilily.
          # This overrides the first merge.
          merge CourseSectionsAbility.new(user)
          ...
        end
      end
    end

    How did it happen?

    It think it is the classic grow of the code where you solve the problem in the easiest way possible. Initially we had:

    class CourseSectionsController < CommonController
    
      def current_ability
        # This is the first merge 
        # CoruseSectionsAbility is merged with ContentsAbility.
        @current_ability ||= Abilities::CourseSectionsAbility.new(current_user)
                               .merge(Abilities::ContentsAbility.new(current_user))
      end
    
    end

    But then a new requirement about an year after that has come and there is a commit that adds the second merge:

    module Abilities
    
      class ContentRefsAbility
        include CanCan::Ability
    
        def initialize user
          # This is the third merge
          # ContentRefsAbility is merged with CourseSectionsAbilily.
          # This overrides the first merge.
          merge CourseSectionsAbility.new(user)
          ...
        end
      end
    end

    What is the problem?

    The problem is that there is a class called ContentRefsAbility that can dependent on everything it wants. It can merge anything inside it with any consideration on what was already merged. It can set it’s own dependencies. This couples the ContentRefsAbility with the places it is used. Because we must take into consideration every place where ContentRefsAbility is used before changing it’s implementation.

    How Dependency Injection solves this?

    We pass the ability in the constructor

    module Abilities
    
      class ContentRefsAbility
        include CanCan::Ability
    
        def initialize user, outside_ability
          # This is the third merge
          # ContentRefsAbility is merged with CourseSectionsAbilily.
          # This overrides the first merge.
          outside_ability.method1 # we call the method of the outside_ability
          ...
        end
      end
    end

    Instead of creating the ability in the class we pass the dependency from the outside. In this way we can control the dependency and choose different dependencies in different conditions.

    The ContentRefsAbility no longer depends on the specific implementation of outside ability, but it depends on the behavior we inject from the outside.

     
  • kmitov 5:23 pm on May 21, 2020 Permalink |
    Tags: , dependency, , , , , ,   

    With assets in a rails engine it could be Gemfile vs gemspec dependency that is messing it up 

    You have a rails engine. It has an asset (like a scss file). You include this engine in another engine. How do you test that the assets is correctly resolved?

    There is a moment when you simply can not understand where and how the asset is resolved/not resolved. The solution actually is in what are the Gemfile and .gemspec for a rails engine. Both of them could describe dependencies, but they contain different things.

    The whole premise of the situation seems strange until you get to building a rails engine that would contain the “theme” of your app. We go in this situation with two of our platforms – FLLCasts.com and 3DAssemblyInstructions.com. We wanted to have different layouts in different gems that are providing different look an feel- separate the main layout from dashboard and from main. We also wanted to have this layouts in a different gems so that we could release them with different versions and test them as separate gems in separate builds.

    Let’s get into this step by step tutorial and by the end I am sure you would know much about how assets are resolved, packed and testes when they are within a rails. Again I am writing this tutorial to help spread the knowledge internally, but the problem is so common that it might be of interest to the community as a whole.

    Case 1 – simple app with an asset

    Just to get the understanding correctly and to have a base we would create a simple rails app with an asset and start it. I am using rails 6 and ruby 2.6.5

    $ rails new simple_assets_app
    $ cd simple_asset_app
    # Create a new asset.scss file with some content
    $ echo "asset_style { background-color: white}" > app/assets/stylesheets/asset.scss
    $ rails s
    => Booting Puma
    => Rails 6.0.3.1 application starting in development 
    => Run `rails server --help` for more startup options
    Puma starting in single mode...
    * Version 4.3.5 (ruby 2.6.5-p114), codename: Mysterious Traveller
    * Min threads: 5, max threads: 5
    * Environment: development
    * Listening on tcp://127.0.0.1:3000
    * Listening on tcp://[::1]:3000
    Use Ctrl-C to stop
    
    

    In another terminal request the asset

    # Request the asset and dislay its content. Everything works. Perfect
    $ curl localhost:3000/assets/asset.scss
    asset_style { background-color: white}

    Case 2 – engine with an asset

    Now let’s build a rails plugin that would hold this asset

    $ rails plugin new simple_asset_engine --full
    $ cd simple_asset_engine/
    $ echo "asset_style { background-color: white}" > app/assets/stylesheets/asset.scss
    # You have to fix the TODOs in the gemspec. After you are ready do 
    $ rails s -p 3001
    => Booting WEBrick
    => Rails 6.0.3.1 application starting in development http://localhost:3001
    => Run `rails server --help` for more startup options
    [2020-05-21 19:48:51] INFO  WEBrick 1.4.2
    [2020-05-21 19:48:51] INFO  ruby 2.6.5 (2019-10-01) [x86_64-linux]
    [2020-05-21 19:48:51] INFO  WEBrick::HTTPServer#start: pid=13394 port=3001
    

    Now that the port is different.

    Request the asset

    # Nothing strange here. Asset is delivered as expected.
    $ curl localhost:3001/assets/asset.scss
    asset_style { background-color: white}
    

    Conclusion – engine, no engine the asset is delivered.

    Case 3 – require simple_asset_engine in another engine

    This could be a requirement. It has happen to us. It is a valid case. It is rather strange to have one engine require another, but we are all grown ups here…

    $ rails plugin new host_engine --full
    # Fix todos in .gemspec
    
    # Add a dependency to simple_asset_engine. Asset should be found, rigth
    $ echo 'gem "simple_asset_engine", path: "~/axles/tmp/simple_asset_engine"' >> Gemfile
    
    # install gems - mainly the simple_asset_engine
    $ bundle install
    
    # Start server. Note the different port
    $ rails s -p 4010
    
    

    Request asset

    $ curl localhost:4010/assets/asset.scss
    asset_style { background-color: white}

    Asset is found.

    That’s the power of rails. It just works. You can have an asset in an app, asset in an engine, asset in an engine, required in another engine and the asset is always found. Except…

    This might sound as a conclusion, because everything works. Until you get to production, which is actually the only time it matters if anything works, but that’s another story.

    Our current host_engine depends on a gem with a local path in the local filesystem.

    host_engine: $ cat Gemfile
    source 'https://rubygems.org'
    git_source(:github) { |repo| "https://github.com/#{repo}.git" }
    
    # Declare your gem's dependencies in host_engine.gemspec.
    # Bundler will treat runtime dependencies like base dependencies, and
    # development dependencies will be added by default to the :development group.
    gemspec
    
    # Declare any dependencies that are still in development here instead of in
    # your gemspec. These might include edge Rails or gems from your path or
    # Git. Remember to move these dependencies to your gemspec before releasing
    # your gem to rubygems.org.
    
    # To use a debugger
    # gem 'byebug', group: [:development, :test]
    gem "simple_asset_engine", path: "~/axles/tmp/simple_asset_engine"

    This is something that we can not ship to production. That’s why we pack “simple_asset_engine” as a gem and place this gem in a gemrepo. This is not part of the tutorial so I will simulate it.

    We should also modify the host_engine.gemspec to depend on simple_asset_engine. We add a dependency.

    # in host_engine.gemspec we add
    
    spec.add_dependency "simple_asset_engine"
    

    This means – ‘add a dependency to “simple_asset_engine”‘. Our gem will host_engine will depend on ‘simple_asset_engine’, but and here is a but, but only for ‘production’ environment. So this means – “production”. To simulate the fact that we don’t have this gem in a repo we would change the dependency in the Gemfile to be only for production.

    # This means the same as spec.add_dependency 'simple_asset_engine' It add the gem as a dependency to production. We are doing it like this to simulate that we have 'spec.add_dependency' and the gem is coming from a gems repo
    
    gem "simple_asset_engine", group: [:production], path: "~/axles/tmp/simple_asset_engine"

    Now as you do request the asset a strange error occurs:

    $ curl localhost:4010/assets/asset.scss
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="utf-8" />
      <title>Action Controller: Exception caught</title>
        ....
       <div id="Framework-Trace-0" style="display: none;">
          <code style="font-size: 11px;">
              <a class="trace-frames trace-frames-0" data-exception-object-id="47236752082100" data-frame-id="0" href="#">
                actionpack (6.0.3.1) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call'
              </a>
              <br>
              <a class="trace-frames trace-frames-0" data-exception-object-id="47236752082100" data-frame-id="1" href="#">
                actionpack (6.0.3.1) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
              </a>
              <br>
              <a class="trace-frames trace-frames-0" data-exception-object-id="47236752082100" data-frame-id="2" href="#">
                railties (6.0.3.1) lib/rails/rack/logger.rb:37:in `call_app'
              </a>
    ...
    </div>
    </body>
    </html>
    

    In the log you see

    ActionController::RoutingError (No route matches [GET] "/assets/asset.scss"):
      
    actionpack (6.0.3.1) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call'
    actionpack (6.0.3.1) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
    railties (6.0.3.1) lib/rails/rack/logger.rb:37:in `call_app'
    railties (6.0.3.1) lib/rails/rack/logger.rb:26:in `block in call'
    activesupport (6.0.3.1) lib/active_support/tagged_logging.rb:80:in `block in tagged'
    activesupport (6.0.3.1) lib/active_support/tagged_logging.rb:28:in `tagged'
    activesupport (6.0.3.1) lib/active_support/tagged_logging.rb:80:in `tagged'
    railties (6.0.3.1) lib/rails/rack/logger.rb:26:in `call'
    sprockets-rails (3.2.1) lib/sprockets/rails/quiet_assets.rb:11:in `block in call'
    activesupport (6.0.3.1) lib/active_support/logger_silence.rb:36:in `silence'
    activesupport (6.0.3.1) lib/active_support/logger.rb:64:in `block (3 levels) in broadcast'
    activesupport (6.0.3.1) lib/active_support/logger_silence.rb:36:in `silence'
    activesupport (6.0.3.1) lib/active_support/logger.rb:62:in `block (2 levels) in broadcast'
    sprockets-rails (3.2.1) lib/sprockets/rails/quiet_assets.rb:11:in `call'
    actionpack (6.0.3.1) lib/action_dispatch/middleware/remote_ip.rb:81:in `call'
    actionpack (6.0.3.1) lib/action_dispatch/middleware/request_id.rb:27:in `call'
    rack (2.2.2) lib/rack/method_override.rb:24:in `call'
    rack (2.2.2) lib/rack/runtime.rb:22:in `call'
    activesupport (6.0.3.1) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'
    actionpack (6.0.3.1) lib/action_dispatch/middleware/executor.rb:14:in `call'
    actionpack (6.0.3.1) lib/action_dispatch/middleware/static.rb:126:in `call'
    rack (2.2.2) lib/rack/sendfile.rb:110:in `call'
    actionpack (6.0.3.1) lib/action_dispatch/middleware/host_authorization.rb:82:in `call'
    railties (6.0.3.1) lib/rails/engine.rb:527:in `call'
    rack (2.2.2) lib/rack/handler/webrick.rb:95:in `service'
    

    It is basically telling us that an error occurred. An error also should occur. It is logical, it is the right thing to do, but it can take hours to debug and understand this.

    The error occurs because we are starting the rails server in ‘development’ environment and the dependency to ‘simple_asset_engine’ is only for production.

    Case 3.1 – change to production and development

    # Change host_engine/Gemfile to have
    
    gem "simple_asset_engine", group: [:production, :development], path: "~/axles/tmp/simple_asset_engine"
    

    Restart server on port 4010.

    host_engine: $ rails s -p 4010
    => Booting WEBrick
    => Rails 6.0.3.1 application starting in development http://localhost:4010
    => Run `rails server --help` for more startup options
    [2020-05-21 20:14:55] INFO  WEBrick 1.4.2
    [2020-05-21 20:14:55] INFO  ruby 2.6.5 (2019-10-01) [x86_64-linux]
    [2020-05-21 20:14:55] INFO  WEBrick::HTTPServer#start: pid=14167 port=4010

    Request the asset

    $ curl localhost:4010/assets/asset.scss
    asset_style { background-color: white}

    Now it is time for conclusion

    spec.add_dependency in a gemspec gives you a dependency for production. There is a ‘spec.add_development_dependency’ that exists, but there are great discussion about it here https://github.com/rubygems/rubygems/issues/1104 Read more there. Really. Read more.

    But as we are trying to test assets separately in an engine it is important to understand what Gemfile and gemspec could be used for. If an asset is not found from a dependency of a gemspec, probably the whole dependency is required for a different environment. In the same time if the dependency contains only assets, it might be difficult to get what is happening at first sight and is it your fault, or that the F..k is sprockets doing or event what the bigger f..k is webpacker doing. But for this case it is just plain old ruby dependencies that are messing us up.

     
c
compose new post
j
next post/next comment
k
previous post/previous comment
r
reply
e
edit
o
show/hide comments
t
go to top
l
go to login
h
show/hide help
shift + esc
cancel