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

Make tracker statistics optional again #102

Merged
merged 2 commits into from
Oct 21, 2022

Conversation

josecelano
Copy link
Member

Commit 7abe0f5 introduced an unwanted change. Thread for statistics is always created regardless configuration.

This commit reverts that change. The config option:

config.tracker_usage_statistics

defines wether the statistics should be enabled or not.

@josecelano josecelano linked an issue Oct 19, 2022 that may be closed by this pull request
@josecelano
Copy link
Member Author

ACK d24af95

@da2ce7
Copy link
Contributor

da2ce7 commented Oct 19, 2022

@josecelano

I find it confusing that calling run_worker() on the stats tracker will enable it, maybe register and enable worker or something would more clearly describe what is happening... What do you think?

Is it possible to write a test to check if the starts are being collected or not?

@josecelano
Copy link
Member Author

josecelano commented Oct 19, 2022

@josecelano

I find it confusing that calling run_worker() on the stats tracker will enable it, maybe register and enable worker or something would more clearly describe what is happening... What do you think?

Is it possible to write a test to check if the starts are being collected or not?

Hi @da2ce7 . Yes, I agree. I've already discussed it with @WarmBeer . I did not want to change anything in this PR, just to revert the bug asap.

Regarding testing, the test would depend on how we refactor it. I told @WarmBeer that I see two options:

  1. We could have a kind of NullStatsTracker (like /dev/null) that implements the TrackerStatsService trait but does nothing.
  2. Or we could make it optional in the Tracker constructor (Option<Box<dyn TrackerStatsService>>).

A more advanced approach could be implementing a middleware or observer pattern. The StatsTracker would be just a new plugin/service. I wanted to open a discussion about it, but I have not done yet.

For the time being, I would merge this PR and create the discussion.

We can rename the method. The method does:

  • Create a channel
  • Store the channel sender in the current struct so StatsTracker can be used to send events.
  • Create a new thread that is the listener.

Maybe we could call it activate_stats_event_listener or something like that.

We could also make small refactors before implementing a more generic approach. I think we could split the class into:

  • TrackerStatisticsService (TrackerStatisticsService::bootstrap -> (TrackerStatisticsEventSender, TrackerStatisticsEventListener))
  • TrackerStatisticsEventSender
  • TrackerStatisticsEventListener
  • TrackerStatisticsRepository

But I do not know if it makes sense or if we can go directly to a generic event/listener approach. The events could belong to the Tracker (core) domain, but you can register as many listeners as you want. One of them could be the StatsTracker.

I will elaborate on those ideas in the discussion.

@da2ce7
Copy link
Contributor

da2ce7 commented Oct 19, 2022

@josecelano What about adding a minimal unit test so that we can check that the stats are indeed being/not being collected?

@josecelano
Copy link
Member Author

@josecelano What about adding a minimal unit test so that we can check that the stats are indeed being/not being collected?

OK, I will try. To be honest, I also wanted to do it but, I thought that would be a big effort since we have to deal with concurrency :-). And I was focused on other things. But let's try at least. I think sooner or later we have to do that kind of test. Even if we change the implementation later, what we can learn it's going to be very useful in the future for other tests.

Commit 7abe0f5 introduced an unwanted
change. Thread for statistics is always created regardless
configuration.

This commit reverts that change. The config option:

config.tracker_usage_statistics

defines wether the statistics should be enabled or not.
@josecelano josecelano force-pushed the issue-97-optional-statistics branch from d24af95 to adee3b5 Compare October 19, 2022 14:00
@josecelano josecelano requested a review from da2ce7 October 19, 2022 17:32

let stats = active_stats_tracker.get_stats().await;

assert_eq!(stats.tcp4_connections_handled, 0);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we have the same assert:
assert_eq!(stats.tcp4_connections_handled, 0);
assert_eq!(stats.tcp4_connections_handled, 0);
for both the active, and the inactive tests?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hey @da2ce7 , It was just a copy/paste typo, but I realized the tests were passing after pushing the commit. I do not know why they pass (why the counter does not work). I'm working on that.

