Skip to content

Commit

Permalink
Docs: defining Akka.Analyzer rules (akkadotnet#7039)
Browse files Browse the repository at this point in the history
* defining Akka.Analyzer rules

These will be shown from the tooltips inside Visual Studio whenever an error is flagged.

* attempting to debug Akka.Persistence.TestKit.Tests

* Revert "attempting to debug Akka.Persistence.TestKit.Tests"

This reverts commit df57633.

* added AK1001.md page

* added `AK2000`

* added links to all rules to index

* added TOC for rules

* fixed markdown linting issues
  • Loading branch information
Aaronontheweb committed Jan 4, 2024
1 parent 0486f97 commit a2c0df4
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 1 deletion.
2 changes: 1 addition & 1 deletion docs/articles/actors/dependency-injection.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
uid: dependency-injection
title: Dependency injection
title: Dependency Injection Support in Akka.NET
---
# Dependency Injection

Expand Down
18 changes: 18 additions & 0 deletions docs/articles/debugging/akka-analyzers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
uid: akka-analyzers
title: Akka.Analyzers - Roslyn Analyzers and Code Fixes for Akka.NET
---

# Akka.Analyzers

As of Akka.NET v1.5.15 we now include [Akka.Analyzers](https://github.com/akkadotnet/akka.analyzers) as a package dependency for the core Akka library, which means any projects that reference anything depending on [`Akka`](https://www.nuget.org/packages/Akka) will automatically pull in all of Akka.Analyzer's rules and code fixes.

Akka.Analyzer is a [Roslyn Analysis and Code Fix](https://learn.microsoft.com/en-us/visualstudio/extensibility/getting-started-with-roslyn-analyzers) package, which means that it leverages the .NET compiler platform ("Roslyn") to detect Akka.NET-specific anti-patterns and problems during _compilation_, rather than at run-time.

## Supported Rules

| Id | Title | Severity | Category |
|--------|--------------------------------------------------------|----------|--------------|
| [AK1000](xref:AK1000) | Do not use `new` to create actors. | Error | Actor Design |
| [AK1001](xref:AK1001) | Should always close over `Sender` when using `PipeTo`. | Error | Actor Design |
| [AK2000](xref:AK2000) | Do not use `Ask` with `TimeSpan.Zero` for timeout. | Error | API Usage |
66 changes: 66 additions & 0 deletions docs/articles/debugging/rules/AK1000.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
uid: AK1000
title: Akka.Analyzers Rule AK1000 - "Do not use `new` to create actors"
---

# AK1000 - Error

Do not use the `new` keyword to create Akka.NET actors.

## Cause

A class inheriting from [`Akka.Actor.ActorBase`](xref:Akka.Actor.ActorBase) or one of its descendants was instantiated directly via the `new` keyword, which is illegal. Actors can only be instantiated inside a [`Props.Create` method](xref:Akka.Actor.Props) or via an [`IIndirectActorProducer`](xref:Akka.Actor.IIndirectActorProducer) implementation (such as [`Akka.DependencyInjection`](xref:dependency-injection).)

An example:

```csharp
using Akka.Actor;

class MyActor : ActorBase {
protected override bool Receive(object message) {
return true;
}
}

class Test
{
void Method()
{
MyActor actorInstance = new MyActor(); // not supported by Akka.NET
}
}
```

## Resolution

The correct way to get a reference back to an actor is to wrap your actor's constructor call inside a `Props.Create` method and then pass that into either [`ActorSystem.ActorOf`](xref:Akka.Actor.ActorSystem) (when creating a top-level actor) or [`Context.ActorOf`](xref:Akka.Actor.IActorRefFactory#Akka_Actor_IActorRefFactory_ActorOf_Akka_Actor_Props_System_String_) (when creating a child actor.)

Here's an example below:

```csharp
using Akka.Actor;

class MyActor : ReceiveActor {
private readonly string _name;
private readonly int _myVar;

public MyActor(string name, int myVar){
_name = name;
_myVar = myVar;
ReceiveAny(_ => {
Sender.Tell(_name + _myVar);
});
}
}

class Test
{
void Method()
{
// obviously, don't create a new ActorSystem every time - this is just an example.
ActorSystem sys = ActorSystem.Create("MySys");
Akka.Actor.Props props = Akka.Actor.Props.Create(() => new MyActor("foo", 1));
IActorRef realActorInstance = sys.ActorOf(props);
}
}
```
60 changes: 60 additions & 0 deletions docs/articles/debugging/rules/AK1001.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
uid: AK1001
title: Akka.Analyzers Rule AK1001 - "Should always close over `Sender` when using `PipeTo`"
---

# AK1000 - Error

You should always close over [`Context.Sender`](xref:Akka.Actor.IActorContext#Akka_Actor_IActorContext_Sender) when using [`PipeTo`](xref:Akka.Actor.PipeToSupport#Akka_Actor_PipeToSupport_PipeTo_System_Threading_Tasks_Task_Akka_Actor_ICanTell_Akka_Actor_IActorRef_System_Func_System_Object__System_Func_System_Exception_System_Object__)

## Cause

When using `PipeTo`, you must always close over `Sender` to ensure that the actor's `Sender` property is captured at the time you're scheduling the `PipeTo`, as this value may change asynchronously.

This is a concurrent programming problem: `PipeTo` will be evaluated and executed at some point in the future because it's an asynchronous continuation, therefore the `Context.Sender` property, which is _mutable and changes each time the original actor processes a message_, may change.

An example:

```csharp
using Akka.Actor;
using System.Threading.Tasks;
using System;

public sealed class MyActor : UntypedActor{

protected override void OnReceive(object message){
async Task<int> LocalFunction(){
await Task.Delay(10);
return message.ToString().Length;
}

// potentially unsafe use of Context.Sender
LocalFunction().PipeTo(Sender);
}
}
```

## Resolution

To avoid this entire category of problem, we should close over the `Context.Sender` property in a local variable.

Here's an example below:

```csharp
using Akka.Actor;
using System.Threading.Tasks;
using System;

public sealed class MyActor : UntypedActor{

protected override void OnReceive(object message){
async Task<int> LocalFunction(){
await Task.Delay(10);
return message.ToString().Length;
}

var sender = Sender;
LocalFunction().PipeTo(sender);
}
}
```
47 changes: 47 additions & 0 deletions docs/articles/debugging/rules/AK2000.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
uid: AK2000
title: Akka.Analyzers Rule AK2000 - "Do not use `Ask` with `TimeSpan.Zero` for timeout."
---

# AK2000 - Error

Do not use [`Ask<T>`](xref:Akka.Actor.Futures#Akka_Actor_Futures_Ask__1_Akka_Actor_ICanTell_System_Object_System_Nullable_System_TimeSpan__) or [`Ask`](xref:Akka.Actor.Futures#Akka_Actor_Futures_Ask_Akka_Actor_ICanTell_System_Object_System_Nullable_System_TimeSpan__) with `TimeSpan.Zero` for timeout.

## Cause

When using `Ask`, you must always specify a timeout value greater than `TimeSpan.Zero` otherwise the process might deadlock. See [https://github.com/akkadotnet/akka.net/issues/6131](https://github.com/akkadotnet/akka.net/issues/6131) for details.

> [!IMPORTANT]
> This rule is not exhaustive - Roslyn can't scan every possible variable value at compilation time, so it's still possible to pass in a `TimeSpan.Zero` value even with this rule present.
An example:

```csharp
using Akka.Actor;
using System.Threading.Tasks;
using System;

public static class MyActorCaller{
public static Task<string> Call(IActorRef actor){
return actor.Ask<string>(""hello"", TimeSpan.Zero);
}
}
```

## Resolution

The right way to fix this issue is to pass in a non-zero value or to use the `Ask<T>` overload that accepts a `CancellationToken`.

Here's an example below:

```csharp
using Akka.Actor;
using System.Threading.Tasks;
using System;

public static class MyActorCaller{
public static Task<string> Call(IActorRef actor){
return actor.Ask<string>(""hello"", TimeSpan.FromSeconds(1));
}
}
```
6 changes: 6 additions & 0 deletions docs/articles/debugging/rules/toc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- name: AK1000
href: AK1000.md
- name: AK1001
href: AK1001.md
- name: AK2000
href: AK2000.md
4 changes: 4 additions & 0 deletions docs/articles/debugging/toc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- name: Akka.NET Roslyn Analyzers
href: akka-analyzers.md
- name: Analysis Rules
href: rules/toc.yml
2 changes: 2 additions & 0 deletions docs/articles/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@
href: configuration/toc.yml
- name: Serialization
href: serialization/toc.yml
- name: Debugging Akka.NET
href: debugging/toc.yml
- name: Examples
href: examples.md

0 comments on commit a2c0df4

Please sign in to comment.