@josecelano josecelano marked this pull request as draft October 19, 2022 17:45
@josecelano
Copy link
Member Author

josecelano commented Oct 19, 2022

hey @da2ce7 I've added the test but as you can see the test for the active stats tracker should not pass because the counters are zero. I'm going to continue working on that.

My first idea was an integration or e2e test since the code to enable/disable stats is in the main.rs file. But then, the only way to get stats is by using the API. I can still do it that way, but I decided to move the code to the StatsTracker so that I can add this simpler unit test. The TorrustTracker also uses the configuration in its constructor.

Tracker statistics can be enabled or disabled using the configuration
option `tracker_usage_statistics`.

This commit adds tests for that behavior.
@josecelano josecelano force-pushed the issue-97-optional-statistics branch from 8e7d006 to e12d8e6 Compare October 20, 2022 16:08
Copy link
Contributor

@da2ce7 da2ce7 left a comment

Choose a reason for hiding this comment

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

Looks good to me :)

@josecelano
Copy link
Member Author

hi @da2ce7 @WarmBeer, I decided to change the testing strategy for this case. I was trying to write a test with the state-based style. I wanted the run the StatsTracker, send a message and assert that the stats change.

The problem with that approach is the worker is an infinite loop. The tests passed if I waited for the worker thread for milliseconds. You can do it this way:

#[tokio::test(flavor = "multi_thread")]
async fn my_test() {
    let stats_tracker = StatsTracker::new_instance(...);

    let result = stats_tracker.send_event(TrackerStatisticsEvent::Tcp4Announce).await;

    // Wait until the worker thread process the event
    println!(...) // A non-blocking wait

    assert!(...);
}

But I did not want to introduce a random wait because:

  • It makes tests slower.
  • Because it's not deterministic.
  • It's a dirty solution :-)

I came up with two ideas:

  1. Send an event to abort the loop and wait until the thread finishes.
  2. Pass an argument to the run_worker() with the number of events you want to process. After processing those events, the thread finished.

With these two solutions, you can wait until the worker ends processing all the events, and you can get the stats.

I did not like it because we change the behaviour only for testing, and we did not want to refactor just to cover this case with a test.

So, I started to think again about what I was trying to test. I think I could say I was:

"Trying to assert that statistics are collected correctly when they are enabled" and "statistics are not collected when they are disabled".

With the new test version, I only check whether the statistics are collected or not, but I do not test if they are updated correctly. Maybe we could add another test to assert that once the worker receives an event, it updates the statistics correctly. This way, we end up having two unit tests instead of one single test for both things. Of course, we are not testing if the event is sent correctly to the worker. But I suppose the goal for this test was the optional behavior. I mean, testing that we can collect or not the statistics with a configuration option.

Actually, we test that the StatTracker can be enabled/disabled from the constructor, but we are not testing that the main function passes the right value from the configuration. If we want to test that, we have the same "waiting" problem again or we can move the login in the main file to a "bootstrap" module. After the bootstrapping, we can check that StatTracker has the right configuration, which is "empty channel".

@josecelano josecelano marked this pull request as ready for review October 20, 2022 16:44
@da2ce7
Copy link
Contributor

da2ce7 commented Oct 20, 2022

ACK e12d8e6

@josecelano josecelano merged commit 97d3d8b into develop Oct 21, 2022
@josecelano josecelano mentioned this pull request Oct 21, 2022
5 tasks
da2ce7 added a commit that referenced this pull request Oct 27, 2022
6f77dfe fix: use only minor version for dependencies (Jose Celano)
9e49305 test: add tests for StatsTracker (Jose Celano)
0dd95e7 refactor: extract statistics event_handler (Jose Celano)
d3297cf refactor: extract StatsRepository (Jose Celano)
8874032 fix: weak tests (Jose Celano)
e570110 test: add new dev dependency mockall (Jose Celano)
bc3df5a fix typo (Jose Celano)
a2b16ff fix: tests using mock for old service (Jose Celano)
daec1fe refactor: extract stats event_listener (Jose Celano)
5b73d80 refactor: removed unused code and extract fn (Jose Celano)
720a584 refactor: use StatsEventSender to send events (Jose Celano)
b784442 refactor: inject new struct StatsEventSender into TorrentTracker (Jose Celano)
a334f17 refactor: extract struct StatsEventSender (Jose Celano)

Pull request description:

  This is an experimental refactor of the [StatsTracker](https://github.com/torrust/torrust-tracker/blob/develop/src/tracker/statistics.rs).

  It's still a WIP.

  Tracker statistics are optional, and we had [problems testing that behaviour](#102).

  I decided to try to make that code more testable. My idea is:

  - Convert the StatsTracker into a StatsService that only creates the communication channel and the other services (EventSender, EventListener, ...)
  - A EventSender to send the events.
  - A EventListener that updates the statistics.
  - A StatisticsRepository that contains the stats.

  The high-level service (StatsService) only creates the StatisticsRepository used by the EventListener and the TorrusTracker. And the communication channel (with the sender and listener).

  TODO:

  - [X] Extract EventSender
  - [X] Extract EventListener
  - [x] Extract StatisticsRepository
  - [x] Fix StatsEventSenderMock
  - [x] Add tests for EnventListener

  Once we have the EventListener, we can add tests to check if it updates the stats correctly, which is something we did not test on [PR-112](#102).

  While working on the refactor, I realized the `StatsEventSenderMock` does not work correctly. It only works when the event is sent, but it does not fail if no event is sent. The obvious solution would be to store the event sent in the Mock, but the method is not mutable.

  ```rust
      struct StatsEventSenderMock {
          expected_event: Option<TrackerStatisticsEvent>,
      }

      impl StatsEventSenderMock {
          fn new() -> Self {
              Self { expected_event: None }
          }

          fn should_throw_event(&mut self, expected_event: TrackerStatisticsEvent) {
              self.expected_event = Some(expected_event);
          }
      }

      #[async_trait]
      impl TrackerStatisticsEventSender for StatsEventSenderMock {
          async fn send_event(&self, event: TrackerStatisticsEvent) -> Option<Result<(), SendError<TrackerStatisticsEvent>>> {
              if self.expected_event.is_some() {
                  assert_eq!(event, *self.expected_event.as_ref().unwrap());
              }
              None
          }
      }

      #[tokio::test]
      async fn it_should_send_the_upd4_connect_event_when_a_client_tries_to_connect_using_a_ip4_socket_address() {
          let tracker_stats_service = Box::new(TrackerStatsServiceMock::new());
          let mut stats_event_sender = Box::new(StatsEventSenderMock::new());

          let client_socket_address = sample_ipv4_socket_address();
          stats_event_sender.should_throw_event(TrackerStatisticsEvent::Udp4Connect);

          let torrent_tracker =
              Arc::new(TorrentTracker::new(default_tracker_config(), tracker_stats_service, Some(stats_event_sender)).unwrap());
          handle_connect(client_socket_address, &sample_connect_request(), torrent_tracker)
              .await
              .unwrap();
      }

  ```

  I did not want to add a mocking library only for this case. That's why I did it manually. And I do not want to make that function mutable only for the test.

ACKs for top commit:
  da2ce7:
    ACK 6f77dfe

Tree-SHA512: 83f3c118b78bf0544cfa7381aa124c6f367ebacf1c699142d54b7b22449234410cb6e1ae8f41474398ec18a308d8294e598bbf00f0a07ae606bdc2fa47c73e09
@josecelano josecelano deleted the issue-97-optional-statistics branch January 16, 2023 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Statistics tracker should be optional again
2 participants