├── .devcontainer ├── devcontainer.json ├── init.sh └── pg_hba.conf ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ ├── pack.yml │ └── test-report.yml ├── .gitignore ├── .vscode └── settings.json ├── COPYING ├── COPYING.LESSER ├── Hangfire.PostgreSql.sln ├── Hangfire.PostgreSql.sln.DotSettings ├── LICENSE.md ├── README.md ├── SECURITY.md ├── src ├── Common │ ├── Hangfire.ruleset │ └── Hangfire.targets └── Hangfire.PostgreSql │ ├── CountersAggregator.cs │ ├── EnqueuedAndFetchedCountDto.cs │ ├── Entities │ ├── JobParameter.cs │ ├── Server.cs │ ├── ServerData.cs │ ├── SqlHash.cs │ ├── SqlJob.cs │ └── SqlState.cs │ ├── EnvironmentHelpers.cs │ ├── ExpirationManager.cs │ ├── Factories │ ├── ExistingNpgsqlConnectionFactory.cs │ ├── NpgsqlConnectionFactory.cs │ └── NpgsqlInstanceConnectionFactoryBase.cs │ ├── Hangfire.PostgreSql.csproj │ ├── IConnectionFactory.cs │ ├── IPersistentJobQueue.cs │ ├── IPersistentJobQueueMonitoringApi.cs │ ├── IPersistentJobQueueProvider.cs │ ├── JsonParameter.cs │ ├── PersistentJobQueueProviderCollection.cs │ ├── PostgreSqlBootstrapperConfigurationExtensions.cs │ ├── PostgreSqlBootstrapperOptions.cs │ ├── PostgreSqlConnection.cs │ ├── PostgreSqlDistributedLock.cs │ ├── PostgreSqlDistributedLockException.cs │ ├── PostgreSqlFetchedJob.cs │ ├── PostgreSqlHeartbeatProcess.cs │ ├── PostgreSqlJobQueue.cs │ ├── PostgreSqlJobQueueMonitoringApi.cs │ ├── PostgreSqlJobQueueProvider.cs │ ├── PostgreSqlMonitoringApi.cs │ ├── PostgreSqlObjectsInstaller.cs │ ├── PostgreSqlStorage.cs │ ├── PostgreSqlStorageOptions.cs │ ├── PostgreSqlWriteOnlyTransaction.cs │ ├── Properties │ ├── Annotations.cs │ └── AssemblyInfo.cs │ ├── Scripts │ ├── Install.v10.sql │ ├── Install.v11.sql │ ├── Install.v12.sql │ ├── Install.v13.sql │ ├── Install.v14.sql │ ├── Install.v15.sql │ ├── Install.v16.sql │ ├── Install.v17.sql │ ├── Install.v18.sql │ ├── Install.v19.sql │ ├── Install.v20.sql │ ├── Install.v21.sql │ ├── Install.v22.sql │ ├── Install.v23.sql │ ├── Install.v3.sql │ ├── Install.v4.sql │ ├── Install.v5.sql │ ├── Install.v6.sql │ ├── Install.v7.sql │ ├── Install.v8.sql │ └── Install.v9.sql │ └── Utils │ ├── AutoResetEventRegistry.cs │ ├── DbConnectionExtensions.cs │ ├── ExceptionTypeHelper.cs │ ├── TimestampHelper.cs │ ├── TransactionHelpers.cs │ └── TryExecute.cs └── tests └── Hangfire.PostgreSql.Tests ├── AssemblyAttributes.cs ├── CountersAggregatorFacts.cs ├── Entities └── TestJob.cs ├── ExpirationManagerFacts.cs ├── FirstClassQueueFeatureSupportTests.cs ├── GlobalSuppressions.cs ├── Hangfire.PostgreSql.Tests.csproj ├── PersistentJobQueueProviderCollectionFacts.cs ├── PostgreSqlConnectionFacts.cs ├── PostgreSqlDistributedLockFacts.cs ├── PostgreSqlFetchedJobFacts.cs ├── PostgreSqlInstallerFacts.cs ├── PostgreSqlJobQueueFacts.cs ├── PostgreSqlMonitoringApiFacts.cs ├── PostgreSqlStorageFacts.cs ├── PostgreSqlStorageOptionsFacts.cs ├── PostgreSqlWriteOnlyTransactionFacts.cs ├── Scripts └── Clean.sql └── Utils ├── CleanDatabaseAttribute.cs ├── ConnectionUtils.cs ├── DefaultConnectionFactory.cs ├── Helper.cs ├── PostgreSqlStorageExtensions.cs ├── PostgreSqlStorageFixture.cs └── PostgreSqlTestObjectsInitializer.cs /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/universal:2", 3 | "features": { 4 | "ghcr.io/devcontainers/features/dotnet:1": {} 5 | }, 6 | "customizations": { 7 | "vscode": { 8 | "extensions": [ 9 | "ms-dotnettools.csharp", 10 | "eamodio.gitlens", 11 | "formulahendry.dotnet-test-explorer", 12 | "mtxr.sqltools-driver-pg" 13 | ] 14 | } 15 | }, 16 | "containerEnv": { 17 | "DOTNET_CLI_HOME": "/tmp/DOTNET_CLI_HOME" 18 | }, 19 | "postCreateCommand": "./.devcontainer/init.sh", 20 | "postStartCommand": "sudo service postgresql start" 21 | } 22 | -------------------------------------------------------------------------------- /.devcontainer/init.sh: -------------------------------------------------------------------------------- 1 | sudo apt update 2 | sudo apt install postgresql postgresql-contrib -y 3 | sudo cp ./.devcontainer/pg_hba.conf /etc/postgresql/12/main/pg_hba.conf 4 | sudo chown postgres:postgres /etc/postgresql/12/main/pg_hba.conf 5 | sudo service postgresql start 6 | 7 | sudo psql -U postgres -c "ALTER USER postgres PASSWORD 'password';" 8 | 9 | dotnet restore -------------------------------------------------------------------------------- /.devcontainer/pg_hba.conf: -------------------------------------------------------------------------------- 1 | # PostgreSQL Client Authentication Configuration File 2 | # =================================================== 3 | # 4 | # Refer to the "Client Authentication" section in the PostgreSQL 5 | # documentation for a complete description of this file. A short 6 | # synopsis follows. 7 | # 8 | # This file controls: which hosts are allowed to connect, how clients 9 | # are authenticated, which PostgreSQL user names they can use, which 10 | # databases they can access. Records take one of these forms: 11 | # 12 | # local DATABASE USER METHOD [OPTIONS] 13 | # host DATABASE USER ADDRESS METHOD [OPTIONS] 14 | # hostssl DATABASE USER ADDRESS METHOD [OPTIONS] 15 | # hostnossl DATABASE USER ADDRESS METHOD [OPTIONS] 16 | # hostgssenc DATABASE USER ADDRESS METHOD [OPTIONS] 17 | # hostnogssenc DATABASE USER ADDRESS METHOD [OPTIONS] 18 | # 19 | # (The uppercase items must be replaced by actual values.) 20 | # 21 | # The first field is the connection type: "local" is a Unix-domain 22 | # socket, "host" is either a plain or SSL-encrypted TCP/IP socket, 23 | # "hostssl" is an SSL-encrypted TCP/IP socket, and "hostnossl" is a 24 | # non-SSL TCP/IP socket. Similarly, "hostgssenc" uses a 25 | # GSSAPI-encrypted TCP/IP socket, while "hostnogssenc" uses a 26 | # non-GSSAPI socket. 27 | # 28 | # DATABASE can be "all", "sameuser", "samerole", "replication", a 29 | # database name, or a comma-separated list thereof. The "all" 30 | # keyword does not match "replication". Access to replication 31 | # must be enabled in a separate record (see example below). 32 | # 33 | # USER can be "all", a user name, a group name prefixed with "+", or a 34 | # comma-separated list thereof. In both the DATABASE and USER fields 35 | # you can also write a file name prefixed with "@" to include names 36 | # from a separate file. 37 | # 38 | # ADDRESS specifies the set of hosts the record matches. It can be a 39 | # host name, or it is made up of an IP address and a CIDR mask that is 40 | # an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that 41 | # specifies the number of significant bits in the mask. A host name 42 | # that starts with a dot (.) matches a suffix of the actual host name. 43 | # Alternatively, you can write an IP address and netmask in separate 44 | # columns to specify the set of hosts. Instead of a CIDR-address, you 45 | # can write "samehost" to match any of the server's own IP addresses, 46 | # or "samenet" to match any address in any subnet that the server is 47 | # directly connected to. 48 | # 49 | # METHOD can be "trust", "reject", "md5", "password", "scram-sha-256", 50 | # "gss", "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert". 51 | # Note that "password" sends passwords in clear text; "md5" or 52 | # "scram-sha-256" are preferred since they send encrypted passwords. 53 | # 54 | # OPTIONS are a set of options for the authentication in the format 55 | # NAME=VALUE. The available options depend on the different 56 | # authentication methods -- refer to the "Client Authentication" 57 | # section in the documentation for a list of which options are 58 | # available for which authentication methods. 59 | # 60 | # Database and user names containing spaces, commas, quotes and other 61 | # special characters must be quoted. Quoting one of the keywords 62 | # "all", "sameuser", "samerole" or "replication" makes the name lose 63 | # its special character, and just match a database or username with 64 | # that name. 65 | # 66 | # This file is read on server startup and when the server receives a 67 | # SIGHUP signal. If you edit the file on a running system, you have to 68 | # SIGHUP the server for the changes to take effect, run "pg_ctl reload", 69 | # or execute "SELECT pg_reload_conf()". 70 | # 71 | # Put your actual configuration here 72 | # ---------------------------------- 73 | # 74 | # If you want to allow non-local connections, you need to add more 75 | # "host" records. In that case you will also need to make PostgreSQL 76 | # listen on a non-local interface via the listen_addresses 77 | # configuration parameter, or via the -i or -h command line switches. 78 | 79 | 80 | 81 | 82 | # DO NOT DISABLE! 83 | # If you change this first entry you will need to make sure that the 84 | # database superuser can access the database using some other method. 85 | # Noninteractive access to all databases is required during automatic 86 | # maintenance (custom daily cronjobs, replication, and similar tasks). 87 | # 88 | # Database administrative login by Unix domain socket 89 | local all postgres trust 90 | local all postgres md5 91 | 92 | # TYPE DATABASE USER ADDRESS METHOD 93 | 94 | # "local" is for Unix domain socket connections only 95 | local all all peer 96 | # IPv4 local connections: 97 | host all all 127.0.0.1/32 md5 98 | # IPv6 local connections: 99 | host all all ::1/128 md5 100 | # Allow replication connections from localhost, by a user with the 101 | # replication privilege. 102 | local replication all peer 103 | host replication all 127.0.0.1/32 md5 104 | host replication all ::1/128 md5 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Verification build 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: ["master"] 7 | paths: 8 | - "src/**" 9 | - "test/**" 10 | 11 | permissions: write-all 12 | 13 | jobs: 14 | build-and-test: 15 | runs-on: ubuntu-latest 16 | 17 | services: 18 | postgres: 19 | image: postgres 20 | env: 21 | POSTGRES_PASSWORD: postgres 22 | POSTGRES_HOST_AUTH_METHOD: trust 23 | TZ: UTC+13 24 | PGTZ: UTC+13 25 | options: >- 26 | --health-cmd pg_isready 27 | --health-interval 10s 28 | --health-timeout 5s 29 | --health-retries 5 30 | ports: 31 | - 5432:5432 32 | 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | with: 37 | fetch-depth: 0 38 | - name: Setup .NET 39 | uses: actions/setup-dotnet@v4 40 | with: 41 | dotnet-version: 9.0.x 42 | - name: Restore dependencies 43 | run: dotnet restore 44 | - name: Build 45 | run: dotnet build --no-restore 46 | - name: Test 47 | run: dotnet test --no-build --verbosity normal --logger "trx;LogFileName=TestResults.trx" 48 | - name: Test Report 49 | uses: actions/upload-artifact@v4 50 | if: success() || failure() 51 | with: 52 | name: test-results 53 | path: "**/TestResults.trx" 54 | -------------------------------------------------------------------------------- /.github/workflows/pack.yml: -------------------------------------------------------------------------------- 1 | name: Build and tests 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: ["master"] 7 | paths: ["src/**"] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Setup .NET 19 | uses: actions/setup-dotnet@v4 20 | with: 21 | dotnet-version: 9.0.x 22 | - name: Restore dependencies 23 | run: dotnet restore 24 | - name: Build 25 | run: dotnet build --no-restore -c Release 26 | - name: Pack 27 | run: dotnet pack -c Release --no-build --output publish 28 | - name: Upload a Build Artifact 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: Package 32 | path: publish/**.nupkg 33 | 34 | test: 35 | permissions: write-all 36 | runs-on: ubuntu-latest 37 | 38 | services: 39 | postgres: 40 | image: postgres 41 | env: 42 | POSTGRES_PASSWORD: postgres 43 | POSTGRES_HOST_AUTH_METHOD: trust 44 | TZ: UTC+13 45 | PGTZ: UTC+13 46 | options: >- 47 | --health-cmd pg_isready 48 | --health-interval 10s 49 | --health-timeout 5s 50 | --health-retries 5 51 | ports: 52 | - 5432:5432 53 | 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | with: 58 | fetch-depth: 0 59 | - name: Setup .NET 60 | uses: actions/setup-dotnet@v4 61 | with: 62 | dotnet-version: 9.0.x 63 | - name: Restore dependencies 64 | run: dotnet restore 65 | - name: Build 66 | run: dotnet build -c Release --no-restore 67 | - name: Test 68 | run: dotnet test -c Release --no-build --verbosity normal --logger "trx;LogFileName=TestResults.trx" || true 69 | - name: Test Report 70 | uses: dorny/test-reporter@v1 71 | if: always() 72 | with: 73 | name: Test results 74 | path: "**/TestResults.trx" 75 | reporter: dotnet-trx 76 | fail-on-error: true 77 | -------------------------------------------------------------------------------- /.github/workflows/test-report.yml: -------------------------------------------------------------------------------- 1 | name: Test report 2 | 3 | on: 4 | workflow_run: 5 | workflows: ['Verification build', 'Build and tests'] 6 | types: 7 | - completed 8 | 9 | permissions: 10 | id-token: write 11 | contents: read 12 | checks: write 13 | 14 | jobs: 15 | report: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: dorny/test-reporter@v1 19 | with: 20 | artifact: test-results 21 | name: Test results 22 | path: "**/TestResults.trx" 23 | reporter: dotnet-trx 24 | fail-on-error: false 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | packages/ 45 | 46 | # Build results 47 | 48 | [Bb]uild/ 49 | [Dd]ebug/ 50 | [Rr]elease/ 51 | x64/ 52 | [Bb]in/ 53 | [Oo]bj/ 54 | 55 | # MSTest test Results 56 | [Tt]est[Rr]esult*/ 57 | [Bb]uild[Ll]og.* 58 | 59 | *_i.c 60 | *_p.c 61 | *.ilk 62 | *.meta 63 | *.obj 64 | *.pch 65 | *.pdb 66 | *.pgc 67 | *.pgd 68 | *.rsp 69 | *.sbr 70 | *.tlb 71 | *.tli 72 | *.tlh 73 | *.tmp 74 | *.tmp_proj 75 | *.log 76 | *.vspscc 77 | *.vssscc 78 | .builds 79 | *.pidb 80 | *.log 81 | *.scc 82 | 83 | # Visual C++ cache files 84 | ipch/ 85 | *.aps 86 | *.ncb 87 | *.opensdf 88 | *.sdf 89 | *.cachefile 90 | 91 | # Visual Studio profiler 92 | *.psess 93 | *.vsp 94 | *.vspx 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | 103 | # TeamCity is a build add-in 104 | _TeamCity* 105 | 106 | # DotCover is a Code Coverage Tool 107 | *.dotCover 108 | 109 | # NCrunch 110 | *.ncrunch* 111 | .*crunch*.local.xml 112 | 113 | # Installshield output folder 114 | [Ee]xpress/ 115 | 116 | # DocProject is a documentation generator add-in 117 | DocProject/buildhelp/ 118 | DocProject/Help/*.HxT 119 | DocProject/Help/*.HxC 120 | DocProject/Help/*.hhc 121 | DocProject/Help/*.hhk 122 | DocProject/Help/*.hhp 123 | DocProject/Help/Html2 124 | DocProject/Help/html 125 | 126 | # Click-Once directory 127 | publish/ 128 | 129 | # Publish Web Output 130 | *.Publish.xml 131 | *.pubxml 132 | 133 | # NuGet Packages Directory 134 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 135 | #packages/ 136 | 137 | # Windows Azure Build Output 138 | csx 139 | *.build.csdef 140 | 141 | # Windows Store app package directory 142 | AppPackages/ 143 | 144 | # Others 145 | sql/ 146 | *.Cache 147 | ClientBin/ 148 | [Ss]tyle[Cc]op.* 149 | ~$* 150 | *~ 151 | *.dbmdl 152 | *.[Pp]ublish.xml 153 | *.pfx 154 | *.publishsettings 155 | 156 | # RIA/Silverlight projects 157 | Generated_Code/ 158 | 159 | # Backup & report files from converting an old project file to a newer 160 | # Visual Studio version. Backup files are not needed, because we have git ;-) 161 | _UpgradeReport_Files/ 162 | Backup*/ 163 | UpgradeLog*.XML 164 | UpgradeLog*.htm 165 | 166 | # SQL Server files 167 | App_Data/*.mdf 168 | App_Data/*.ldf 169 | 170 | ############# 171 | ## Windows detritus 172 | ############# 173 | 174 | # Windows image file caches 175 | Thumbs.db 176 | ehthumbs.db 177 | 178 | # Folder config file 179 | Desktop.ini 180 | 181 | # Recycle Bin used on file shares 182 | $RECYCLE.BIN/ 183 | 184 | # Mac crap 185 | .DS_Store 186 | 187 | 188 | ############# 189 | ## Python 190 | ############# 191 | 192 | *.py[co] 193 | 194 | # Packages 195 | *.egg 196 | *.egg-info 197 | dist/ 198 | eggs/ 199 | parts/ 200 | var/ 201 | sdist/ 202 | develop-eggs/ 203 | .installed.cfg 204 | 205 | # Installer logs 206 | pip-log.txt 207 | 208 | # Unit test / coverage reports 209 | .coverage 210 | .tox 211 | 212 | #Translations 213 | *.mo 214 | 215 | #Mr Developer 216 | .mr.developer.cfg 217 | 218 | #Sphinx Built Docs 219 | docs/_build 220 | 221 | #Jekyll site builds 222 | _site 223 | *.userprefs 224 | src/Hangfire.PostgreSql/.vs/restore.dg 225 | src/Hangfire.PostgreSql/project.lock.json 226 | */**/project.lock.json 227 | tools 228 | .vs 229 | .idea 230 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "sqltools.connections": [ 3 | { 4 | "previewLimit": 50, 5 | "server": "localhost", 6 | "port": 5432, 7 | "driver": "PostgreSQL", 8 | "name": "localhost", 9 | "database": "postgres", 10 | "username": "postgres", 11 | "password": "password" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /COPYING.LESSER: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Hangfire.PostgreSql.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33424.131 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{766BE831-F758-46BC-AFD3-BBEEFE0F686F}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5CA38188-92EE-453C-A04E-A506DF15A787}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0D30A51B-814F-474E-93B8-44E9C155255C}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangfire.PostgreSql", "src\Hangfire.PostgreSql\Hangfire.PostgreSql.csproj", "{3E4DBC41-F38E-4D1C-A6A7-206A132A29D6}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangfire.PostgreSql.Tests", "tests\Hangfire.PostgreSql.Tests\Hangfire.PostgreSql.Tests.csproj", "{6044A48D-730B-4D1F-B03A-EB2B458DAF53}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{AAA78654-9846-4870-A13C-D9DBAF0792C4}" 17 | ProjectSection(SolutionItems) = preProject 18 | .github\workflows\ci.yml = .github\workflows\ci.yml 19 | .github\workflows\pack.yml = .github\workflows\pack.yml 20 | EndProjectSection 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {3E4DBC41-F38E-4D1C-A6A7-206A132A29D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {3E4DBC41-F38E-4D1C-A6A7-206A132A29D6}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {3E4DBC41-F38E-4D1C-A6A7-206A132A29D6}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {3E4DBC41-F38E-4D1C-A6A7-206A132A29D6}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {6044A48D-730B-4D1F-B03A-EB2B458DAF53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {6044A48D-730B-4D1F-B03A-EB2B458DAF53}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {6044A48D-730B-4D1F-B03A-EB2B458DAF53}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {6044A48D-730B-4D1F-B03A-EB2B458DAF53}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(NestedProjects) = preSolution 41 | {3E4DBC41-F38E-4D1C-A6A7-206A132A29D6} = {0D30A51B-814F-474E-93B8-44E9C155255C} 42 | {6044A48D-730B-4D1F-B03A-EB2B458DAF53} = {766BE831-F758-46BC-AFD3-BBEEFE0F686F} 43 | {AAA78654-9846-4870-A13C-D9DBAF0792C4} = {5CA38188-92EE-453C-A04E-A506DF15A787} 44 | EndGlobalSection 45 | GlobalSection(ExtensibilityGlobals) = postSolution 46 | SolutionGuid = {F7E32105-7F61-4127-8517-5E4275B9CABE} 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /Hangfire.PostgreSql.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True 5 | True 6 | True 7 | True 8 | <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="Hangfire.Core.Tests" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /><Filter ModuleMask="Hangfire.SqlServer.Tests" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data> 9 | <data /> -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | License 2 | ======== 3 | 4 | Copyright © 2014-2022 Frank Hommers https://github.com/frankhommers/Hangfire.PostgreSql and others (Burhan Irmikci (barhun), Zachary Sims( 5 | zsims), kgamecarter, Stafford Williams (staff0rd), briangweber, Viktor Svyatokha (ahydrax), Christopher Dresel (Dresel), 6 | Vytautas Kasparavičius (vytautask), Vincent Vrijburg, David Roth (davidroth). 7 | 8 | Hangfire.PostgreSql is an Open Source project licensed under the terms of the LGPLv3 license. Please 9 | see http://www.gnu.org/licenses/lgpl-3.0.html for license text or COPYING.LESSER file distributed with the source code. 10 | 11 | This work is based on the work of Sergey Odinokov, author of Hangfire. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hangfire.PostgreSql 2 | 3 | [![Build status](https://github.com/hangfire-postgres/Hangfire.PostgreSql/actions/workflows/pack.yml/badge.svg)](https://github.com/hangfire-postgres/Hangfire.PostgreSql/actions/workflows/pack.yml) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/hangfire-postgres/Hangfire.PostgreSql?label=Release)](https://github.com/hangfire-postgres/Hangfire.PostgreSql/releases/latest) [![Nuget](https://img.shields.io/nuget/v/Hangfire.PostgreSql?label=NuGet)](https://www.nuget.org/packages/Hangfire.PostgreSql) 4 | 5 | This is an plugin to the Hangfire to enable PostgreSQL as a storage system. 6 | Read about hangfire here: https://github.com/HangfireIO/Hangfire#overview 7 | and here: http://hangfire.io/ 8 | 9 | ## Instructions 10 | 11 | ### For .NET 12 | 13 | Install Hangfire, see https://github.com/HangfireIO/Hangfire#installation 14 | 15 | Download all files from this repository, add the Hangfire.PostgreSql.csproj to your solution. 16 | Reference it in your project, and you are ready to go by using: 17 | 18 | ```csharp 19 | app.UseHangfireServer(new BackgroundJobServerOptions(), 20 | new PostgreSqlStorage("")); 21 | app.UseHangfireDashboard(); 22 | ``` 23 | 24 | ### For ASP.NET Core 25 | 26 | First, NuGet package needs installation. 27 | 28 | - Hangfire.AspNetCore 29 | - Hangfire.PostgreSql (Uses Npgsql 6) 30 | - Hangfire.PostgreSql.Npgsql5 (Uses Npgsql 5) 31 | 32 | Historically both packages were functionally the same up until the package version 1.9.11, the only difference was the underlying Npgsql dependency version. Afterwards, the support for Npgsql v5 has been dropped and now minimum required version is 6.0.0. 33 | 34 | In `Startup.cs` _ConfigureServices(IServiceCollection services)_ method add the following line: 35 | 36 | ```csharp 37 | services.AddHangfire(config => 38 | config.UsePostgreSqlStorage(c => 39 | c.UseNpgsqlConnection(Configuration.GetConnectionString("HangfireConnection")))); 40 | ``` 41 | 42 | In Configure method, add these two lines: 43 | 44 | ```csharp 45 | app.UseHangfireServer(); 46 | app.UseHangfireDashboard(); 47 | ``` 48 | 49 | And... That's it. You are ready to go. 50 | 51 | If you encounter any issues/bugs or have idea of a feature regarding Hangfire.Postgresql, [create us an issue](https://github.com/hangfire-postgres/Hangfire.PostgreSql/issues/new). Thanks! 52 | 53 | ### Enabling SSL support 54 | 55 | SSL support can be enabled for Hangfire.PostgreSql library using the following mechanism: 56 | 57 | ```csharp 58 | config.UsePostgreSqlStorage(c => 59 | c.UseNpgsqlConnection( 60 | Configuration.GetConnectionString("HangfireConnection"), // connection string, 61 | connection => // connection setup - gets called after instantiating the connection and before any calls to DB are made 62 | { 63 | connection.ProvideClientCertificatesCallback += clientCerts => 64 | { 65 | clientCerts.Add(X509Certificate.CreateFromCertFile("[CERT_FILENAME]")); 66 | }; 67 | } 68 | ) 69 | ); 70 | ``` 71 | ### Queue processing 72 | 73 | Similar to `Hangfire.SqlServer`, queues are processed in alphabetical order. Given the following example 74 | 75 | ```csharp 76 | var options = new BackgroundJobServerOptions 77 | { 78 | Queues = new[] { "general-queue", "very-fast-queue", "a-long-running-queue" } 79 | }; 80 | app.UseHangfireServer(options); 81 | ``` 82 | 83 | this provider would first process jobs in `a-long-running-queue`, then `general-queue` and lastly `very-fast-queue`. 84 | 85 | ### License 86 | 87 | Copyright © 2014-2024 Frank Hommers https://github.com/hangfire-postgres/Hangfire.PostgreSql. 88 | 89 | Collaborators: 90 | Frank Hommers (frankhommers), Vytautas Kasparavičius (vytautask), Žygimantas Arūna (azygis) 91 | 92 | Contributors: 93 | Andrew Armstrong (Plasma), Burhan Irmikci (barhun), Zachary Sims(zsims), kgamecarter, Stafford Williams (staff0rd), briangweber, Viktor Svyatokha (ahydrax), Christopher Dresel (Dresel), Vincent Vrijburg, David Roth (davidroth) and Ivan Tiniakov (Tinyakov). 94 | 95 | Hangfire.PostgreSql is an Open Source project licensed under the terms of the LGPLv3 license. Please see http://www.gnu.org/licenses/lgpl-3.0.html for license text or COPYING.LESSER file distributed with the source code. 96 | 97 | This work is based on the work of Sergey Odinokov, author of Hangfire. 98 | 99 | ### Related Projects 100 | 101 | - [Hangfire.Core](https://github.com/HangfireIO/Hangfire) 102 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 1.6.3 | :white_check_mark: | 11 | | < 1.6.3 | :x: | 12 | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | Do not create a new issue ticket with the detailed information of the vulnerability under any circumstances! 17 | 18 | All vulnerabilities can be reported directly to the main project maintainers (currently @frankfommers and @vytautask) directly 19 | via email (frank-hfpg[at]hommers[dot]nl or vytautaskasp[at]gmail[dot]com respectively). We should get back to you in 24 hours. Please include as much information as possible (if you have a POC - even better). All security issues have our highest attention and will be solved ASAP. 20 | 21 | You would help us out if you also include a fix in a pull request. That way we can quickly release a fix. 22 | 23 | Thank you for your understanding. 24 | -------------------------------------------------------------------------------- /src/Common/Hangfire.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(ArtifactsDir)\$(MSBuildProjectName) 5 | $(ArtifactsDir)\$(MSBuildProjectName)\bin 6 | 7 | 8 | 9 | $(MSBuildThisFileDirectory)Hangfire.ruleset 10 | false 11 | 1591 12 | false 13 | 14 | 15 | 16 | $(DefineConstants);CODE_ANALYSIS 17 | 11.0 18 | 19 | 20 | 21 | $(DefineConstants);MONO 22 | 23 | 24 | 25 | $(DefineConstants);SIGNED 26 | true 27 | true 28 | $(KeyFile) 29 | 30 | 31 | 32 | 33 | GlobalSuppressions.cs 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/CountersAggregator.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | using System.Threading; 24 | using Dapper; 25 | using Hangfire.Common; 26 | using Hangfire.Logging; 27 | using Hangfire.Server; 28 | 29 | namespace Hangfire.PostgreSql 30 | { 31 | #pragma warning disable 618 32 | internal class CountersAggregator : IServerComponent 33 | #pragma warning restore 618 34 | { 35 | // This number should be high enough to aggregate counters efficiently, 36 | // but low enough to not to cause large amount of row locks to be taken. 37 | // Lock escalation to page locks may pause the background processing. 38 | private const int NumberOfRecordsInSinglePass = 1000; 39 | 40 | private static readonly TimeSpan _delayBetweenPasses = TimeSpan.FromMilliseconds(500); 41 | 42 | private readonly ILog _logger = LogProvider.For(); 43 | private readonly TimeSpan _interval; 44 | private readonly PostgreSqlStorage _storage; 45 | 46 | public CountersAggregator(PostgreSqlStorage storage, TimeSpan interval) 47 | { 48 | _storage = storage ?? throw new ArgumentNullException(nameof(storage)); 49 | _interval = interval; 50 | } 51 | 52 | public void Execute(CancellationToken cancellationToken) 53 | { 54 | _logger.Debug("Aggregating records in 'Counter' table..."); 55 | 56 | int removedCount = 0; 57 | 58 | do 59 | { 60 | _storage.UseConnection(null, connection => { 61 | removedCount = connection.Execute(GetAggregationQuery(), 62 | new { now = DateTime.UtcNow, count = NumberOfRecordsInSinglePass }, 63 | commandTimeout: 0); 64 | }); 65 | 66 | if (removedCount < NumberOfRecordsInSinglePass) 67 | { 68 | continue; 69 | } 70 | 71 | cancellationToken.Wait(_delayBetweenPasses); 72 | cancellationToken.ThrowIfCancellationRequested(); 73 | // ReSharper disable once LoopVariableIsNeverChangedInsideLoop 74 | } while (removedCount >= NumberOfRecordsInSinglePass); 75 | 76 | _logger.Trace("Records from the 'Counter' table aggregated."); 77 | 78 | cancellationToken.Wait(_interval); 79 | } 80 | 81 | private string GetAggregationQuery() 82 | { 83 | string schemaName = _storage.Options.SchemaName; 84 | return 85 | $""" 86 | BEGIN; 87 | 88 | INSERT INTO "{schemaName}"."aggregatedcounter" ("key", "value", "expireat") 89 | SELECT 90 | "key", 91 | SUM("value"), 92 | MAX("expireat") 93 | FROM "{schemaName}"."counter" 94 | GROUP BY "key" 95 | ON CONFLICT("key") DO UPDATE 96 | SET "value" = "aggregatedcounter"."value" + EXCLUDED."value", "expireat" = EXCLUDED."expireat"; 97 | 98 | DELETE FROM "{schemaName}"."counter" 99 | WHERE "key" IN ( 100 | SELECT "key" FROM "{schemaName}"."aggregatedcounter" 101 | ); 102 | 103 | COMMIT; 104 | """; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/EnqueuedAndFetchedCountDto.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | namespace Hangfire.PostgreSql 23 | { 24 | public class EnqueuedAndFetchedCountDto 25 | { 26 | public long EnqueuedCount { get; set; } 27 | public long FetchedCount { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Entities/JobParameter.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using Hangfire.PostgreSql.Properties; 23 | 24 | namespace Hangfire.PostgreSql.Entities 25 | { 26 | [UsedImplicitly] 27 | internal class JobParameter 28 | { 29 | public long JobId { get; set; } 30 | public string Name { get; set; } 31 | public string Value { get; set; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Entities/Server.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | using Hangfire.PostgreSql.Properties; 24 | 25 | namespace Hangfire.PostgreSql.Entities 26 | { 27 | [UsedImplicitly] 28 | internal class Server 29 | { 30 | public string Id { get; set; } 31 | public string Data { get; set; } 32 | public DateTime LastHeartbeat { get; set; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Entities/ServerData.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | 24 | namespace Hangfire.PostgreSql.Entities 25 | { 26 | internal class ServerData 27 | { 28 | public int WorkerCount { get; set; } 29 | public string[] Queues { get; set; } 30 | public DateTime? StartedAt { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Entities/SqlHash.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | using Hangfire.PostgreSql.Properties; 24 | 25 | namespace Hangfire.PostgreSql.Entities 26 | { 27 | [UsedImplicitly] 28 | internal class SqlHash 29 | { 30 | public long Id { get; set; } 31 | public string Key { get; set; } 32 | public string Field { get; set; } 33 | public string Value { get; set; } 34 | public DateTime? ExpireAt { get; set; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Entities/SqlJob.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright � 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | using Hangfire.PostgreSql.Properties; 24 | 25 | namespace Hangfire.PostgreSql.Entities 26 | { 27 | [UsedImplicitly] 28 | internal class SqlJob 29 | { 30 | public long Id { get; set; } 31 | public string InvocationData { get; set; } 32 | public string Arguments { get; set; } 33 | public DateTime CreatedAt { get; set; } 34 | public DateTime? ExpireAt { get; set; } 35 | 36 | public DateTime? FetchedAt { get; set; } 37 | 38 | public string StateName { get; set; } 39 | public string StateReason { get; set; } 40 | public string StateData { get; set; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Entities/SqlState.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | using Hangfire.PostgreSql.Properties; 24 | 25 | namespace Hangfire.PostgreSql.Entities 26 | { 27 | [UsedImplicitly] 28 | internal class SqlState 29 | { 30 | public long JobId { get; set; } 31 | public string Name { get; set; } 32 | public string Reason { get; set; } 33 | public DateTime CreatedAt { get; set; } 34 | public string Data { get; set; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/EnvironmentHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Hangfire.PostgreSql 4 | { 5 | internal class EnvironmentHelpers 6 | { 7 | private static bool? _isMono; 8 | 9 | public static bool IsMono() 10 | { 11 | return _isMono ??= Type.GetType("Mono.Runtime") != null; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/ExpirationManager.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | using System.Data; 24 | using System.Globalization; 25 | using System.Threading; 26 | using Dapper; 27 | using Hangfire.Logging; 28 | using Hangfire.Server; 29 | using Hangfire.Storage; 30 | 31 | namespace Hangfire.PostgreSql 32 | { 33 | #pragma warning disable CS0618 34 | internal class ExpirationManager : IBackgroundProcess, IServerComponent 35 | #pragma warning restore CS0618 36 | { 37 | private const string DistributedLockKey = "locks:expirationmanager"; 38 | 39 | private static readonly TimeSpan _defaultLockTimeout = TimeSpan.FromMinutes(5); 40 | private static readonly TimeSpan _delayBetweenPasses = TimeSpan.FromSeconds(1); 41 | private static readonly ILog _logger = LogProvider.GetLogger(typeof(ExpirationManager)); 42 | 43 | private static readonly string[] _processedCounters = { 44 | "stats:succeeded", 45 | "stats:deleted", 46 | }; 47 | private static readonly string[] _processedTables = { 48 | "aggregatedcounter", 49 | "counter", 50 | "job", 51 | "list", 52 | "set", 53 | "hash", 54 | }; 55 | 56 | private readonly TimeSpan _checkInterval; 57 | private readonly PostgreSqlStorage _storage; 58 | 59 | public ExpirationManager(PostgreSqlStorage storage) 60 | : this(storage ?? throw new ArgumentNullException(nameof(storage)), storage.Options.JobExpirationCheckInterval) { } 61 | 62 | public ExpirationManager(PostgreSqlStorage storage, TimeSpan checkInterval) 63 | { 64 | _storage = storage ?? throw new ArgumentNullException(nameof(storage)); 65 | _checkInterval = checkInterval; 66 | } 67 | 68 | public void Execute(BackgroundProcessContext context) 69 | { 70 | Execute(context.StoppingToken); 71 | } 72 | 73 | public void Execute(CancellationToken cancellationToken) 74 | { 75 | foreach (string table in _processedTables) 76 | { 77 | _logger.DebugFormat("Removing outdated records from table '{0}'...", table); 78 | 79 | UseConnectionDistributedLock(_storage, connection => { 80 | int removedCount; 81 | do 82 | { 83 | using IDbTransaction transaction = connection.BeginTransaction(); 84 | removedCount = connection.Execute($@" 85 | DELETE FROM ""{_storage.Options.SchemaName}"".""{table}"" 86 | WHERE ""id"" IN ( 87 | SELECT ""id"" 88 | FROM ""{_storage.Options.SchemaName}"".""{table}"" 89 | WHERE ""expireat"" < NOW() 90 | LIMIT {_storage.Options.DeleteExpiredBatchSize.ToString(CultureInfo.InvariantCulture)} 91 | )", transaction: transaction); 92 | 93 | if (removedCount <= 0) 94 | { 95 | continue; 96 | } 97 | 98 | transaction.Commit(); 99 | _logger.InfoFormat("Removed {0} outdated record(s) from '{1}' table.", removedCount, table); 100 | 101 | cancellationToken.WaitHandle.WaitOne(_delayBetweenPasses); 102 | cancellationToken.ThrowIfCancellationRequested(); 103 | } 104 | while (removedCount != 0); 105 | }); 106 | } 107 | 108 | AggregateCounters(cancellationToken); 109 | cancellationToken.WaitHandle.WaitOne(_checkInterval); 110 | } 111 | 112 | public override string ToString() 113 | { 114 | return "SQL Records Expiration Manager"; 115 | } 116 | 117 | private void AggregateCounters(CancellationToken cancellationToken) 118 | { 119 | foreach (string processedCounter in _processedCounters) 120 | { 121 | AggregateCounter(processedCounter); 122 | cancellationToken.ThrowIfCancellationRequested(); 123 | } 124 | } 125 | 126 | private void AggregateCounter(string counterName) 127 | { 128 | UseConnectionDistributedLock(_storage, connection => { 129 | using IDbTransaction transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); 130 | string aggregateQuery = $@" 131 | WITH ""counters"" AS ( 132 | DELETE FROM ""{_storage.Options.SchemaName}"".""counter"" 133 | WHERE ""key"" = @Key 134 | AND ""expireat"" IS NULL 135 | RETURNING * 136 | ) 137 | 138 | SELECT SUM(""value"") FROM ""counters""; 139 | "; 140 | 141 | long aggregatedValue = connection.ExecuteScalar(aggregateQuery, new { Key = counterName }, transaction); 142 | transaction.Commit(); 143 | 144 | if (aggregatedValue > 0) 145 | { 146 | string insertQuery = $@"INSERT INTO ""{_storage.Options.SchemaName}"".""counter""(""key"", ""value"") VALUES (@Key, @Value);"; 147 | connection.Execute(insertQuery, new { Key = counterName, Value = aggregatedValue }); 148 | } 149 | }); 150 | } 151 | 152 | private void UseConnectionDistributedLock(PostgreSqlStorage storage, Action action) 153 | { 154 | try 155 | { 156 | storage.UseConnection(null, connection => { 157 | PostgreSqlDistributedLock.Acquire(connection, DistributedLockKey, _defaultLockTimeout, _storage.Options); 158 | 159 | try 160 | { 161 | action(connection); 162 | } 163 | finally 164 | { 165 | PostgreSqlDistributedLock.Release(connection, DistributedLockKey, _storage.Options); 166 | } 167 | }); 168 | } 169 | catch (DistributedLockTimeoutException e) when (e.Resource == DistributedLockKey) 170 | { 171 | // DistributedLockTimeoutException here doesn't mean that outdated records weren't removed. 172 | // It just means another Hangfire server did this work. 173 | _logger.Log(LogLevel.Debug, 174 | () => 175 | $@"An exception was thrown during acquiring distributed lock on the {DistributedLockKey} resource within {_defaultLockTimeout.TotalSeconds} seconds. Outdated records were not removed. It will be retried in {_checkInterval.TotalSeconds} seconds.", 176 | e); 177 | } 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Factories/ExistingNpgsqlConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Npgsql; 3 | 4 | namespace Hangfire.PostgreSql.Factories; 5 | 6 | /// 7 | /// Connection factory that utilizes an already-existing . 8 | /// 9 | public sealed class ExistingNpgsqlConnectionFactory : NpgsqlInstanceConnectionFactoryBase 10 | { 11 | private readonly NpgsqlConnection _connection; 12 | 13 | /// 14 | /// Instantiates the factory using specified . 15 | /// 16 | /// to use. 17 | /// used for connection string verification. 18 | /// 19 | public ExistingNpgsqlConnectionFactory(NpgsqlConnection connection, PostgreSqlStorageOptions options) : base(options) 20 | { 21 | _connection = connection ?? throw new ArgumentNullException(nameof(connection)); 22 | // To ensure valid connection string - throws internally 23 | SetupConnectionStringBuilder(_connection.ConnectionString); 24 | } 25 | 26 | /// 27 | public override NpgsqlConnection GetOrCreateConnection() 28 | { 29 | return _connection; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Factories/NpgsqlConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Hangfire.Annotations; 3 | using Npgsql; 4 | 5 | namespace Hangfire.PostgreSql.Factories; 6 | 7 | /// 8 | /// Connection factory that creates a new based on the connection string. 9 | /// 10 | public sealed class NpgsqlConnectionFactory : NpgsqlInstanceConnectionFactoryBase 11 | { 12 | private readonly string _connectionString; 13 | [CanBeNull] private readonly Action _connectionSetup; 14 | [CanBeNull] private readonly Func _getConnectionString; 15 | 16 | /// 17 | /// Instantiates the factory using specified . 18 | /// 19 | /// Connection string. 20 | /// used for connection string verification. 21 | /// Optional additional connection setup action to be performed on the created . 22 | /// Throws if is null. 23 | public NpgsqlConnectionFactory(string connectionString, PostgreSqlStorageOptions options, [CanBeNull] Action connectionSetup = null) : base(options) 24 | { 25 | _connectionString = SetupConnectionStringBuilder(connectionString ?? throw new ArgumentNullException(nameof(connectionString))).ConnectionString; 26 | _connectionSetup = connectionSetup; 27 | } 28 | 29 | public NpgsqlConnectionFactory(Func getConnectionString, PostgreSqlStorageOptions options, [CanBeNull] Action connectionSetup = null) : this(getConnectionString.Invoke(), options, connectionSetup) 30 | { 31 | _getConnectionString = getConnectionString; 32 | } 33 | 34 | /// 35 | public override NpgsqlConnection GetOrCreateConnection() 36 | { 37 | var connectionString = _connectionString; 38 | if (_getConnectionString != null) 39 | { 40 | connectionString = SetupConnectionStringBuilder(_getConnectionString.Invoke()).ConnectionString; 41 | } 42 | 43 | NpgsqlConnection connection = new(connectionString); 44 | _connectionSetup?.Invoke(connection); 45 | return connection; 46 | } 47 | } -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Factories/NpgsqlInstanceConnectionFactoryBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Hangfire.Annotations; 3 | using Npgsql; 4 | 5 | namespace Hangfire.PostgreSql.Factories; 6 | 7 | public abstract class NpgsqlInstanceConnectionFactoryBase : IConnectionFactory 8 | { 9 | private readonly PostgreSqlStorageOptions _options; 10 | [CanBeNull] private NpgsqlConnectionStringBuilder _connectionStringBuilder; 11 | [CanBeNull] private string _connectionString; 12 | 13 | protected NpgsqlInstanceConnectionFactoryBase(PostgreSqlStorageOptions options) 14 | { 15 | _options = options ?? throw new ArgumentNullException(nameof(options)); 16 | } 17 | 18 | /// 19 | /// Gets the connection string builder associated with the current instance. 20 | /// 21 | /// Throws if connection string builder has not been initialized. 22 | public NpgsqlConnectionStringBuilder ConnectionString => 23 | _connectionStringBuilder ?? throw new InvalidOperationException("Connection string builder has not been initialized"); 24 | 25 | protected NpgsqlConnectionStringBuilder SetupConnectionStringBuilder(string connectionString) 26 | { 27 | if (_connectionStringBuilder != null && string.Equals(_connectionString, connectionString, StringComparison.OrdinalIgnoreCase)) 28 | { 29 | return _connectionStringBuilder; 30 | } 31 | 32 | try 33 | { 34 | _connectionString = connectionString; 35 | NpgsqlConnectionStringBuilder builder = new(connectionString); 36 | 37 | // The connection string must not be modified when transaction enlistment is enabled, otherwise it will cause 38 | // prepared transactions and probably fail when other statements (outside of hangfire) ran within the same 39 | // transaction. Also see #248. 40 | if (!_options.EnableTransactionScopeEnlistment && builder.Enlist) 41 | { 42 | throw new ArgumentException($"TransactionScope enlistment must be enabled by setting {nameof(PostgreSqlStorageOptions)}.{nameof(PostgreSqlStorageOptions.EnableTransactionScopeEnlistment)} to `true`."); 43 | } 44 | 45 | return _connectionStringBuilder = builder; 46 | } 47 | catch (ArgumentException ex) 48 | { 49 | throw new ArgumentException($"Connection string is not valid", nameof(connectionString), ex); 50 | } 51 | } 52 | 53 | /// 54 | public abstract NpgsqlConnection GetOrCreateConnection(); 55 | } -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Hangfire.PostgreSql.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | PostgreSql storage implementation for Hangfire (background job system for ASP.NET and aspnet core applications). 5 | Copyright © 2014-2024 Frank Hommers and others 6 | Hangfire PostgreSql Storage 7 | 1.9.4 8 | Frank Hommers, Vytautas Kasparavičius, Žygimantas Arūna 9 | netstandard2.0 10 | Hangfire.PostgreSql 11 | Library 12 | Hangfire;PostgreSql;Postgres 13 | Hangfire.PostgreSql 14 | https://github.com/frankhommers/Hangfire.PostgreSql/releases 15 | http://hmm.rs/Hangfire.PostgreSql 16 | 17 | 1.9.4.0 18 | 1.9.4.0 19 | 1.9.4.0 20 | True 21 | LICENSE.md 22 | https://github.com/frankhommers/Hangfire.PostgreSql 23 | git 24 | https://github.com/hangfire-postgres 25 | default 26 | true 27 | $(NoWarn);1591 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | all 38 | runtime; build; native; contentfiles; analyzers; buildtransitive 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | True 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/IConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using Npgsql; 23 | 24 | namespace Hangfire.PostgreSql 25 | { 26 | /// 27 | /// Connection factory for creating at runtime. 28 | /// 29 | public interface IConnectionFactory 30 | { 31 | /// 32 | /// Get or create . 33 | /// 34 | NpgsqlConnection GetOrCreateConnection(); 35 | } 36 | } -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/IPersistentJobQueue.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System.Data; 23 | using System.Threading; 24 | using Hangfire.Storage; 25 | 26 | namespace Hangfire.PostgreSql 27 | { 28 | public interface IPersistentJobQueue 29 | { 30 | IFetchedJob Dequeue(string[] queues, CancellationToken cancellationToken); 31 | void Enqueue(IDbConnection connection, string queue, string jobId); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/IPersistentJobQueueMonitoringApi.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System.Collections.Generic; 23 | 24 | namespace Hangfire.PostgreSql 25 | { 26 | public interface IPersistentJobQueueMonitoringApi 27 | { 28 | IEnumerable GetQueues(); 29 | IEnumerable GetEnqueuedJobIds(string queue, int from, int perPage); 30 | IEnumerable GetFetchedJobIds(string queue, int from, int perPage); 31 | EnqueuedAndFetchedCountDto GetEnqueuedAndFetchedCount(string queue); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/IPersistentJobQueueProvider.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | namespace Hangfire.PostgreSql 23 | { 24 | public interface IPersistentJobQueueProvider 25 | { 26 | IPersistentJobQueue GetJobQueue(); 27 | IPersistentJobQueueMonitoringApi GetJobQueueMonitoringApi(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/JsonParameter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Text.Json; 4 | using Dapper; 5 | using Hangfire.Annotations; 6 | using Npgsql; 7 | using NpgsqlTypes; 8 | 9 | namespace Hangfire.PostgreSql; 10 | 11 | internal class JsonParameter : SqlMapper.ICustomQueryParameter 12 | { 13 | [CanBeNull] private readonly object _value; 14 | private readonly ValueType _type; 15 | 16 | public JsonParameter([CanBeNull] object value) : this(value, ValueType.Object) 17 | { 18 | } 19 | 20 | public JsonParameter([CanBeNull] object value, ValueType type) 21 | { 22 | _value = value; 23 | _type = type; 24 | } 25 | 26 | public void AddParameter(IDbCommand command, string name) 27 | { 28 | string value = _value switch { 29 | string { Length: > 0 } stringValue => stringValue, 30 | string { Length: 0 } or null => GetDefaultValue(), 31 | var _ => JsonSerializer.Serialize(_value), 32 | }; 33 | command.Parameters.Add(new NpgsqlParameter(name, NpgsqlDbType.Jsonb) { Value = value }); 34 | } 35 | 36 | private string GetDefaultValue() 37 | { 38 | return _type switch 39 | { 40 | ValueType.Object => "{}", 41 | ValueType.Array => "[]", 42 | var _ => throw new ArgumentOutOfRangeException(), 43 | }; 44 | } 45 | 46 | public enum ValueType 47 | { 48 | Object, 49 | Array, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/PersistentJobQueueProviderCollection.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | using System.Collections; 24 | using System.Collections.Generic; 25 | 26 | namespace Hangfire.PostgreSql 27 | { 28 | public class PersistentJobQueueProviderCollection : IEnumerable 29 | { 30 | private readonly IPersistentJobQueueProvider _defaultProvider; 31 | 32 | private readonly List _providers = new(); 33 | 34 | private readonly Dictionary _providersByQueue = new(StringComparer.OrdinalIgnoreCase); 35 | 36 | public PersistentJobQueueProviderCollection(IPersistentJobQueueProvider defaultProvider) 37 | { 38 | _defaultProvider = defaultProvider ?? throw new ArgumentNullException(nameof(defaultProvider)); 39 | _providers.Add(_defaultProvider); 40 | } 41 | 42 | public IEnumerator GetEnumerator() 43 | { 44 | return _providers.GetEnumerator(); 45 | } 46 | 47 | IEnumerator IEnumerable.GetEnumerator() 48 | { 49 | return GetEnumerator(); 50 | } 51 | 52 | public void Add(IPersistentJobQueueProvider provider, IEnumerable queues) 53 | { 54 | if (provider == null) 55 | { 56 | throw new ArgumentNullException(nameof(provider)); 57 | } 58 | 59 | if (queues == null) 60 | { 61 | throw new ArgumentNullException(nameof(queues)); 62 | } 63 | 64 | _providers.Add(provider); 65 | 66 | foreach (string queue in queues) 67 | { 68 | _providersByQueue.Add(queue, provider); 69 | } 70 | } 71 | 72 | public IPersistentJobQueueProvider GetProvider(string queue) 73 | { 74 | return _providersByQueue.TryGetValue(queue, out IPersistentJobQueueProvider provider) 75 | ? provider 76 | : _defaultProvider; 77 | } 78 | 79 | public void Remove(string queue) 80 | { 81 | if (!_providersByQueue.ContainsKey(queue)) 82 | { 83 | return; 84 | } 85 | 86 | IPersistentJobQueueProvider provider = _providersByQueue[queue]; 87 | _providersByQueue.Remove(queue); 88 | _providers.Remove(provider); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/PostgreSqlBootstrapperConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | using Npgsql; 24 | 25 | namespace Hangfire.PostgreSql 26 | { 27 | public static class PostgreSqlBootstrapperConfigurationExtensions 28 | { 29 | /// 30 | /// Tells the bootstrapper to use PostgreSQL as a job storage, 31 | /// that can be accessed using the given connection string or 32 | /// its name. 33 | /// 34 | /// Configuration 35 | /// Connection string 36 | [Obsolete("Will be removed in 2.0. Please use UsePostgreSqlStorage(Action) overload.")] 37 | public static IGlobalConfiguration UsePostgreSqlStorage( 38 | this IGlobalConfiguration configuration, 39 | string connectionString) 40 | { 41 | return configuration.UsePostgreSqlStorage(connectionString, null, new PostgreSqlStorageOptions()); 42 | } 43 | 44 | /// 45 | /// Tells the bootstrapper to use PostgreSQL as a job storage 46 | /// with the given options, that can be accessed using the specified 47 | /// connection string. 48 | /// 49 | /// Configuration 50 | /// Connection string 51 | /// Advanced options 52 | [Obsolete("Will be removed in 2.0. Please use UsePostgreSqlStorage(Action, PostgreSqlStorageOptions) overload.")] 53 | public static IGlobalConfiguration UsePostgreSqlStorage( 54 | this IGlobalConfiguration configuration, 55 | string connectionString, 56 | PostgreSqlStorageOptions options) 57 | { 58 | return configuration.UsePostgreSqlStorage(connectionString, null, options); 59 | } 60 | 61 | /// 62 | /// Tells the bootstrapper to use PostgreSQL as a job storage 63 | /// with the given options, that can be accessed using the specified 64 | /// connection string. 65 | /// 66 | /// Configuration 67 | /// Connection string 68 | /// Optional setup action to apply to created connections 69 | /// Advanced options 70 | [Obsolete("Will be removed in 2.0. Please use UsePostgreSqlStorage(Action, PostgreSqlStorageOptions) overload.")] 71 | public static IGlobalConfiguration UsePostgreSqlStorage( 72 | this IGlobalConfiguration configuration, 73 | string connectionString, 74 | Action connectionSetup, 75 | PostgreSqlStorageOptions options) 76 | { 77 | return configuration.UsePostgreSqlStorage(configure => configure.UseNpgsqlConnection(connectionString, connectionSetup), options); 78 | } 79 | 80 | /// 81 | /// Tells the bootstrapper to use PostgreSQL as a job storage 82 | /// with the given options, that can be accessed using the specified 83 | /// connection factory. 84 | /// 85 | /// Configuration 86 | /// Connection factory 87 | /// Advanced options 88 | [Obsolete("Will be removed in 2.0. Please use UsePostgreSqlStorage(Action, PostgreSqlStorageOptions) overload.")] 89 | public static IGlobalConfiguration UsePostgreSqlStorage( 90 | this IGlobalConfiguration configuration, 91 | IConnectionFactory connectionFactory, 92 | PostgreSqlStorageOptions options) 93 | { 94 | return configuration.UsePostgreSqlStorage(configure => configure.UseConnectionFactory(connectionFactory), options); 95 | } 96 | 97 | /// 98 | /// Tells the bootstrapper to use PostgreSQL as a job storage 99 | /// with the given options, that can be accessed using the specified 100 | /// connection factory. 101 | /// 102 | /// Configuration 103 | /// Connection factory 104 | [Obsolete("Will be removed in 2.0. Please use UsePostgreSqlStorage(Action) overload.")] 105 | public static IGlobalConfiguration UsePostgreSqlStorage( 106 | this IGlobalConfiguration configuration, 107 | IConnectionFactory connectionFactory) 108 | { 109 | return configuration.UsePostgreSqlStorage(connectionFactory, new PostgreSqlStorageOptions()); 110 | } 111 | 112 | /// 113 | /// Tells the bootstrapper to use PostgreSQL as the job storage with the default storage options. 114 | /// 115 | /// Configuration instance. 116 | /// Bootstrapper configuration action. 117 | /// instance whose generic type argument is . 118 | public static IGlobalConfiguration UsePostgreSqlStorage(this IGlobalConfiguration configuration, Action configure) 119 | { 120 | return configuration.UsePostgreSqlStorage(configure, new PostgreSqlStorageOptions()); 121 | } 122 | 123 | /// 124 | /// Tells the bootstrapper to use PostgreSQL as the job storage with the specified storage options. 125 | /// 126 | /// Configuration instance. 127 | /// Bootstrapper configuration action. 128 | /// Storage options. 129 | /// instance whose generic type argument is . 130 | /// Throws if is not set up in the action. 131 | public static IGlobalConfiguration UsePostgreSqlStorage(this IGlobalConfiguration configuration, Action configure, PostgreSqlStorageOptions options) 132 | { 133 | if (options == null) 134 | { 135 | throw new ArgumentNullException(nameof(options)); 136 | } 137 | 138 | PostgreSqlBootstrapperOptions bootstrapperOptions = new(options); 139 | configure(bootstrapperOptions); 140 | 141 | IConnectionFactory connectionFactory = bootstrapperOptions.ConnectionFactory; 142 | if (connectionFactory == null) 143 | { 144 | throw new InvalidOperationException("Connection factory is not specified"); 145 | } 146 | 147 | PostgreSqlStorage storage = new(connectionFactory, options); 148 | return configuration.UseStorage(storage); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/PostgreSqlBootstrapperOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Hangfire.Annotations; 3 | using Hangfire.PostgreSql.Factories; 4 | using Npgsql; 5 | 6 | namespace Hangfire.PostgreSql; 7 | 8 | /// 9 | /// Bootstrapper options. 10 | /// 11 | public class PostgreSqlBootstrapperOptions 12 | { 13 | private readonly PostgreSqlStorageOptions _options; 14 | 15 | internal PostgreSqlBootstrapperOptions(PostgreSqlStorageOptions options) 16 | { 17 | _options = options ?? throw new ArgumentNullException(nameof(options)); 18 | } 19 | 20 | [CanBeNull] internal IConnectionFactory ConnectionFactory { get; private set; } 21 | 22 | /// 23 | /// Configures the bootstrapper to use a custom to use for each database action. 24 | /// 25 | /// Instance of . 26 | /// This instance. 27 | /// Throws if is null. 28 | public PostgreSqlBootstrapperOptions UseConnectionFactory(IConnectionFactory connectionFactory) 29 | { 30 | ConnectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); 31 | return this; 32 | } 33 | 34 | /// 35 | /// Configures the bootstrapper to create a new for each database action. 36 | /// 37 | /// Connection string. 38 | /// Optional additional connection setup action to be performed on the created . 39 | /// This instance. 40 | public PostgreSqlBootstrapperOptions UseNpgsqlConnection(string connectionString, [CanBeNull] Action connectionSetup = null) 41 | { 42 | return UseConnectionFactory(new NpgsqlConnectionFactory(connectionString, _options, connectionSetup)); 43 | } 44 | 45 | public PostgreSqlBootstrapperOptions UseNpgsqlConnection(Func getConnectionString, [CanBeNull] Action connectionSetup = null) 46 | { 47 | return UseConnectionFactory(new NpgsqlConnectionFactory(getConnectionString, _options, connectionSetup)); 48 | } 49 | 50 | /// 51 | /// Configures the bootstrapper to use the existing for each database action. 52 | /// 53 | /// to use. 54 | /// This instance. 55 | public PostgreSqlBootstrapperOptions UseExistingNpgsqlConnection(NpgsqlConnection connection) 56 | { 57 | return UseConnectionFactory(new ExistingNpgsqlConnectionFactory(connection, _options)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/PostgreSqlDistributedLock.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | using System.Data; 24 | using System.Diagnostics; 25 | using System.Threading; 26 | using System.Transactions; 27 | using Dapper; 28 | using Hangfire.Annotations; 29 | using Hangfire.Logging; 30 | using Npgsql; 31 | using IsolationLevel = System.Data.IsolationLevel; 32 | 33 | namespace Hangfire.PostgreSql 34 | { 35 | public sealed class PostgreSqlDistributedLock 36 | { 37 | private static readonly ILog _logger = LogProvider.GetCurrentClassLogger(); 38 | 39 | private static void Log(string resource, string message, Exception ex) 40 | { 41 | bool isConcurrencyError = ex is PostgresException { SqlState: PostgresErrorCodes.SerializationFailure }; 42 | _logger.Log(isConcurrencyError ? LogLevel.Trace : LogLevel.Warn, () => $"{resource}: {message}", ex); 43 | } 44 | 45 | internal static void Acquire(IDbConnection connection, string resource, TimeSpan timeout, PostgreSqlStorageOptions options) 46 | { 47 | if (connection == null) 48 | { 49 | throw new ArgumentNullException(nameof(connection)); 50 | } 51 | 52 | if (string.IsNullOrEmpty(resource)) 53 | { 54 | throw new ArgumentNullException(nameof(resource)); 55 | } 56 | 57 | if (options == null) 58 | { 59 | throw new ArgumentNullException(nameof(options)); 60 | } 61 | 62 | if (connection.State != ConnectionState.Open) 63 | { 64 | // When we are passing a closed connection to Dapper's Execute method, 65 | // it kindly opens it for us, but after command execution, it will be closed 66 | // automatically, and our just-acquired application lock will immediately 67 | // be released. This is not behavior we want to achieve, so let's throw an 68 | // exception instead. 69 | throw new InvalidOperationException("Connection must be open before acquiring a distributed lock."); 70 | } 71 | 72 | LockHandler.Lock(resource, timeout, connection, options); 73 | } 74 | 75 | internal static void Release(IDbConnection connection, string resource, PostgreSqlStorageOptions options) 76 | { 77 | if (connection == null) 78 | { 79 | throw new ArgumentNullException(nameof(connection)); 80 | } 81 | 82 | if (resource == null) 83 | { 84 | throw new ArgumentNullException(nameof(resource)); 85 | } 86 | 87 | if (options == null) 88 | { 89 | throw new ArgumentNullException(nameof(options)); 90 | } 91 | 92 | if (!LockHandler.TryRemoveLock(resource, connection, options, false)) 93 | { 94 | throw new PostgreSqlDistributedLockException(resource); 95 | } 96 | } 97 | 98 | private static class LockHandler 99 | { 100 | public static void Lock(string resource, TimeSpan timeout, IDbConnection connection, PostgreSqlStorageOptions options) 101 | { 102 | Stopwatch lockAcquiringTime = Stopwatch.StartNew(); 103 | 104 | bool tryAcquireLock = true; 105 | Func tryLock = options.UseNativeDatabaseTransactions 106 | ? TransactionLockHandler.TryLock 107 | : UpdateCountLockHandler.TryLock; 108 | 109 | while (tryAcquireLock) 110 | { 111 | if (connection.State != ConnectionState.Open) 112 | { 113 | connection.Open(); 114 | } 115 | 116 | TryRemoveLock(resource, connection, options, true); 117 | 118 | try 119 | { 120 | if (tryLock(connection, options.SchemaName, resource)) 121 | { 122 | return; 123 | } 124 | } 125 | catch (Exception ex) 126 | { 127 | Log(resource, "Failed to acquire lock", ex); 128 | } 129 | 130 | if (lockAcquiringTime.ElapsedMilliseconds > timeout.TotalMilliseconds) 131 | { 132 | tryAcquireLock = false; 133 | } 134 | else 135 | { 136 | int sleepDuration = (int)(timeout.TotalMilliseconds - lockAcquiringTime.ElapsedMilliseconds); 137 | if (sleepDuration > 1000) 138 | { 139 | sleepDuration = 1000; 140 | } 141 | 142 | if (sleepDuration > 0) 143 | { 144 | Thread.Sleep(sleepDuration); 145 | } 146 | else 147 | { 148 | tryAcquireLock = false; 149 | } 150 | } 151 | } 152 | 153 | throw new PostgreSqlDistributedLockException(resource); 154 | } 155 | 156 | public static bool TryRemoveLock(string resource, IDbConnection connection, PostgreSqlStorageOptions options, bool onlyExpired) 157 | { 158 | IDbTransaction trx = null; 159 | try 160 | { 161 | // Non-expired locks are removed only when releasing them. Transaction is not needed in that case. 162 | if (onlyExpired && options.UseNativeDatabaseTransactions) 163 | { 164 | trx = TransactionLockHandler.BeginTransactionIfNotPresent(connection); 165 | } 166 | 167 | DateTime timeout = onlyExpired ? DateTime.UtcNow - options.DistributedLockTimeout : DateTime.MaxValue; 168 | 169 | int rowsAffected = connection.Execute($@"DELETE FROM ""{options.SchemaName}"".""lock"" WHERE ""resource"" = @Resource AND ""acquired"" < @Timeout", 170 | new { 171 | Resource = resource, 172 | Timeout = timeout, 173 | }, trx); 174 | 175 | trx?.Commit(); 176 | 177 | return rowsAffected >= 0; 178 | } 179 | catch (Exception ex) 180 | { 181 | Log(resource, "Failed to remove lock", ex); 182 | return false; 183 | } 184 | finally 185 | { 186 | trx?.Dispose(); 187 | } 188 | } 189 | } 190 | 191 | private static class TransactionLockHandler 192 | { 193 | public static bool TryLock(IDbConnection connection, string schemaName, string resource) 194 | { 195 | IDbTransaction trx = null; 196 | try 197 | { 198 | trx = BeginTransactionIfNotPresent(connection); 199 | 200 | int rowsAffected = connection.Execute($@" 201 | INSERT INTO ""{schemaName}"".""lock""(""resource"", ""acquired"") 202 | SELECT @Resource, @Acquired 203 | WHERE NOT EXISTS ( 204 | SELECT 1 FROM ""{schemaName}"".""lock"" 205 | WHERE ""resource"" = @Resource 206 | ) 207 | ON CONFLICT DO NOTHING; 208 | ", 209 | new { 210 | Resource = resource, 211 | Acquired = DateTime.UtcNow, 212 | }, trx); 213 | trx?.Commit(); 214 | 215 | return rowsAffected > 0; 216 | } 217 | finally 218 | { 219 | trx?.Dispose(); 220 | } 221 | } 222 | 223 | [CanBeNull] 224 | public static IDbTransaction BeginTransactionIfNotPresent(IDbConnection connection) 225 | { 226 | // If transaction scope was created outside of hangfire, the newly-opened connection is automatically enlisted into the transaction. 227 | // Starting a new transaction throws "A transaction is already in progress; nested/concurrent transactions aren't supported." in that case. 228 | return Transaction.Current == null ? connection.BeginTransaction(IsolationLevel.ReadCommitted) : null; 229 | } 230 | } 231 | 232 | private static class UpdateCountLockHandler 233 | { 234 | public static bool TryLock(IDbConnection connection, string schemaName, string resource) 235 | { 236 | connection.Execute($@" 237 | INSERT INTO ""{schemaName}"".""lock""(""resource"", ""updatecount"", ""acquired"") 238 | SELECT @Resource, 0, @Acquired 239 | WHERE NOT EXISTS ( 240 | SELECT 1 FROM ""{schemaName}"".""lock"" 241 | WHERE ""resource"" = @Resource 242 | ) 243 | ON CONFLICT DO NOTHING; 244 | ", new { 245 | Resource = resource, 246 | Acquired = DateTime.UtcNow, 247 | }); 248 | 249 | int rowsAffected = connection.Execute( 250 | $@"UPDATE ""{schemaName}"".""lock"" SET ""updatecount"" = 1 WHERE ""updatecount"" = 0 AND ""resource"" = @Resource", 251 | new { Resource = resource }); 252 | 253 | return rowsAffected > 0; 254 | } 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/PostgreSqlDistributedLockException.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | using Hangfire.Storage; 24 | 25 | namespace Hangfire.PostgreSql 26 | { 27 | [Serializable] 28 | public class PostgreSqlDistributedLockException : DistributedLockTimeoutException 29 | { 30 | public PostgreSqlDistributedLockException(string resource) : base(resource) 31 | { 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/PostgreSqlFetchedJob.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | using System.Threading; 24 | using Dapper; 25 | using Hangfire.Logging; 26 | using Hangfire.PostgreSql.Utils; 27 | using Hangfire.Storage; 28 | 29 | namespace Hangfire.PostgreSql 30 | { 31 | public class PostgreSqlFetchedJob : IFetchedJob 32 | { 33 | private readonly ILog _logger = LogProvider.GetLogger(typeof(PostgreSqlFetchedJob)); 34 | 35 | private readonly PostgreSqlStorage _storage; 36 | private bool _disposed; 37 | private bool _removedFromQueue; 38 | private bool _requeued; 39 | 40 | private readonly object _syncRoot = new object(); 41 | private long _lastHeartbeat; 42 | private readonly TimeSpan _interval; 43 | 44 | public PostgreSqlFetchedJob( 45 | PostgreSqlStorage storage, 46 | long id, 47 | string jobId, 48 | string queue, 49 | DateTime? fetchedAt) 50 | { 51 | _storage = storage ?? throw new ArgumentNullException(nameof(storage)); 52 | 53 | Id = id; 54 | JobId = jobId ?? throw new ArgumentNullException(nameof(jobId)); 55 | Queue = queue ?? throw new ArgumentNullException(nameof(queue)); 56 | FetchedAt = fetchedAt ?? throw new ArgumentNullException(nameof(fetchedAt)); 57 | 58 | if (storage.Options.UseSlidingInvisibilityTimeout) 59 | { 60 | _lastHeartbeat = TimestampHelper.GetTimestamp(); 61 | _interval = TimeSpan.FromSeconds(storage.Options.InvisibilityTimeout.TotalSeconds / 5); 62 | storage.HeartbeatProcess.Track(this); 63 | } 64 | } 65 | 66 | public long Id { get; } 67 | public string Queue { get; } 68 | public string JobId { get; } 69 | internal DateTime? FetchedAt { get; private set; } 70 | 71 | public void RemoveFromQueue() 72 | { 73 | lock (_syncRoot) 74 | { 75 | if (!FetchedAt.HasValue) 76 | { 77 | return; 78 | } 79 | 80 | _storage.UseConnection(null, connection => connection.Execute($@" 81 | DELETE FROM ""{_storage.Options.SchemaName}"".""jobqueue"" WHERE ""id"" = @Id AND ""fetchedat"" = @FetchedAt; 82 | ", 83 | new { Id, FetchedAt })); 84 | 85 | _removedFromQueue = true; 86 | } 87 | } 88 | 89 | public void Requeue() 90 | { 91 | lock (_syncRoot) 92 | { 93 | if (!FetchedAt.HasValue) 94 | { 95 | return; 96 | } 97 | 98 | _storage.UseConnection(null, connection => connection.Execute($@" 99 | UPDATE ""{_storage.Options.SchemaName}"".""jobqueue"" 100 | SET ""fetchedat"" = NULL 101 | WHERE ""id"" = @Id AND ""fetchedat"" = @FetchedAt; 102 | ", 103 | new { Id, FetchedAt })); 104 | 105 | FetchedAt = null; 106 | _requeued = true; 107 | } 108 | } 109 | 110 | public void Dispose() 111 | { 112 | if (_disposed) 113 | { 114 | return; 115 | } 116 | 117 | _disposed = true; 118 | 119 | DisposeTimer(); 120 | 121 | lock (_syncRoot) 122 | { 123 | if (!_removedFromQueue && !_requeued) 124 | { 125 | Requeue(); 126 | } 127 | } 128 | } 129 | 130 | internal void DisposeTimer() 131 | { 132 | if (_storage.Options.UseSlidingInvisibilityTimeout) 133 | { 134 | _storage.HeartbeatProcess.Untrack(this); 135 | } 136 | } 137 | 138 | internal void ExecuteKeepAliveQueryIfRequired() 139 | { 140 | var now = TimestampHelper.GetTimestamp(); 141 | 142 | if (TimestampHelper.Elapsed(now, Interlocked.Read(ref _lastHeartbeat)) < _interval) 143 | { 144 | return; 145 | } 146 | 147 | lock (_syncRoot) 148 | { 149 | if (!FetchedAt.HasValue) 150 | { 151 | return; 152 | } 153 | 154 | if (_requeued || _removedFromQueue) 155 | { 156 | return; 157 | } 158 | 159 | string updateFetchAtSql = $@" 160 | UPDATE ""{_storage.Options.SchemaName}"".""jobqueue"" 161 | SET ""fetchedat"" = NOW() 162 | WHERE ""id"" = @id AND ""fetchedat"" = @fetchedAt 163 | RETURNING ""fetchedat"" AS ""FetchedAt""; 164 | "; 165 | 166 | try 167 | { 168 | _storage.UseConnection(null, connection => 169 | { 170 | FetchedAt = connection.ExecuteScalar(updateFetchAtSql, 171 | new { queue = Queue, id = Id, fetchedAt = FetchedAt }); 172 | }); 173 | 174 | if (!FetchedAt.HasValue) 175 | { 176 | _logger.Warn( 177 | $"Background job identifier '{JobId}' was fetched by another worker, will not execute keep alive."); 178 | } 179 | 180 | _logger.Trace($"Keep-alive query for message {Id} sent"); 181 | Interlocked.Exchange(ref _lastHeartbeat, now); 182 | } 183 | catch (Exception ex) when (ex.IsCatchableExceptionType()) 184 | { 185 | _logger.DebugException($"Unable to execute keep-alive query for message {Id}", ex); 186 | } 187 | } 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/PostgreSqlHeartbeatProcess.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | using System.Collections.Concurrent; 24 | using System.Threading; 25 | using Hangfire.Common; 26 | using Hangfire.Server; 27 | 28 | namespace Hangfire.PostgreSql 29 | { 30 | #pragma warning disable CS0618 31 | internal sealed class PostgreSqlHeartbeatProcess : IServerComponent, IBackgroundProcess 32 | #pragma warning restore CS0618 33 | { 34 | private readonly ConcurrentDictionary _items = new(); 35 | 36 | public void Track(PostgreSqlFetchedJob item) 37 | { 38 | _items.TryAdd(item, null); 39 | } 40 | 41 | public void Untrack(PostgreSqlFetchedJob item) 42 | { 43 | _items.TryRemove(item, out var _); 44 | } 45 | 46 | public void Execute(CancellationToken cancellationToken) 47 | { 48 | foreach (var item in _items) 49 | { 50 | item.Key.ExecuteKeepAliveQueryIfRequired(); 51 | } 52 | 53 | cancellationToken.Wait(TimeSpan.FromSeconds(1)); 54 | } 55 | 56 | public void Execute(BackgroundProcessContext context) 57 | { 58 | Execute(context.StoppingToken); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/PostgreSqlJobQueueMonitoringApi.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | using System.Collections.Generic; 24 | using System.Linq; 25 | using Dapper; 26 | 27 | namespace Hangfire.PostgreSql 28 | { 29 | internal class PostgreSqlJobQueueMonitoringApi : IPersistentJobQueueMonitoringApi 30 | { 31 | private readonly PostgreSqlStorage _storage; 32 | 33 | public PostgreSqlJobQueueMonitoringApi(PostgreSqlStorage storage) 34 | { 35 | _storage = storage ?? throw new ArgumentNullException(nameof(storage)); 36 | } 37 | 38 | public IEnumerable GetQueues() 39 | { 40 | string sqlQuery = $@"SELECT DISTINCT ""queue"" FROM ""{_storage.Options.SchemaName}"".""jobqueue"""; 41 | return _storage.UseConnection(null, connection => connection.Query(sqlQuery).ToList()); 42 | } 43 | 44 | public IEnumerable GetEnqueuedJobIds(string queue, int from, int perPage) 45 | { 46 | return GetQueuedOrFetchedJobIds(queue, false, from, perPage); 47 | } 48 | 49 | public IEnumerable GetFetchedJobIds(string queue, int from, int perPage) 50 | { 51 | return GetQueuedOrFetchedJobIds(queue, true, from, perPage); 52 | } 53 | 54 | public EnqueuedAndFetchedCountDto GetEnqueuedAndFetchedCount(string queue) 55 | { 56 | string sqlQuery = $@" 57 | SELECT ( 58 | SELECT COUNT(*) 59 | FROM ""{_storage.Options.SchemaName}"".""jobqueue"" 60 | WHERE ""fetchedat"" IS NULL 61 | AND ""queue"" = @Queue 62 | ) ""EnqueuedCount"", 63 | ( 64 | SELECT COUNT(*) 65 | FROM ""{_storage.Options.SchemaName}"".""jobqueue"" 66 | WHERE ""fetchedat"" IS NOT NULL 67 | AND ""queue"" = @Queue 68 | ) ""FetchedCount""; 69 | "; 70 | 71 | (long enqueuedCount, long fetchedCount) = _storage.UseConnection(null, connection => 72 | connection.QuerySingle<(long EnqueuedCount, long FetchedCount)>(sqlQuery, new { Queue = queue })); 73 | 74 | return new EnqueuedAndFetchedCountDto { 75 | EnqueuedCount = enqueuedCount, 76 | FetchedCount = fetchedCount, 77 | }; 78 | } 79 | 80 | private IEnumerable GetQueuedOrFetchedJobIds(string queue, bool fetched, int from, int perPage) 81 | { 82 | string sqlQuery = $@" 83 | SELECT j.""id"" 84 | FROM ""{_storage.Options.SchemaName}"".""jobqueue"" jq 85 | LEFT JOIN ""{_storage.Options.SchemaName}"".""job"" j ON jq.""jobid"" = j.""id"" 86 | WHERE jq.""queue"" = @Queue 87 | AND jq.""fetchedat"" {(fetched ? "IS NOT NULL" : "IS NULL")} 88 | AND j.""id"" IS NOT NULL 89 | ORDER BY jq.""fetchedat"", jq.""jobid"" 90 | LIMIT @Limit OFFSET @Offset; 91 | "; 92 | 93 | return _storage.UseConnection(null, connection => connection.Query(sqlQuery, 94 | new { Queue = queue, Offset = from, Limit = perPage }) 95 | .ToList()); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/PostgreSqlJobQueueProvider.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | 24 | namespace Hangfire.PostgreSql 25 | { 26 | public class PostgreSqlJobQueueProvider : IPersistentJobQueueProvider 27 | { 28 | public PostgreSqlJobQueueProvider(PostgreSqlStorage storage, PostgreSqlStorageOptions options) 29 | { 30 | Storage = storage ?? throw new ArgumentNullException(nameof(storage)); 31 | Options = options ?? throw new ArgumentNullException(nameof(options)); 32 | } 33 | 34 | public PostgreSqlStorageOptions Options { get; } 35 | public PostgreSqlStorage Storage { get; } 36 | 37 | public IPersistentJobQueue GetJobQueue() 38 | { 39 | return new PostgreSqlJobQueue(Storage); 40 | } 41 | 42 | public IPersistentJobQueueMonitoringApi GetJobQueueMonitoringApi() 43 | { 44 | return new PostgreSqlJobQueueMonitoringApi(Storage); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/PostgreSqlObjectsInstaller.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | using System.Data; 24 | using System.Globalization; 25 | using System.IO; 26 | using System.Reflection; 27 | using System.Resources; 28 | using Hangfire.Logging; 29 | using Npgsql; 30 | 31 | namespace Hangfire.PostgreSql 32 | { 33 | public static class PostgreSqlObjectsInstaller 34 | { 35 | private static readonly ILog _logger = LogProvider.GetLogger(typeof(PostgreSqlStorage)); 36 | 37 | public static void Install(NpgsqlConnection connection, string schemaName = "hangfire") 38 | { 39 | if (connection == null) 40 | { 41 | throw new ArgumentNullException(nameof(connection)); 42 | } 43 | 44 | _logger.Info("Start installing Hangfire SQL objects..."); 45 | 46 | // starts with version 3 to keep in check with Hangfire SqlServer, but I couldn't keep up with that idea after all; 47 | int version = 3; 48 | int previousVersion = 1; 49 | do 50 | { 51 | try 52 | { 53 | string script; 54 | try 55 | { 56 | script = GetStringResource(typeof(PostgreSqlObjectsInstaller).GetTypeInfo().Assembly, 57 | $"Hangfire.PostgreSql.Scripts.Install.v{version.ToString(CultureInfo.InvariantCulture)}.sql"); 58 | } 59 | catch (MissingManifestResourceException) 60 | { 61 | break; 62 | } 63 | 64 | if (schemaName != "hangfire") 65 | { 66 | script = script.Replace("'hangfire'", $"'{schemaName}'").Replace(@"""hangfire""", $@"""{schemaName}"""); 67 | } 68 | 69 | if (!VersionAlreadyApplied(connection, schemaName, version)) 70 | { 71 | string commandText = $@"{script}; UPDATE ""{schemaName}"".""schema"" SET ""version"" = @Version WHERE ""version"" = @PreviousVersion"; 72 | using NpgsqlTransaction transaction = connection.BeginTransaction(IsolationLevel.Serializable); 73 | using NpgsqlCommand command = new(commandText, connection, transaction); 74 | command.CommandTimeout = 120; 75 | command.Parameters.Add(new NpgsqlParameter("Version", version)); 76 | command.Parameters.Add(new NpgsqlParameter("PreviousVersion", previousVersion)); 77 | try 78 | { 79 | command.ExecuteNonQuery(); 80 | transaction.Commit(); 81 | } 82 | catch (PostgresException ex) 83 | { 84 | if ((ex.MessageText ?? "") != "version-already-applied") 85 | { 86 | throw; 87 | } 88 | } 89 | } 90 | } 91 | catch (Exception ex) 92 | { 93 | if (ex.Source.Equals("Npgsql")) 94 | { 95 | _logger.ErrorException("Error while executing install/upgrade", ex); 96 | } 97 | 98 | throw; 99 | } 100 | 101 | previousVersion = version; 102 | version++; 103 | } while (true); 104 | 105 | _logger.Info("Hangfire SQL objects installed."); 106 | } 107 | 108 | private static bool VersionAlreadyApplied(NpgsqlConnection connection, string schemaName, int version) 109 | { 110 | try 111 | { 112 | using NpgsqlCommand command = new($@"SELECT true ""VersionAlreadyApplied"" FROM ""{schemaName}"".""schema"" WHERE ""version"" >= $1", connection); 113 | command.Parameters.Add(new NpgsqlParameter { Value = version }); 114 | object result = command.ExecuteScalar(); 115 | if (true.Equals(result)) 116 | { 117 | return true; 118 | } 119 | } 120 | catch (PostgresException ex) 121 | { 122 | if (ex.SqlState.Equals(PostgresErrorCodes.UndefinedTable)) //42P01: Relation (table) does not exist. So no schema table yet. 123 | { 124 | return false; 125 | } 126 | 127 | throw; 128 | } 129 | 130 | return false; 131 | } 132 | 133 | private static string GetStringResource(Assembly assembly, string resourceName) 134 | { 135 | using Stream stream = assembly.GetManifestResourceStream(resourceName); 136 | if (stream == null) 137 | { 138 | throw new MissingManifestResourceException($"Requested resource `{resourceName}` was not found in the assembly `{assembly}`."); 139 | } 140 | 141 | using StreamReader reader = new(stream); 142 | return reader.ReadToEnd(); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/PostgreSqlStorageOptions.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | 24 | namespace Hangfire.PostgreSql 25 | { 26 | public class PostgreSqlStorageOptions 27 | { 28 | private static readonly TimeSpan _minimumQueuePollInterval = TimeSpan.FromMilliseconds(50); 29 | 30 | private int _deleteExpiredBatchSize; 31 | private TimeSpan _distributedLockTimeout; 32 | private TimeSpan _invisibilityTimeout; 33 | private TimeSpan _jobExpirationCheckInterval; 34 | private TimeSpan _queuePollInterval; 35 | private TimeSpan _transactionSerializationTimeout; 36 | private TimeSpan _countersAggregateInterval; 37 | 38 | public PostgreSqlStorageOptions() 39 | { 40 | QueuePollInterval = TimeSpan.FromSeconds(15); 41 | InvisibilityTimeout = TimeSpan.FromMinutes(30); 42 | DistributedLockTimeout = TimeSpan.FromMinutes(10); 43 | TransactionSynchronisationTimeout = TimeSpan.FromMilliseconds(500); 44 | JobExpirationCheckInterval = TimeSpan.FromHours(1); 45 | CountersAggregateInterval = TimeSpan.FromMinutes(5); 46 | SchemaName = "hangfire"; 47 | AllowUnsafeValues = false; 48 | UseNativeDatabaseTransactions = true; 49 | PrepareSchemaIfNecessary = true; 50 | EnableTransactionScopeEnlistment = true; 51 | DeleteExpiredBatchSize = 1000; 52 | UseSlidingInvisibilityTimeout = false; 53 | } 54 | 55 | public TimeSpan QueuePollInterval 56 | { 57 | get => _queuePollInterval; 58 | set { 59 | ThrowIfValueIsLowerThan(_minimumQueuePollInterval, value, nameof(QueuePollInterval)); 60 | _queuePollInterval = value; 61 | } 62 | } 63 | 64 | public TimeSpan InvisibilityTimeout 65 | { 66 | get => _invisibilityTimeout; 67 | set { 68 | ThrowIfValueIsNotPositive(value, nameof(InvisibilityTimeout)); 69 | _invisibilityTimeout = value; 70 | } 71 | } 72 | 73 | public TimeSpan DistributedLockTimeout 74 | { 75 | get => _distributedLockTimeout; 76 | set { 77 | ThrowIfValueIsNotPositive(value, nameof(DistributedLockTimeout)); 78 | _distributedLockTimeout = value; 79 | } 80 | } 81 | 82 | // ReSharper disable once IdentifierTypo 83 | public TimeSpan TransactionSynchronisationTimeout 84 | { 85 | get => _transactionSerializationTimeout; 86 | set { 87 | ThrowIfValueIsNotPositive(value, nameof(TransactionSynchronisationTimeout)); 88 | _transactionSerializationTimeout = value; 89 | } 90 | } 91 | 92 | public TimeSpan JobExpirationCheckInterval 93 | { 94 | get => _jobExpirationCheckInterval; 95 | set { 96 | ThrowIfValueIsNotPositive(value, nameof(JobExpirationCheckInterval)); 97 | _jobExpirationCheckInterval = value; 98 | } 99 | } 100 | 101 | public TimeSpan CountersAggregateInterval 102 | { 103 | get => _countersAggregateInterval; 104 | set { 105 | ThrowIfValueIsNotPositive(value, nameof(CountersAggregateInterval)); 106 | _countersAggregateInterval = value; 107 | } 108 | } 109 | 110 | /// 111 | /// Gets or sets the number of records deleted in a single batch in expiration manager 112 | /// 113 | public int DeleteExpiredBatchSize 114 | { 115 | get => _deleteExpiredBatchSize; 116 | set { 117 | ThrowIfValueIsNotPositive(value, nameof(DeleteExpiredBatchSize)); 118 | _deleteExpiredBatchSize = value; 119 | } 120 | } 121 | 122 | public bool AllowUnsafeValues { get; set; } 123 | public bool UseNativeDatabaseTransactions { get; set; } 124 | public bool PrepareSchemaIfNecessary { get; set; } 125 | public string SchemaName { get; set; } 126 | public bool EnableTransactionScopeEnlistment { get; set; } 127 | public bool EnableLongPolling { get; set; } 128 | 129 | /// 130 | /// Apply a sliding invisibility timeout where the last fetched time is continually updated in the background. 131 | /// This allows a lower invisibility timeout to be used with longer running jobs 132 | /// IMPORTANT: If option is used, then sliding invisiblity timeouts will not work 133 | /// since the background storage processes are not run (which is used to update the invisibility timeouts) 134 | /// 135 | public bool UseSlidingInvisibilityTimeout { get; set; } 136 | 137 | private static void ThrowIfValueIsNotPositive(TimeSpan value, string fieldName) 138 | { 139 | string message = $"The {fieldName} property value should be positive. Given: {value}."; 140 | 141 | if (value == TimeSpan.Zero) 142 | { 143 | throw new ArgumentException(message, nameof(value)); 144 | } 145 | 146 | if (value != value.Duration()) 147 | { 148 | throw new ArgumentException(message, nameof(value)); 149 | } 150 | } 151 | 152 | private void ThrowIfValueIsLowerThan(TimeSpan minValue, TimeSpan value, string fieldName) 153 | { 154 | if (!AllowUnsafeValues) 155 | { 156 | string message = $"The {fieldName} property value seems to be too low ({value}, lower than suggested minimum of {minValue}). Consider increasing it. If you really need to have such a low value, please set {nameof(PostgreSqlStorageOptions)}.{nameof(AllowUnsafeValues)} to true."; 157 | 158 | if (value < minValue) 159 | { 160 | throw new ArgumentException(message, nameof(value)); 161 | } 162 | } 163 | 164 | ThrowIfValueIsNotPositive(value, fieldName); 165 | } 166 | 167 | private static void ThrowIfValueIsNotPositive(int value, string fieldName) 168 | { 169 | if (value <= 0) 170 | { 171 | throw new ArgumentException($"The {fieldName} property value should be positive. Given: {value}."); 172 | } 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Hangfire.PostgreSql.Tests")] 4 | // Allow the generation of mocks for internal types 5 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 6 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v10.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | -- 3 | -- Table structure for table `Schema` 4 | -- 5 | 6 | DO 7 | $$ 8 | BEGIN 9 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 10) THEN 10 | RAISE EXCEPTION 'version-already-applied'; 11 | END IF; 12 | END 13 | $$; 14 | 15 | ALTER TABLE "jobqueue" 16 | ALTER COLUMN "queue" TYPE TEXT; 17 | 18 | RESET search_path; -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v11.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | 3 | DO 4 | $$ 5 | BEGIN 6 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 11) THEN 7 | RAISE EXCEPTION 'version-already-applied'; 8 | END IF; 9 | END 10 | $$; 11 | 12 | ALTER TABLE "counter" 13 | ALTER COLUMN id TYPE BIGINT; 14 | ALTER TABLE "hash" 15 | ALTER COLUMN id TYPE BIGINT; 16 | ALTER TABLE "job" 17 | ALTER COLUMN id TYPE BIGINT; 18 | ALTER TABLE "job" 19 | ALTER COLUMN stateid TYPE BIGINT; 20 | ALTER TABLE "state" 21 | ALTER COLUMN id TYPE BIGINT; 22 | ALTER TABLE "state" 23 | ALTER COLUMN jobid TYPE BIGINT; 24 | ALTER TABLE "jobparameter" 25 | ALTER COLUMN id TYPE BIGINT; 26 | ALTER TABLE "jobparameter" 27 | ALTER COLUMN jobid TYPE BIGINT; 28 | ALTER TABLE "jobqueue" 29 | ALTER COLUMN id TYPE BIGINT; 30 | ALTER TABLE "jobqueue" 31 | ALTER COLUMN jobid TYPE BIGINT; 32 | ALTER TABLE "list" 33 | ALTER COLUMN id TYPE BIGINT; 34 | ALTER TABLE "set" 35 | ALTER COLUMN id TYPE BIGINT; 36 | 37 | RESET search_path; -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v12.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | 3 | DO 4 | $$ 5 | BEGIN 6 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 12) THEN 7 | RAISE EXCEPTION 'version-already-applied'; 8 | END IF; 9 | END 10 | $$; 11 | 12 | ALTER TABLE "counter" 13 | ALTER COLUMN "key" TYPE TEXT; 14 | ALTER TABLE "hash" 15 | ALTER COLUMN "key" TYPE TEXT; 16 | ALTER TABLE "hash" 17 | ALTER COLUMN field TYPE TEXT; 18 | ALTER TABLE "job" 19 | ALTER COLUMN statename TYPE TEXT; 20 | ALTER TABLE "list" 21 | ALTER COLUMN "key" TYPE TEXT; 22 | ALTER TABLE "server" 23 | ALTER COLUMN id TYPE TEXT; 24 | ALTER TABLE "set" 25 | ALTER COLUMN "key" TYPE TEXT; 26 | ALTER TABLE "jobparameter" 27 | ALTER COLUMN "name" TYPE TEXT; 28 | ALTER TABLE "state" 29 | ALTER COLUMN "name" TYPE TEXT; 30 | ALTER TABLE "state" 31 | ALTER COLUMN reason TYPE TEXT; 32 | 33 | RESET search_path; -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v13.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | 3 | 4 | 5 | DO 6 | $$ 7 | BEGIN 8 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 13) THEN 9 | RAISE EXCEPTION 'version-already-applied'; 10 | END IF; 11 | END 12 | $$; 13 | 14 | CREATE INDEX IF NOT EXISTS jobqueue_queue_fetchat_jobId ON jobqueue USING btree (queue asc, fetchedat asc nulls last, jobid asc); 15 | 16 | RESET search_path; -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v14.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | 3 | DO 4 | $$ 5 | BEGIN 6 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 14) THEN 7 | RAISE EXCEPTION 'version-already-applied'; 8 | END IF; 9 | END 10 | $$; 11 | 12 | do 13 | $$ 14 | DECLARE 15 | BEGIN 16 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".job_id_seq AS bigint MAXVALUE 9223372036854775807'); 17 | EXCEPTION 18 | WHEN syntax_error THEN 19 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".job_id_seq MAXVALUE 9223372036854775807'); 20 | END; 21 | $$; 22 | 23 | RESET search_path; 24 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v15.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | 3 | DO 4 | $$ 5 | BEGIN 6 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 15) THEN 7 | RAISE EXCEPTION 'version-already-applied'; 8 | END IF; 9 | END 10 | $$; 11 | 12 | CREATE INDEX ix_hangfire_job_expireat ON "job" (expireat); 13 | CREATE INDEX ix_hangfire_list_expireat ON "list" (expireat); 14 | CREATE INDEX ix_hangfire_set_expireat ON "set" (expireat); 15 | CREATE INDEX ix_hangfire_hash_expireat ON "hash" (expireat); 16 | 17 | RESET search_path; 18 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v16.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | 3 | DO 4 | $$ 5 | BEGIN 6 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 16) THEN 7 | RAISE EXCEPTION 'version-already-applied'; 8 | END IF; 9 | END 10 | $$; 11 | 12 | -- Note: job_id_seq is already bigint as per migration script v14 13 | DO 14 | $$ 15 | DECLARE 16 | BEGIN 17 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".counter_id_seq AS bigint MAXVALUE 9223372036854775807'); 18 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".hash_id_seq AS bigint MAXVALUE 9223372036854775807'); 19 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".jobparameter_id_seq AS bigint MAXVALUE 9223372036854775807'); 20 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".jobqueue_id_seq AS bigint MAXVALUE 9223372036854775807'); 21 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".list_id_seq AS bigint MAXVALUE 9223372036854775807'); 22 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".set_id_seq AS bigint MAXVALUE 9223372036854775807'); 23 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".state_id_seq AS bigint MAXVALUE 9223372036854775807'); 24 | EXCEPTION 25 | WHEN syntax_error THEN 26 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".counter_id_seq MAXVALUE 9223372036854775807'); 27 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".hash_id_seq MAXVALUE 9223372036854775807'); 28 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".jobparameter_id_seq MAXVALUE 9223372036854775807'); 29 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".jobqueue_id_seq MAXVALUE 9223372036854775807'); 30 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".list_id_seq MAXVALUE 9223372036854775807'); 31 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".set_id_seq MAXVALUE 9223372036854775807'); 32 | EXECUTE ('ALTER SEQUENCE "' || 'hangfire' || '".state_id_seq MAXVALUE 9223372036854775807'); 33 | END 34 | $$; 35 | 36 | RESET search_path; 37 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v17.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | 3 | DO 4 | $$ 5 | BEGIN 6 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 17) THEN 7 | RAISE EXCEPTION 'version-already-applied'; 8 | END IF; 9 | END 10 | $$; 11 | 12 | CREATE INDEX IF NOT EXISTS ix_hangfire_set_key_score ON "set" (key, score); 13 | 14 | RESET search_path; 15 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v18.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | 3 | DO 4 | $$ 5 | BEGIN 6 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 18) THEN 7 | RAISE EXCEPTION 'version-already-applied'; 8 | END IF; 9 | END 10 | $$; 11 | 12 | CREATE TABLE aggregatedcounter ( 13 | "id" bigserial PRIMARY KEY NOT NULL, 14 | "key" text NOT NULL UNIQUE, 15 | "value" int8 NOT NULL, 16 | "expireat" timestamp 17 | ); 18 | 19 | RESET search_path; 20 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v19.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | 3 | DO $$ 4 | BEGIN 5 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 19) THEN 6 | RAISE EXCEPTION 'version-already-applied'; 7 | END IF; 8 | END $$; 9 | 10 | ALTER TABLE "aggregatedcounter" ALTER COLUMN "expireat" TYPE timestamp with time zone; 11 | ALTER TABLE "counter" ALTER COLUMN "expireat" TYPE timestamp with time zone; 12 | ALTER TABLE "hash" ALTER COLUMN "expireat" TYPE timestamp with time zone; 13 | ALTER TABLE "job" ALTER COLUMN "createdat" TYPE timestamp with time zone; 14 | ALTER TABLE "job" ALTER COLUMN "expireat" TYPE timestamp with time zone; 15 | ALTER TABLE "jobqueue" ALTER COLUMN "fetchedat" TYPE timestamp with time zone; 16 | ALTER TABLE "list" ALTER COLUMN "expireat" TYPE timestamp with time zone; 17 | ALTER TABLE "lock" ALTER COLUMN "acquired" TYPE timestamp with time zone; 18 | ALTER TABLE "server" ALTER COLUMN "lastheartbeat" TYPE timestamp with time zone; 19 | ALTER TABLE "set" ALTER COLUMN "expireat" TYPE timestamp with time zone; 20 | ALTER TABLE "state" ALTER COLUMN "createdat" TYPE timestamp with time zone; 21 | 22 | RESET search_path; 23 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v20.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | 3 | DO $$ 4 | BEGIN 5 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 20) THEN 6 | RAISE EXCEPTION 'version-already-applied'; 7 | END IF; 8 | END $$; 9 | 10 | -- Update existing jobs, if any have empty values first 11 | UPDATE "job" SET "invocationdata" = '{}' WHERE "invocationdata" = ''; 12 | UPDATE "job" SET "arguments" = '[]' WHERE "arguments" = ''; 13 | 14 | -- Change the type 15 | 16 | ALTER TABLE "job" ALTER COLUMN "invocationdata" TYPE jsonb USING "invocationdata"::jsonb; 17 | ALTER TABLE "job" ALTER COLUMN "arguments" TYPE jsonb USING "arguments"::jsonb; 18 | ALTER TABLE "server" ALTER COLUMN "data" TYPE jsonb USING "data"::jsonb; 19 | ALTER TABLE "state" ALTER COLUMN "data" TYPE jsonb USING "data"::jsonb; 20 | 21 | RESET search_path; 22 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v21.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | 3 | DO $$ 4 | BEGIN 5 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 21) THEN 6 | RAISE EXCEPTION 'version-already-applied'; 7 | END IF; 8 | END $$; 9 | 10 | -- Set REPLICA IDENTITY to allow replication 11 | ALTER TABLE "lock" REPLICA IDENTITY USING INDEX "lock_resource_key"; 12 | 13 | RESET search_path; 14 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v22.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | 3 | DO $$ 4 | BEGIN 5 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 22) THEN 6 | RAISE EXCEPTION 'version-already-applied'; 7 | END IF; 8 | END $$; 9 | 10 | DROP INDEX IF EXISTS jobqueue_queue_fetchat_jobId; 11 | CREATE INDEX IF NOT EXISTS ix_hangfire_jobqueue_fetchedat_queue_jobid ON jobqueue USING btree (fetchedat nulls first, queue, jobid); 12 | 13 | RESET search_path; 14 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v23.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | 3 | DO $$ 4 | BEGIN 5 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 23) THEN 6 | RAISE EXCEPTION 'version-already-applied'; 7 | END IF; 8 | END $$; 9 | 10 | DROP INDEX IF EXISTS ix_hangfire_job_statename_is_not_null; 11 | 12 | DO $$ 13 | BEGIN 14 | IF current_setting('server_version_num')::int >= 110000 THEN 15 | EXECUTE 'CREATE INDEX ix_hangfire_job_statename_is_not_null ON job USING btree(statename) INCLUDE (id) WHERE statename IS NOT NULL'; 16 | ELSE 17 | CREATE INDEX ix_hangfire_job_statename_is_not_null ON job USING btree(statename) WHERE statename IS NOT NULL; 18 | END IF; 19 | END $$; 20 | 21 | RESET search_path; 22 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v3.sql: -------------------------------------------------------------------------------- 1 | DO 2 | $$ 3 | BEGIN 4 | IF NOT EXISTS( 5 | SELECT schema_name 6 | FROM information_schema.schemata 7 | WHERE schema_name = 'hangfire' 8 | ) 9 | THEN 10 | EXECUTE 'CREATE SCHEMA "hangfire";'; 11 | END IF; 12 | 13 | END 14 | $$; 15 | 16 | SET search_path = 'hangfire'; 17 | -- 18 | -- Table structure for table `Schema` 19 | -- 20 | 21 | CREATE TABLE IF NOT EXISTS "schema" 22 | ( 23 | "version" INT NOT NULL, 24 | PRIMARY KEY ("version") 25 | ); 26 | 27 | 28 | DO 29 | $$ 30 | BEGIN 31 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 3) THEN 32 | RAISE EXCEPTION 'version-already-applied'; 33 | END IF; 34 | END 35 | $$; 36 | 37 | INSERT INTO "schema"("version") 38 | VALUES ('1'); 39 | 40 | -- 41 | -- Table structure for table `Counter` 42 | -- 43 | 44 | CREATE TABLE IF NOT EXISTS "counter" 45 | ( 46 | "id" SERIAL NOT NULL, 47 | "key" VARCHAR(100) NOT NULL, 48 | "value" SMALLINT NOT NULL, 49 | "expireat" TIMESTAMP NULL, 50 | PRIMARY KEY ("id") 51 | ); 52 | 53 | DO 54 | $$ 55 | BEGIN 56 | BEGIN 57 | CREATE INDEX "ix_hangfire_counter_key" ON "counter" ("key"); 58 | EXCEPTION 59 | WHEN duplicate_table THEN RAISE NOTICE 'INDEX ix_hangfire_counter_key already exists.'; 60 | END; 61 | END; 62 | $$; 63 | 64 | -- 65 | -- Table structure for table `Hash` 66 | -- 67 | 68 | CREATE TABLE IF NOT EXISTS "hash" 69 | ( 70 | "id" SERIAL NOT NULL, 71 | "key" VARCHAR(100) NOT NULL, 72 | "field" VARCHAR(100) NOT NULL, 73 | "value" TEXT NULL, 74 | "expireat" TIMESTAMP NULL, 75 | PRIMARY KEY ("id"), 76 | UNIQUE ("key", "field") 77 | ); 78 | 79 | 80 | -- 81 | -- Table structure for table `Job` 82 | -- 83 | 84 | CREATE TABLE IF NOT EXISTS "job" 85 | ( 86 | "id" SERIAL NOT NULL, 87 | "stateid" INT NULL, 88 | "statename" VARCHAR(20) NULL, 89 | "invocationdata" TEXT NOT NULL, 90 | "arguments" TEXT NOT NULL, 91 | "createdat" TIMESTAMP NOT NULL, 92 | "expireat" TIMESTAMP NULL, 93 | PRIMARY KEY ("id") 94 | ); 95 | 96 | DO 97 | $$ 98 | BEGIN 99 | BEGIN 100 | CREATE INDEX "ix_hangfire_job_statename" ON "job" ("statename"); 101 | EXCEPTION 102 | WHEN duplicate_table THEN RAISE NOTICE 'INDEX "ix_hangfire_job_statename" already exists.'; 103 | END; 104 | END; 105 | $$; 106 | 107 | -- 108 | -- Table structure for table `State` 109 | -- 110 | 111 | CREATE TABLE IF NOT EXISTS "state" 112 | ( 113 | "id" SERIAL NOT NULL, 114 | "jobid" INT NOT NULL, 115 | "name" VARCHAR(20) NOT NULL, 116 | "reason" VARCHAR(100) NULL, 117 | "createdat" TIMESTAMP NOT NULL, 118 | "data" TEXT NULL, 119 | PRIMARY KEY ("id"), 120 | FOREIGN KEY ("jobid") REFERENCES "job" ("id") ON UPDATE CASCADE ON DELETE CASCADE 121 | ); 122 | 123 | DO 124 | $$ 125 | BEGIN 126 | BEGIN 127 | CREATE INDEX "ix_hangfire_state_jobid" ON "state" ("jobid"); 128 | EXCEPTION 129 | WHEN duplicate_table THEN RAISE NOTICE 'INDEX "ix_hangfire_state_jobid" already exists.'; 130 | END; 131 | END; 132 | $$; 133 | 134 | 135 | 136 | -- 137 | -- Table structure for table `JobQueue` 138 | -- 139 | 140 | CREATE TABLE IF NOT EXISTS "jobqueue" 141 | ( 142 | "id" SERIAL NOT NULL, 143 | "jobid" INT NOT NULL, 144 | "queue" VARCHAR(20) NOT NULL, 145 | "fetchedat" TIMESTAMP NULL, 146 | PRIMARY KEY ("id") 147 | ); 148 | 149 | DO 150 | $$ 151 | BEGIN 152 | BEGIN 153 | CREATE INDEX "ix_hangfire_jobqueue_queueandfetchedat" ON "jobqueue" ("queue", "fetchedat"); 154 | EXCEPTION 155 | WHEN duplicate_table THEN RAISE NOTICE 'INDEX "ix_hangfire_jobqueue_queueandfetchedat" already exists.'; 156 | END; 157 | END; 158 | $$; 159 | 160 | 161 | -- 162 | -- Table structure for table `List` 163 | -- 164 | 165 | CREATE TABLE IF NOT EXISTS "list" 166 | ( 167 | "id" SERIAL NOT NULL, 168 | "key" VARCHAR(100) NOT NULL, 169 | "value" TEXT NULL, 170 | "expireat" TIMESTAMP NULL, 171 | PRIMARY KEY ("id") 172 | ); 173 | 174 | 175 | -- 176 | -- Table structure for table `Server` 177 | -- 178 | 179 | CREATE TABLE IF NOT EXISTS "server" 180 | ( 181 | "id" VARCHAR(50) NOT NULL, 182 | "data" TEXT NULL, 183 | "lastheartbeat" TIMESTAMP NOT NULL, 184 | PRIMARY KEY ("id") 185 | ); 186 | 187 | 188 | -- 189 | -- Table structure for table `Set` 190 | -- 191 | 192 | CREATE TABLE IF NOT EXISTS "set" 193 | ( 194 | "id" SERIAL NOT NULL, 195 | "key" VARCHAR(100) NOT NULL, 196 | "score" FLOAT8 NOT NULL, 197 | "value" TEXT NOT NULL, 198 | "expireat" TIMESTAMP NULL, 199 | PRIMARY KEY ("id"), 200 | UNIQUE ("key", "value") 201 | ); 202 | 203 | 204 | -- 205 | -- Table structure for table `JobParameter` 206 | -- 207 | 208 | CREATE TABLE IF NOT EXISTS "jobparameter" 209 | ( 210 | "id" SERIAL NOT NULL, 211 | "jobid" INT NOT NULL, 212 | "name" VARCHAR(40) NOT NULL, 213 | "value" TEXT NULL, 214 | PRIMARY KEY ("id"), 215 | FOREIGN KEY ("jobid") REFERENCES "job" ("id") ON UPDATE CASCADE ON DELETE CASCADE 216 | ); 217 | 218 | DO 219 | $$ 220 | BEGIN 221 | BEGIN 222 | CREATE INDEX "ix_hangfire_jobparameter_jobidandname" ON "jobparameter" ("jobid", "name"); 223 | EXCEPTION 224 | WHEN duplicate_table THEN RAISE NOTICE 'INDEX "ix_hangfire_jobparameter_jobidandname" already exists.'; 225 | END; 226 | END; 227 | $$; 228 | 229 | CREATE TABLE IF NOT EXISTS "lock" 230 | ( 231 | "resource" VARCHAR(100) NOT NULL, 232 | UNIQUE ("resource") 233 | ); 234 | 235 | RESET search_path; -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v4.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | -- 3 | -- Table structure for table `Schema` 4 | -- 5 | 6 | DO 7 | $$ 8 | BEGIN 9 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 4) THEN 10 | RAISE EXCEPTION 'version-already-applied'; 11 | END IF; 12 | END 13 | $$; 14 | 15 | ALTER TABLE "counter" 16 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0; 17 | ALTER TABLE "lock" 18 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0; 19 | ALTER TABLE "hash" 20 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0; 21 | ALTER TABLE "job" 22 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0; 23 | ALTER TABLE "jobparameter" 24 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0; 25 | ALTER TABLE "jobqueue" 26 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0; 27 | ALTER TABLE "list" 28 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0; 29 | ALTER TABLE "server" 30 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0; 31 | ALTER TABLE "set" 32 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0; 33 | ALTER TABLE "state" 34 | ADD COLUMN "updatecount" integer NOT NULL DEFAULT 0; 35 | 36 | RESET search_path; -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v5.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | -- 3 | -- Table structure for table `Schema` 4 | -- 5 | 6 | DO 7 | $$ 8 | BEGIN 9 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 5) THEN 10 | RAISE EXCEPTION 'version-already-applied'; 11 | END IF; 12 | END 13 | $$; 14 | 15 | ALTER TABLE "server" 16 | ALTER COLUMN "id" TYPE VARCHAR(100); 17 | 18 | RESET search_path; -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v6.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | -- 3 | -- Adds indices, greatly speeds-up deleting old jobs. 4 | -- 5 | 6 | DO 7 | $$ 8 | BEGIN 9 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 6) THEN 10 | RAISE EXCEPTION 'version-already-applied'; 11 | END IF; 12 | END 13 | $$; 14 | 15 | 16 | DO 17 | $$ 18 | BEGIN 19 | BEGIN 20 | CREATE INDEX "ix_hangfire_counter_expireat" ON "counter" ("expireat"); 21 | EXCEPTION 22 | WHEN duplicate_table THEN RAISE NOTICE 'INDEX ix_hangfire_counter_expireat already exists.'; 23 | END; 24 | END; 25 | $$; 26 | 27 | DO 28 | $$ 29 | BEGIN 30 | BEGIN 31 | CREATE INDEX "ix_hangfire_jobqueue_jobidandqueue" ON "jobqueue" ("jobid", "queue"); 32 | EXCEPTION 33 | WHEN duplicate_table THEN RAISE NOTICE 'INDEX "ix_hangfire_jobqueue_jobidandqueue" already exists.'; 34 | END; 35 | END; 36 | $$; 37 | 38 | RESET search_path; -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v7.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | -- 3 | -- Table structure for table `Schema` 4 | -- 5 | 6 | DO 7 | $$ 8 | BEGIN 9 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 7) THEN 10 | RAISE EXCEPTION 'version-already-applied'; 11 | END IF; 12 | END 13 | $$; 14 | 15 | ALTER TABLE "lock" 16 | ADD COLUMN acquired timestamp without time zone; 17 | 18 | RESET search_path; -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v8.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | -- 3 | -- Table structure for table `Schema` 4 | -- 5 | 6 | DO 7 | $$ 8 | BEGIN 9 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 8) THEN 10 | RAISE EXCEPTION 'version-already-applied'; 11 | END IF; 12 | END 13 | $$; 14 | 15 | ALTER TABLE "counter" 16 | ALTER COLUMN value TYPE bigint; 17 | ALTER TABLE "counter" 18 | DROP COLUMN updatecount RESTRICT; 19 | 20 | RESET search_path; -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Scripts/Install.v9.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | -- 3 | -- Table structure for table `Schema` 4 | -- 5 | 6 | DO 7 | $$ 8 | BEGIN 9 | IF EXISTS(SELECT 1 FROM "schema" WHERE "version"::integer >= 9) THEN 10 | RAISE EXCEPTION 'version-already-applied'; 11 | END IF; 12 | END 13 | $$; 14 | 15 | ALTER TABLE "lock" 16 | ALTER COLUMN "resource" TYPE TEXT; 17 | 18 | RESET search_path; -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Utils/AutoResetEventRegistry.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | 5 | namespace Hangfire.PostgreSql.Utils 6 | { 7 | /// 8 | /// Represents a registry for managing AutoResetEvent instances using event keys. 9 | /// 10 | public class AutoResetEventRegistry 11 | { 12 | private readonly ConcurrentDictionary _events = new(); 13 | 14 | /// 15 | /// Retrieves the wait handles associated with the specified event keys. 16 | /// 17 | /// The event keys. 18 | /// An enumerable of wait handles. 19 | public IEnumerable GetWaitHandles(IEnumerable eventKeys) 20 | { 21 | foreach (string eventKey in eventKeys) 22 | { 23 | AutoResetEvent newHandle = _events.GetOrAdd(eventKey, _ => new AutoResetEvent(false)); 24 | yield return newHandle; 25 | } 26 | } 27 | 28 | /// 29 | /// Sets the specified event. 30 | /// 31 | /// The event key. 32 | public void Set(string eventKey) 33 | { 34 | if (_events.TryGetValue(eventKey, out AutoResetEvent handle)) 35 | { 36 | handle.Set(); 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Utils/DbConnectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using Npgsql; 3 | 4 | namespace Hangfire.PostgreSql.Utils; 5 | 6 | internal static class DbConnectionExtensions 7 | { 8 | private static bool? _supportsNotifications; 9 | 10 | internal static bool SupportsNotifications(this IDbConnection connection) 11 | { 12 | if (_supportsNotifications.HasValue) 13 | { 14 | return _supportsNotifications.Value; 15 | } 16 | 17 | if (connection is not NpgsqlConnection npgsqlConnection) 18 | { 19 | _supportsNotifications = false; 20 | return false; 21 | } 22 | 23 | if (npgsqlConnection.State != ConnectionState.Open) 24 | { 25 | npgsqlConnection.Open(); 26 | } 27 | 28 | _supportsNotifications = npgsqlConnection.PostgreSqlVersion.Major >= 11; 29 | return _supportsNotifications.Value; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Utils/ExceptionTypeHelper.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire. Copyright © 2022 Hangfire OÜ. 2 | // 3 | // Hangfire is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU Lesser General Public License as 5 | // published by the Free Software Foundation, either version 3 6 | // of the License, or any later version. 7 | // 8 | // Hangfire is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public 14 | // License along with Hangfire. If not, see . 15 | 16 | // Borrowed from Hangfire 17 | 18 | using System; 19 | 20 | namespace Hangfire.PostgreSql.Utils 21 | { 22 | internal static class ExceptionTypeHelper 23 | { 24 | #if !NETSTANDARD1_3 25 | private static readonly Type StackOverflowType = typeof(StackOverflowException); 26 | #endif 27 | private static readonly Type OutOfMemoryType = typeof(OutOfMemoryException); 28 | 29 | internal static bool IsCatchableExceptionType(this Exception e) 30 | { 31 | var type = e.GetType(); 32 | return 33 | #if !NETSTANDARD1_3 34 | type != StackOverflowType && 35 | #endif 36 | type != OutOfMemoryType; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Utils/TimestampHelper.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire. Copyright © 2022 Hangfire OÜ. 2 | // 3 | // Hangfire is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU Lesser General Public License as 5 | // published by the Free Software Foundation, either version 3 6 | // of the License, or any later version. 7 | // 8 | // Hangfire is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public 14 | // License along with Hangfire. If not, see . 15 | 16 | // Borrowed from Hangfire 17 | 18 | using System; 19 | 20 | namespace Hangfire.PostgreSql.Utils 21 | { 22 | internal static class TimestampHelper 23 | { 24 | public static long GetTimestamp() 25 | { 26 | #if NETCOREAPP3_0 27 | return Environment.TickCount64; 28 | #else 29 | return Environment.TickCount; 30 | #endif 31 | } 32 | 33 | public static TimeSpan Elapsed(long timestamp) 34 | { 35 | long now = GetTimestamp(); 36 | return Elapsed(now, timestamp); 37 | } 38 | 39 | public static TimeSpan Elapsed(long now, long timestamp) 40 | { 41 | #if NETCOREAPP3_0 42 | return TimeSpan.FromMilliseconds(now - timestamp); 43 | #else 44 | return TimeSpan.FromMilliseconds(unchecked((int)now - (int)timestamp)); 45 | #endif 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Utils/TransactionHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Transactions; 3 | 4 | namespace Hangfire.PostgreSql.Utils 5 | { 6 | public static class TransactionHelpers 7 | { 8 | internal static TransactionScope CreateTransactionScope(IsolationLevel? isolationLevel = IsolationLevel.ReadCommitted, bool enlist = true, TimeSpan? timeout = null) 9 | { 10 | TransactionScopeOption scopeOption = TransactionScopeOption.RequiresNew; 11 | if (enlist) 12 | { 13 | Transaction currentTransaction = Transaction.Current; 14 | if (currentTransaction != null) 15 | { 16 | isolationLevel = currentTransaction.IsolationLevel; 17 | scopeOption = TransactionScopeOption.Required; 18 | } 19 | } 20 | 21 | return new TransactionScope( 22 | scopeOption, 23 | new TransactionOptions { 24 | IsolationLevel = isolationLevel.GetValueOrDefault(IsolationLevel.ReadCommitted), 25 | Timeout = timeout.GetValueOrDefault(TransactionManager.DefaultTimeout), 26 | }); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Hangfire.PostgreSql/Utils/TryExecute.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | // ReSharper disable ArrangeDefaultValueWhenTypeNotEvident 24 | 25 | namespace Hangfire.PostgreSql.Utils 26 | { 27 | public static class Utils 28 | { 29 | public static bool TryExecute( 30 | Func func, 31 | out T result, 32 | Func swallowException = default, 33 | int? tryCount = default) 34 | { 35 | while (tryCount == default || tryCount-- > 0) 36 | { 37 | try 38 | { 39 | result = func(); 40 | return true; 41 | } 42 | catch (Exception ex) 43 | { 44 | if (swallowException != null && !swallowException(ex)) 45 | { 46 | throw; 47 | } 48 | } 49 | } 50 | result = default; 51 | return false; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/AssemblyAttributes.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | // Running tests in parallel actually takes more time when we are cleaning the database for majority of tests. It's quicker to just let them run sequentially. 4 | [assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)] -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/CountersAggregatorFacts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using Dapper; 4 | using Hangfire.PostgreSql.Tests.Utils; 5 | using Npgsql; 6 | using Xunit; 7 | 8 | namespace Hangfire.PostgreSql.Tests; 9 | 10 | public class CountersAggregatorFacts : IClassFixture 11 | { 12 | private static readonly string _schemaName = ConnectionUtils.GetSchemaName(); 13 | 14 | private readonly CancellationToken _token; 15 | private readonly PostgreSqlStorageFixture _fixture; 16 | 17 | public CountersAggregatorFacts(PostgreSqlStorageFixture fixture) 18 | { 19 | CancellationTokenSource cts = new(); 20 | _token = cts.Token; 21 | _fixture = fixture; 22 | _fixture.SetupOptions(o => o.CountersAggregateInterval = TimeSpan.FromMinutes(5)); 23 | } 24 | 25 | [Fact] 26 | [CleanDatabase] 27 | public void Execute_AggregatesCounters() 28 | { 29 | UseConnection((connection, manager) => { 30 | CreateEntry(1); 31 | CreateEntry(5); 32 | CreateEntry(15); 33 | CreateEntry(5, "key2"); 34 | CreateEntry(10, "key2"); 35 | 36 | manager.Execute(_token); 37 | 38 | Assert.Equal(21, GetAggregatedCounters(connection)); 39 | Assert.Equal(15, GetAggregatedCounters(connection, "key2")); 40 | Assert.Null(GetRegularCounters(connection)); 41 | Assert.Null(GetRegularCounters(connection, "key2")); 42 | return; 43 | 44 | void CreateEntry(long value, string key = "key") 45 | { 46 | CreateCounterEntry(connection, value, key); 47 | } 48 | }); 49 | } 50 | 51 | private void UseConnection(Action action) 52 | { 53 | PostgreSqlStorage storage = _fixture.SafeInit(); 54 | CountersAggregator aggregator = new(storage, TimeSpan.Zero); 55 | action(storage.CreateAndOpenConnection(), aggregator); 56 | } 57 | 58 | private static void CreateCounterEntry(NpgsqlConnection connection, long? value, string key = "key") 59 | { 60 | value ??= 1; 61 | string insertSql = 62 | $""" 63 | INSERT INTO "{_schemaName}"."counter"("key", "value", "expireat") 64 | VALUES (@Key, @Value, null) 65 | """; 66 | 67 | connection.Execute(insertSql, new { Key = key, Value = value }); 68 | } 69 | 70 | private static long GetAggregatedCounters(NpgsqlConnection connection, string key = "key") 71 | { 72 | return connection.QuerySingle( 73 | $""" 74 | SELECT "value" 75 | FROM {_schemaName}."aggregatedcounter" 76 | WHERE "key" = @Key 77 | """, new { Key = key }); 78 | } 79 | 80 | private static long? GetRegularCounters(NpgsqlConnection connection, string key = "key") 81 | { 82 | return connection.QuerySingle( 83 | $""" 84 | SELECT SUM("value") 85 | FROM {_schemaName}."counter" 86 | WHERE "key" = @Key 87 | """, new { Key = key }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/Entities/TestJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Hangfire.PostgreSql.Tests.Entities 4 | { 5 | public record TestJob(long Id, string InvocationData, string Arguments, DateTime? ExpireAt, string StateName, long? StateId, DateTime CreatedAt); 6 | 7 | public class TestJobs 8 | { 9 | public void Run(string logMessage) 10 | { 11 | Console.WriteLine("Running test job: {0}", logMessage); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/ExpirationManagerFacts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Threading; 6 | using Dapper; 7 | using Hangfire.PostgreSql.Tests.Utils; 8 | using Npgsql; 9 | using Xunit; 10 | 11 | namespace Hangfire.PostgreSql.Tests 12 | { 13 | public class ExpirationManagerFacts : IClassFixture 14 | { 15 | private readonly PostgreSqlStorageFixture _fixture; 16 | private readonly CancellationToken _token; 17 | 18 | public ExpirationManagerFacts(PostgreSqlStorageFixture fixture) 19 | { 20 | CancellationTokenSource cts = new(); 21 | _token = cts.Token; 22 | _fixture = fixture; 23 | _fixture.SetupOptions(o => o.DeleteExpiredBatchSize = 2); 24 | } 25 | 26 | [Fact] 27 | public void Ctor_ThrowsAnException_WhenStorageIsNull() 28 | { 29 | Assert.Throws(() => new ExpirationManager(null)); 30 | } 31 | 32 | [Fact] 33 | [CleanDatabase] 34 | public void Execute_RemovesOutdatedRecords() 35 | { 36 | UseConnection((connection, manager) => { 37 | long CreateEntry(string key) 38 | { 39 | return CreateExpirationEntry(connection, DateTime.UtcNow.AddMonths(-1), key); 40 | } 41 | 42 | List entryIds = Enumerable.Range(1, 3).Select(i => CreateEntry($"key{i}")).ToList(); 43 | 44 | manager.Execute(_token); 45 | 46 | entryIds.ForEach(entryId => Assert.True(IsEntryExpired(connection, entryId))); 47 | }); 48 | } 49 | 50 | [Fact] 51 | [CleanDatabase] 52 | public void Execute_DoesNotRemoveEntries_WithNoExpirationTimeSet() 53 | { 54 | UseConnection((connection, manager) => { 55 | long entryId = CreateExpirationEntry(connection, null); 56 | 57 | manager.Execute(_token); 58 | 59 | Assert.False(IsEntryExpired(connection, entryId)); 60 | }); 61 | } 62 | 63 | [Fact] 64 | [CleanDatabase] 65 | public void Execute_DoesNotRemoveEntries_WithFreshExpirationTime() 66 | { 67 | UseConnection((connection, manager) => { 68 | long entryId = CreateExpirationEntry(connection, DateTime.Now.AddMonths(1)); 69 | 70 | manager.Execute(_token); 71 | 72 | Assert.False(IsEntryExpired(connection, entryId)); 73 | }); 74 | } 75 | 76 | [Fact] 77 | [CleanDatabase] 78 | public void Execute_Processes_CounterTable() 79 | { 80 | UseConnection((connection, manager) => { 81 | // Arrange 82 | string createSql = $@" 83 | INSERT INTO ""{GetSchemaName()}"".""counter"" (""key"", ""value"", ""expireat"") 84 | VALUES ('key', 1, @ExpireAt) 85 | "; 86 | connection.Execute(createSql, new { ExpireAt = DateTime.UtcNow.AddMonths(-1) }); 87 | 88 | // Act 89 | manager.Execute(_token); 90 | 91 | // Assert 92 | Assert.Equal(0, connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""counter""")); 93 | }); 94 | } 95 | 96 | [Fact] 97 | [CleanDatabase] 98 | public void Execute_Aggregates_CounterTable() 99 | { 100 | UseConnection((connection, manager) => { 101 | // Arrange 102 | string createSql = $@" 103 | INSERT INTO ""{GetSchemaName()}"".""counter"" (""key"", ""value"") 104 | VALUES ('stats:succeeded', 1) 105 | "; 106 | for (int i = 0; i < 5; i++) 107 | { 108 | connection.Execute(createSql); 109 | } 110 | 111 | // Act 112 | manager.Execute(_token); 113 | 114 | // Assert 115 | Assert.Equal(1, connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""counter""")); 116 | Assert.Equal(5, connection.QuerySingle($@"SELECT SUM(""value"") FROM ""{GetSchemaName()}"".""counter""")); 117 | }); 118 | } 119 | 120 | [Fact] 121 | [CleanDatabase] 122 | public void Execute_Processes_JobTable() 123 | { 124 | UseConnection((connection, manager) => { 125 | // Arrange 126 | string createSql = $@" 127 | INSERT INTO ""{GetSchemaName()}"".""job"" (""invocationdata"", ""arguments"", ""createdat"", ""expireat"") 128 | VALUES ('{{}}', '[]', NOW(), @ExpireAt) 129 | "; 130 | connection.Execute(createSql, new { ExpireAt = DateTime.UtcNow.AddMonths(-1) }); 131 | 132 | // Act 133 | manager.Execute(_token); 134 | 135 | // Assert 136 | Assert.Equal(0, connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""job""")); 137 | }); 138 | } 139 | 140 | [Fact] 141 | [CleanDatabase] 142 | public void Execute_Processes_ListTable() 143 | { 144 | UseConnection((connection, manager) => { 145 | // Arrange 146 | string createSql = $@"INSERT INTO ""{GetSchemaName()}"".""list"" (""key"", ""expireat"") VALUES ('key', @ExpireAt)"; 147 | connection.Execute(createSql, new { ExpireAt = DateTime.UtcNow.AddMonths(-1) }); 148 | 149 | // Act 150 | manager.Execute(_token); 151 | 152 | // Assert 153 | Assert.Equal(0, connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""list""")); 154 | }); 155 | } 156 | 157 | [Fact] 158 | [CleanDatabase] 159 | public void Execute_Processes_SetTable() 160 | { 161 | UseConnection((connection, manager) => { 162 | // Arrange 163 | string createSql = $@"INSERT INTO ""{GetSchemaName()}"".""set"" (""key"", ""score"", ""value"", ""expireat"") VALUES ('key', 0, '', @ExpireAt)"; 164 | connection.Execute(createSql, new { ExpireAt = DateTime.UtcNow.AddMonths(-1) }); 165 | 166 | // Act 167 | manager.Execute(_token); 168 | 169 | // Assert 170 | Assert.Equal(0, connection.Query($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""set""").Single()); 171 | }); 172 | } 173 | 174 | [Fact] 175 | [CleanDatabase] 176 | public void Execute_Processes_HashTable() 177 | { 178 | UseConnection((connection, manager) => { 179 | // Arrange 180 | string createSql = $@" 181 | INSERT INTO ""{GetSchemaName()}"".""hash"" (""key"", ""field"", ""value"", ""expireat"") 182 | VALUES ('key', 'field', '', @ExpireAt)"; 183 | connection.Execute(createSql, new { ExpireAt = DateTime.UtcNow.AddMonths(-1) }); 184 | 185 | // Act 186 | manager.Execute(_token); 187 | 188 | // Assert 189 | Assert.Equal(0, connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""hash""")); 190 | }); 191 | } 192 | 193 | private static long CreateExpirationEntry(NpgsqlConnection connection, DateTime? expireAt, string key = "key") 194 | { 195 | string insertSqlNull = $@" 196 | INSERT INTO ""{GetSchemaName()}"".""counter""(""key"", ""value"", ""expireat"") 197 | VALUES (@Key, 1, null) RETURNING ""id"" 198 | "; 199 | 200 | string insertSqlValue = $@" 201 | INSERT INTO ""{GetSchemaName()}"".""counter""(""key"", ""value"", ""expireat"") 202 | VALUES (@Key, 1, NOW() - interval '{{0}} seconds') RETURNING ""id"" 203 | "; 204 | 205 | string insertSql = expireAt == null 206 | ? insertSqlNull 207 | : string.Format(CultureInfo.InvariantCulture, insertSqlValue, 208 | ((long)(DateTime.UtcNow - expireAt.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture)); 209 | 210 | return connection.QuerySingle(insertSql, new { Key = key }); 211 | } 212 | 213 | private static bool IsEntryExpired(NpgsqlConnection connection, long entryId) 214 | { 215 | return connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""counter"" WHERE ""id"" = @Id", new { Id = entryId }) == 0; 216 | } 217 | 218 | private void UseConnection(Action action) 219 | { 220 | PostgreSqlStorage storage = _fixture.SafeInit(); 221 | ExpirationManager manager = new ExpirationManager(storage, TimeSpan.Zero); 222 | action(storage.CreateAndOpenConnection(), manager); 223 | } 224 | 225 | private static string GetSchemaName() 226 | { 227 | return ConnectionUtils.GetSchemaName(); 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/FirstClassQueueFeatureSupportTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using Hangfire.PostgreSql.Tests.Entities; 3 | using Hangfire.PostgreSql.Tests.Utils; 4 | using Hangfire.Storage; 5 | using Hangfire.Storage.Monitoring; 6 | using Xunit; 7 | 8 | namespace Hangfire.PostgreSql.Tests; 9 | 10 | public class FirstClassQueueFeatureSupportTests 11 | { 12 | public FirstClassQueueFeatureSupportTests() 13 | { 14 | JobStorage.Current = new PostgreSqlStorage(ConnectionUtils.GetDefaultConnectionFactory()); 15 | } 16 | 17 | [Fact] 18 | public void HasFlag_ShouldReturnTrue_ForJobQueueProperty() 19 | { 20 | bool supportJobQueueProperty = JobStorage.Current.HasFeature(JobStorageFeatures.JobQueueProperty); 21 | Assert.True(supportJobQueueProperty); 22 | } 23 | 24 | [Fact] 25 | [CleanDatabase] 26 | public void EnqueueJobWithSpecificQueue_ShouldEnqueueCorrectlyAndJobMustBeProcessedInThatQueue() 27 | { 28 | BackgroundJob.Enqueue("critical", job => job.Run("critical")); 29 | BackgroundJob.Enqueue("offline", job => job.Run("offline")); 30 | 31 | BackgroundJobServer unused = new(new BackgroundJobServerOptions() { 32 | Queues = new[] { "critical" }, 33 | }); 34 | 35 | Thread.Sleep(200); 36 | 37 | IMonitoringApi monitoringApi = JobStorage.Current.GetMonitoringApi(); 38 | 39 | JobList jobsInCriticalQueue = monitoringApi.EnqueuedJobs("critical", 0, 10); 40 | JobList jobsInOfflineQueue = monitoringApi.EnqueuedJobs("offline", 0, 10); 41 | 42 | Assert.Empty(jobsInCriticalQueue); //Job from 'critical' queue must be processed by the server 43 | Assert.NotEmpty(jobsInOfflineQueue); //Job from 'offline' queue must be left untouched because no server is processing it 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "This is a test project, underscores in test names are allowed", Scope = "module")] 9 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/Hangfire.PostgreSql.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Hangfire.PostgreSql.Tests 5 | net9.0 6 | Hangfire.PostgreSql.Tests 7 | Hangfire.PostgreSql.Tests 8 | true 9 | default 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/PersistentJobQueueProviderCollectionFacts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Moq; 4 | using Xunit; 5 | 6 | namespace Hangfire.PostgreSql.Tests 7 | { 8 | public class PersistentJobQueueProviderCollectionFacts 9 | { 10 | private static readonly string[] _queues = { "default", "critical" }; 11 | private readonly Mock _defaultProvider; 12 | private readonly Mock _provider; 13 | 14 | public PersistentJobQueueProviderCollectionFacts() 15 | { 16 | _defaultProvider = new Mock(); 17 | _provider = new Mock(); 18 | } 19 | 20 | [Fact] 21 | public void Ctor_ThrowsAnException_WhenDefaultProviderIsNull() 22 | { 23 | Assert.Throws(() => new PersistentJobQueueProviderCollection(null)); 24 | } 25 | 26 | [Fact] 27 | public void Enumeration_IncludesTheDefaultProvider() 28 | { 29 | PersistentJobQueueProviderCollection collection = CreateCollection(); 30 | 31 | IPersistentJobQueueProvider[] result = collection.ToArray(); 32 | 33 | Assert.Single(result); 34 | Assert.Same(_defaultProvider.Object, result[0]); 35 | } 36 | 37 | [Fact] 38 | public void GetProvider_ReturnsTheDefaultProvider_WhenProviderCanNotBeResolvedByQueue() 39 | { 40 | PersistentJobQueueProviderCollection collection = CreateCollection(); 41 | 42 | IPersistentJobQueueProvider provider = collection.GetProvider("queue"); 43 | 44 | Assert.Same(_defaultProvider.Object, provider); 45 | } 46 | 47 | [Fact] 48 | public void Add_ThrowsAnException_WhenProviderIsNull() 49 | { 50 | PersistentJobQueueProviderCollection collection = CreateCollection(); 51 | 52 | ArgumentNullException exception = Assert.Throws(() => collection.Add(null, _queues)); 53 | 54 | Assert.Equal("provider", exception.ParamName); 55 | } 56 | 57 | [Fact] 58 | public void Add_ThrowsAnException_WhenQueuesCollectionIsNull() 59 | { 60 | PersistentJobQueueProviderCollection collection = CreateCollection(); 61 | 62 | ArgumentNullException exception = Assert.Throws(() => collection.Add(_provider.Object, null)); 63 | 64 | Assert.Equal("queues", exception.ParamName); 65 | } 66 | 67 | [Fact] 68 | public void Enumeration_ContainsAddedProvider() 69 | { 70 | PersistentJobQueueProviderCollection collection = CreateCollection(); 71 | 72 | collection.Add(_provider.Object, _queues); 73 | 74 | Assert.Contains(_provider.Object, collection); 75 | } 76 | 77 | [Fact] 78 | public void GetProvider_CanBeResolved_ByAnyQueue() 79 | { 80 | PersistentJobQueueProviderCollection collection = CreateCollection(); 81 | collection.Add(_provider.Object, _queues); 82 | 83 | IPersistentJobQueueProvider provider1 = collection.GetProvider("default"); 84 | IPersistentJobQueueProvider provider2 = collection.GetProvider("critical"); 85 | 86 | Assert.NotSame(_defaultProvider.Object, provider1); 87 | Assert.Same(provider1, provider2); 88 | } 89 | 90 | private PersistentJobQueueProviderCollection CreateCollection() 91 | { 92 | return new PersistentJobQueueProviderCollection(_defaultProvider.Object); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/PostgreSqlFetchedJobFacts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | using System.Threading; 5 | using Dapper; 6 | using Hangfire.PostgreSql.Tests.Utils; 7 | using Xunit; 8 | 9 | namespace Hangfire.PostgreSql.Tests 10 | { 11 | public class PostgreSqlFetchedJobFacts 12 | { 13 | private const string JobId = "id"; 14 | private const string Queue = "queue"; 15 | private DateTime _fetchedAt = DateTime.UtcNow; 16 | 17 | private readonly PostgreSqlStorage _storage; 18 | 19 | public PostgreSqlFetchedJobFacts() 20 | { 21 | _storage = new PostgreSqlStorage(ConnectionUtils.GetDefaultConnectionFactory()); 22 | } 23 | 24 | [Fact] 25 | public void Ctor_ThrowsAnException_WhenStorageIsNull() 26 | { 27 | ArgumentNullException exception = Assert.Throws(() => new PostgreSqlFetchedJob(null, 1, JobId, Queue, _fetchedAt)); 28 | 29 | Assert.Equal("storage", exception.ParamName); 30 | } 31 | 32 | [Fact] 33 | public void Ctor_ThrowsAnException_WhenJobIdIsNull() 34 | { 35 | ArgumentNullException exception = Assert.Throws(() => new PostgreSqlFetchedJob(_storage, 1, null, Queue, _fetchedAt)); 36 | 37 | Assert.Equal("jobId", exception.ParamName); 38 | } 39 | 40 | [Fact] 41 | public void Ctor_ThrowsAnException_WhenQueueIsNull() 42 | { 43 | ArgumentNullException exception = Assert.Throws(() => new PostgreSqlFetchedJob(_storage, 1, JobId, null, _fetchedAt)); 44 | 45 | Assert.Equal("queue", exception.ParamName); 46 | } 47 | 48 | [Fact] 49 | public void Ctor_ThrowsAnException_WhenFetchedAtIsNull() 50 | { 51 | ArgumentNullException exception = Assert.Throws(() => new PostgreSqlFetchedJob(_storage, 1, JobId, Queue, null)); 52 | Assert.Equal("fetchedAt", exception.ParamName); 53 | } 54 | 55 | [Fact] 56 | public void Ctor_CorrectlySets_AllInstanceProperties() 57 | { 58 | PostgreSqlFetchedJob fetchedJob = new(_storage, 1, JobId, Queue, _fetchedAt); 59 | 60 | Assert.Equal(1, fetchedJob.Id); 61 | Assert.Equal(JobId, fetchedJob.JobId); 62 | Assert.Equal(Queue, fetchedJob.Queue); 63 | Assert.Equal(_fetchedAt, fetchedJob.FetchedAt); 64 | } 65 | 66 | [Fact] 67 | [CleanDatabase] 68 | public void RemoveFromQueue_ReallyDeletesTheJobFromTheQueue() 69 | { 70 | // Arrange 71 | long id = CreateJobQueueRecord(_storage, "1", "default", _fetchedAt); 72 | PostgreSqlFetchedJob processingJob = new(_storage, id, "1", "default", _fetchedAt); 73 | 74 | // Act 75 | processingJob.RemoveFromQueue(); 76 | 77 | // Assert 78 | long count = _storage.UseConnection(null, connection => 79 | connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""jobqueue""")); 80 | Assert.Equal(0, count); 81 | } 82 | 83 | [Fact] 84 | [CleanDatabase] 85 | public void RemoveFromQueue_DoesNotDelete_UnrelatedJobs() 86 | { 87 | // Arrange 88 | CreateJobQueueRecord(_storage, "1", "default", _fetchedAt); 89 | CreateJobQueueRecord(_storage, "1", "critical", _fetchedAt); 90 | CreateJobQueueRecord(_storage, "2", "default", _fetchedAt); 91 | 92 | PostgreSqlFetchedJob fetchedJob = new PostgreSqlFetchedJob(_storage, 999, "1", "default", _fetchedAt); 93 | 94 | // Act 95 | fetchedJob.RemoveFromQueue(); 96 | 97 | // Assert 98 | long count = _storage.UseConnection(null, connection => 99 | connection.QuerySingle($@"SELECT COUNT(*) FROM ""{GetSchemaName()}"".""jobqueue""")); 100 | Assert.Equal(3, count); 101 | } 102 | 103 | [Fact] 104 | [CleanDatabase] 105 | public void Requeue_SetsFetchedAtValueToNull() 106 | { 107 | // Arrange 108 | long id = CreateJobQueueRecord(_storage, "1", "default", _fetchedAt); 109 | PostgreSqlFetchedJob processingJob = new(_storage, id, "1", "default", _fetchedAt); 110 | 111 | // Act 112 | processingJob.Requeue(); 113 | 114 | // Assert 115 | dynamic record = _storage.UseConnection(null, connection => 116 | connection.Query($@"SELECT * FROM ""{GetSchemaName()}"".""jobqueue""").Single()); 117 | Assert.Null(record.fetchedat); 118 | } 119 | 120 | [Fact] 121 | [CleanDatabase] 122 | public void Timer_UpdatesFetchedAtColumn() 123 | { 124 | _storage.UseConnection(null, connection => { 125 | // Arrange 126 | var fetchedAt = DateTime.UtcNow.AddMinutes(-5); 127 | long id = CreateJobQueueRecord(_storage, "1", "default", fetchedAt); 128 | using (var processingJob = new PostgreSqlFetchedJob(_storage, id, "1", "default", fetchedAt)) 129 | { 130 | processingJob.DisposeTimer(); 131 | Thread.Sleep(TimeSpan.FromSeconds(10)); 132 | processingJob.ExecuteKeepAliveQueryIfRequired(); 133 | 134 | dynamic record = connection.Query($@"SELECT * FROM ""{GetSchemaName()}"".""jobqueue""").Single(); 135 | 136 | Assert.NotNull(processingJob.FetchedAt); 137 | Assert.Equal(processingJob.FetchedAt, record.fetchedat); 138 | DateTime now = DateTime.UtcNow; 139 | Assert.True(now.AddSeconds(-5) < record.fetchedat, (now - record.fetchedat).ToString()); 140 | } 141 | }); 142 | } 143 | 144 | [Fact] 145 | [CleanDatabase] 146 | public void RemoveFromQueue_AfterTimer_RemovesJobFromTheQueue() 147 | { 148 | _storage.UseConnection(null, connection => { 149 | // Arrange 150 | long id = CreateJobQueueRecord(_storage, "1", "default", _fetchedAt); 151 | using (PostgreSqlFetchedJob processingJob = new PostgreSqlFetchedJob(_storage, id, "1", "default", _fetchedAt)) 152 | { 153 | Thread.Sleep(TimeSpan.FromSeconds(10)); 154 | processingJob.DisposeTimer(); 155 | 156 | // Act 157 | processingJob.RemoveFromQueue(); 158 | 159 | // Assert 160 | int count = connection.Query($@"SELECT count(*) FROM ""{GetSchemaName()}"".""jobqueue""").Single(); 161 | Assert.Equal(0, count); 162 | } 163 | }); 164 | } 165 | 166 | [Fact] 167 | [CleanDatabase] 168 | public void RequeueQueue_AfterTimer_SetsFetchedAtValueToNull() 169 | { 170 | _storage.UseConnection(null, connection => { 171 | // Arrange 172 | long id = CreateJobQueueRecord(_storage, "1", "default", _fetchedAt); 173 | using (var processingJob = new PostgreSqlFetchedJob(_storage, id, "1", "default", _fetchedAt)) 174 | { 175 | Thread.Sleep(TimeSpan.FromSeconds(10)); 176 | processingJob.DisposeTimer(); 177 | 178 | // Act 179 | processingJob.Requeue(); 180 | 181 | // Assert 182 | dynamic record = connection.Query($@"SELECT * FROM ""{GetSchemaName()}"".""jobqueue""").Single(); 183 | Assert.Null(record.fetchedat); 184 | } 185 | }); 186 | } 187 | 188 | [Fact] 189 | [CleanDatabase] 190 | public void Dispose_SetsFetchedAtValueToNull_IfThereWereNoCallsToComplete() 191 | { 192 | // Arrange 193 | long id = CreateJobQueueRecord(_storage, "1", "default", _fetchedAt); 194 | PostgreSqlFetchedJob processingJob = new(_storage, id, "1", "default", _fetchedAt); 195 | 196 | // Act 197 | processingJob.Dispose(); 198 | 199 | // Assert 200 | dynamic record = _storage.UseConnection(null, connection => 201 | connection.Query($@"SELECT * FROM ""{GetSchemaName()}"".""jobqueue""").Single()); 202 | Assert.Null(record.fetchedat); 203 | } 204 | 205 | private static long CreateJobQueueRecord(PostgreSqlStorage storage, string jobId, string queue, DateTime? fetchedAt) 206 | { 207 | string arrangeSql = $@" 208 | INSERT INTO ""{GetSchemaName()}"".""jobqueue"" (""jobid"", ""queue"", ""fetchedat"") 209 | VALUES (@Id, @Queue, @FetchedAt) RETURNING ""id"" 210 | "; 211 | 212 | return 213 | storage.UseConnection(null, connection => 214 | connection.QuerySingle(arrangeSql, 215 | new { Id = Convert.ToInt64(jobId, CultureInfo.InvariantCulture), Queue = queue, FetchedAt = fetchedAt })); 216 | } 217 | 218 | private static string GetSchemaName() 219 | { 220 | return ConnectionUtils.GetSchemaName(); 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/PostgreSqlInstallerFacts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | using Dapper; 5 | using Hangfire.PostgreSql.Tests.Utils; 6 | using Npgsql; 7 | using Xunit; 8 | 9 | namespace Hangfire.PostgreSql.Tests 10 | { 11 | public class PostgreSqlInstallerFacts 12 | { 13 | [Fact] 14 | public void InstallingSchemaUpdatesVersionAndShouldNotThrowAnException() 15 | { 16 | Exception ex = Record.Exception(() => { 17 | UseConnection(connection => { 18 | string schemaName = "hangfire_tests_" + Guid.NewGuid().ToString().Replace("-", "_").ToLower(CultureInfo.InvariantCulture); 19 | 20 | PostgreSqlObjectsInstaller.Install(connection, schemaName); 21 | 22 | int lastVersion = connection.Query($@"SELECT version FROM ""{schemaName}"".""schema""").Single(); 23 | Assert.Equal(23, lastVersion); 24 | 25 | connection.Execute($@"DROP SCHEMA ""{schemaName}"" CASCADE;"); 26 | }); 27 | }); 28 | 29 | Assert.Null(ex); 30 | } 31 | 32 | [Fact] 33 | public void InstallingSchemaWithCapitalsUpdatesVersionAndShouldNotThrowAnException() 34 | { 35 | Exception ex = Record.Exception(() => { 36 | UseConnection(connection => { 37 | string schemaName = "Hangfire_Tests_" + Guid.NewGuid().ToString().Replace("-", "_").ToLower(CultureInfo.InvariantCulture); 38 | 39 | PostgreSqlObjectsInstaller.Install(connection, schemaName); 40 | 41 | int lastVersion = connection.Query($@"SELECT version FROM ""{schemaName}"".""schema""").Single(); 42 | Assert.Equal(23, lastVersion); 43 | 44 | connection.Execute($@"DROP SCHEMA ""{schemaName}"" CASCADE;"); 45 | }); 46 | }); 47 | 48 | Assert.Null(ex); 49 | } 50 | 51 | private static void UseConnection(Action action) 52 | { 53 | using NpgsqlConnection connection = ConnectionUtils.CreateConnection(); 54 | action(connection); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/PostgreSqlMonitoringApiFacts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using Dapper; 6 | using Hangfire.Common; 7 | using Hangfire.PostgreSql.Tests.Utils; 8 | using Hangfire.States; 9 | using Hangfire.Storage; 10 | using Hangfire.Storage.Monitoring; 11 | using Moq; 12 | using Npgsql; 13 | using Xunit; 14 | 15 | namespace Hangfire.PostgreSql.Tests 16 | { 17 | public class PostgreSqlMonitoringApiFacts : IClassFixture 18 | { 19 | private readonly PostgreSqlStorageFixture _fixture; 20 | 21 | public PostgreSqlMonitoringApiFacts(PostgreSqlStorageFixture fixture) 22 | { 23 | _fixture = fixture; 24 | } 25 | 26 | [Fact] 27 | [CleanDatabase] 28 | public void GetJobs_MixedCasing_ReturnsJob() 29 | { 30 | string arrangeSql = $@" 31 | INSERT INTO ""{ConnectionUtils.GetSchemaName()}"".""job""(""invocationdata"", ""arguments"", ""createdat"") 32 | VALUES (@InvocationData, @Arguments, NOW()) RETURNING ""id"""; 33 | 34 | Job job = Job.FromExpression(() => SampleMethod("Hello")); 35 | InvocationData invocationData = InvocationData.SerializeJob(job); 36 | 37 | UseConnection(connection => { 38 | long jobId = connection.QuerySingle(arrangeSql, 39 | new { 40 | InvocationData = new JsonParameter(SerializationHelper.Serialize(invocationData)), 41 | Arguments = new JsonParameter(invocationData.Arguments, JsonParameter.ValueType.Array), 42 | }); 43 | 44 | Mock state = new(); 45 | state.Setup(x => x.Name).Returns(SucceededState.StateName); 46 | state.Setup(x => x.SerializeData()) 47 | .Returns(new Dictionary { 48 | { "SUCCEEDEDAT", "2018-05-03T13:28:18.3939693Z" }, 49 | { "PerformanceDuration", "53" }, 50 | { "latency", "6730" }, 51 | }); 52 | 53 | Commit(connection, x => x.SetJobState(jobId.ToString(CultureInfo.InvariantCulture), state.Object)); 54 | 55 | IMonitoringApi monitoringApi = _fixture.Storage.GetMonitoringApi(); 56 | JobList jobs = monitoringApi.SucceededJobs(0, 10); 57 | 58 | Assert.NotNull(jobs); 59 | }); 60 | } 61 | 62 | [Fact] 63 | [CleanDatabase] 64 | public void HourlySucceededJobs_ReturnsAggregatedStats() 65 | { 66 | DateTime now = DateTime.UtcNow; 67 | string schemaName = ConnectionUtils.GetSchemaName(); 68 | string key = $"stats:succeeded:{now.ToString("yyyy-MM-dd-HH", CultureInfo.InvariantCulture)}"; 69 | string arrangeSql = 70 | $""" 71 | BEGIN; 72 | INSERT INTO "{schemaName}"."counter"("key", "value") 73 | VALUES (@Key, 5); 74 | INSERT INTO "{schemaName}"."aggregatedcounter"("key", "value") 75 | VALUES (@Key, 7); 76 | COMMIT; 77 | """; 78 | UseConnection(connection => { 79 | connection.Execute(arrangeSql, new { Key = key }); 80 | 81 | IMonitoringApi monitoringApi = _fixture.Storage.GetMonitoringApi(); 82 | IDictionary stats = monitoringApi.HourlySucceededJobs(); 83 | Assert.Equal(24, stats.Count); 84 | 85 | long actualCounter = Assert.Single(stats.Where(x => x.Key.Hour == now.Hour).Select(x => x.Value)); 86 | Assert.Equal(12, actualCounter); 87 | }); 88 | } 89 | 90 | private void UseConnection(Action action) 91 | { 92 | PostgreSqlStorage storage = _fixture.SafeInit(); 93 | action(storage.CreateAndOpenConnection()); 94 | } 95 | 96 | private void Commit( 97 | NpgsqlConnection connection, 98 | Action action) 99 | { 100 | PostgreSqlStorage storage = _fixture.SafeInit(); 101 | using PostgreSqlWriteOnlyTransaction transaction = new(storage, () => connection); 102 | action(transaction); 103 | transaction.Commit(); 104 | } 105 | 106 | #pragma warning disable xUnit1013 // Public method should be marked as test 107 | public static void SampleMethod(string arg) 108 | #pragma warning restore xUnit1013 // Public method should be marked as test 109 | { } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/PostgreSqlStorageFacts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Transactions; 5 | using Hangfire.PostgreSql.Factories; 6 | using Hangfire.PostgreSql.Tests.Utils; 7 | using Hangfire.Server; 8 | using Hangfire.Storage; 9 | using Npgsql; 10 | using Xunit; 11 | 12 | namespace Hangfire.PostgreSql.Tests 13 | { 14 | public class PostgreSqlStorageFacts 15 | { 16 | private readonly PostgreSqlStorageOptions _options; 17 | 18 | public PostgreSqlStorageFacts() 19 | { 20 | _options = new PostgreSqlStorageOptions { PrepareSchemaIfNecessary = false, EnableTransactionScopeEnlistment = true }; 21 | } 22 | 23 | [Fact] 24 | [CleanDatabase] 25 | public void Ctor_CanCreateSqlServerStorage_WithExistingConnection() 26 | { 27 | NpgsqlConnection connection = ConnectionUtils.CreateConnection(); 28 | PostgreSqlStorage storage = new(new ExistingNpgsqlConnectionFactory(connection, _options), _options); 29 | 30 | Assert.NotNull(storage); 31 | } 32 | 33 | [Fact] 34 | [CleanDatabase] 35 | public void Ctor_InitializesDefaultJobQueueProvider_AndPassesCorrectOptions() 36 | { 37 | PostgreSqlStorage storage = CreateStorage(); 38 | PersistentJobQueueProviderCollection providers = storage.QueueProviders; 39 | 40 | PostgreSqlJobQueueProvider provider = (PostgreSqlJobQueueProvider)providers.GetProvider("default"); 41 | 42 | Assert.Same(_options, provider.Options); 43 | } 44 | 45 | [Fact] 46 | [CleanDatabase] 47 | public void GetMonitoringApi_ReturnsNonNullInstance() 48 | { 49 | PostgreSqlStorage storage = CreateStorage(); 50 | IMonitoringApi api = storage.GetMonitoringApi(); 51 | Assert.NotNull(api); 52 | } 53 | 54 | [Fact] 55 | [CleanDatabase] 56 | public void GetComponents_ReturnsAllNeededComponents() 57 | { 58 | PostgreSqlStorage storage = CreateStorage(); 59 | 60 | #pragma warning disable CS0618 // Type or member is obsolete 61 | IEnumerable components = storage.GetComponents(); 62 | #pragma warning restore CS0618 // Type or member is obsolete 63 | 64 | Type[] componentTypes = components.Select(x => x.GetType()).ToArray(); 65 | Assert.Contains(typeof(ExpirationManager), componentTypes); 66 | } 67 | 68 | [Fact] 69 | public void Ctor_ThrowsAnException_WhenConnectionFactoryIsNull() 70 | { 71 | ArgumentNullException exception = Assert.Throws(() => new PostgreSqlStorage(connectionFactory: null, new PostgreSqlStorageOptions())); 72 | Assert.Equal("connectionFactory", exception.ParamName); 73 | } 74 | 75 | [Fact] 76 | [CleanDatabase] 77 | public void Ctor_CanCreateSqlServerStorage_WithExistingConnectionFactory() 78 | { 79 | PostgreSqlStorage storage = new(new DefaultConnectionFactory(), _options); 80 | Assert.NotNull(storage); 81 | } 82 | 83 | [Fact] 84 | [CleanDatabase] 85 | public void CanCreateAndOpenConnection_WithExistingConnectionFactory() 86 | { 87 | PostgreSqlStorage storage = new(new DefaultConnectionFactory(), _options); 88 | NpgsqlConnection connection = storage.CreateAndOpenConnection(); 89 | Assert.NotNull(connection); 90 | } 91 | 92 | [Fact] 93 | public void CreateAndOpenConnection_ThrowsAnException_WithExistingConnectionFactoryAndInvalidOptions() 94 | { 95 | PostgreSqlStorageOptions option = new() { 96 | EnableTransactionScopeEnlistment = false, 97 | PrepareSchemaIfNecessary = false, 98 | }; 99 | Assert.Throws(() => new PostgreSqlStorage(ConnectionUtils.GetDefaultConnectionFactory(option), option)); 100 | } 101 | 102 | [Fact] 103 | public void CanUseTransaction_WithDifferentTransactionIsolationLevel() 104 | { 105 | using TransactionScope scope = new(TransactionScopeOption.Required, 106 | new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable }); 107 | 108 | PostgreSqlStorage storage = new(new DefaultConnectionFactory(), _options); 109 | NpgsqlConnection connection = storage.CreateAndOpenConnection(); 110 | 111 | bool success = storage.UseTransaction(connection, (_, _) => true); 112 | 113 | Assert.True(success); 114 | } 115 | 116 | [Fact] 117 | public void HasFeature_ThrowsAnException_WhenFeatureIsNull() 118 | { 119 | ArgumentNullException aex = Assert.Throws(() => new PostgreSqlStorage(new DefaultConnectionFactory(), _options).HasFeature(null)); 120 | Assert.Equal("featureId", aex.ParamName); 121 | } 122 | 123 | [Theory] 124 | [InlineData("Job.Queue", true)] // JobStorageFeatures.JobQueueProperty 125 | [InlineData("Connection.BatchedGetFirstByLowestScoreFromSet", true)] // JobStorageFeatures.Connection.BatchedGetFirstByLowest 126 | [InlineData("", false)] 127 | [InlineData("Unsupported", false)] 128 | public void HasFeature_ReturnsCorrectValues(string featureName, bool expected) 129 | { 130 | PostgreSqlStorage storage = new(new DefaultConnectionFactory(), _options); 131 | bool actual = storage.HasFeature(featureName); 132 | Assert.Equal(expected, actual); 133 | } 134 | 135 | private PostgreSqlStorage CreateStorage() 136 | { 137 | return new PostgreSqlStorage(ConnectionUtils.GetDefaultConnectionFactory(), _options); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/PostgreSqlStorageOptionsFacts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace Hangfire.PostgreSql.Tests 5 | { 6 | public class PostgreSqlStorageOptionsFacts 7 | { 8 | [Fact] 9 | public void Ctor_SetsTheDefaultOptions() 10 | { 11 | PostgreSqlStorageOptions options = new(); 12 | 13 | Assert.True(options.QueuePollInterval > TimeSpan.Zero); 14 | Assert.True(options.InvisibilityTimeout > TimeSpan.Zero); 15 | Assert.True(options.DistributedLockTimeout > TimeSpan.Zero); 16 | Assert.True(options.PrepareSchemaIfNecessary); 17 | } 18 | 19 | [Fact] 20 | public void Set_QueuePollInterval_ShouldThrowAnException_WhenGivenIntervalIsTooLow() 21 | { 22 | PostgreSqlStorageOptions options = new(); 23 | Assert.Throws(() => options.QueuePollInterval = TimeSpan.FromMilliseconds(10)); 24 | } 25 | 26 | [Fact] 27 | public void Set_QueuePollInterval_SetsTheValue_WhenGivenIntervalIsTooLow_ButIgnored() 28 | { 29 | PostgreSqlStorageOptions options = new() { 30 | AllowUnsafeValues = true, 31 | QueuePollInterval = TimeSpan.FromMilliseconds(10), 32 | }; 33 | Assert.Equal(TimeSpan.FromMilliseconds(10), options.QueuePollInterval); 34 | } 35 | 36 | [Fact] 37 | public void Set_QueuePollInterval_ShouldThrowAnException_WhenGivenIntervalIsEqualToZero_EvenIfIgnored() 38 | { 39 | PostgreSqlStorageOptions options = new() { AllowUnsafeValues = true }; 40 | Assert.Throws(() => options.QueuePollInterval = TimeSpan.Zero); 41 | } 42 | 43 | [Fact] 44 | public void Set_QueuePollInterval_SetsTheValue() 45 | { 46 | PostgreSqlStorageOptions options = new(); 47 | options.QueuePollInterval = TimeSpan.FromSeconds(1); 48 | Assert.Equal(TimeSpan.FromSeconds(1), options.QueuePollInterval); 49 | } 50 | 51 | [Fact] 52 | public void Set_InvisibilityTimeout_ShouldThrowAnException_WhenGivenIntervalIsEqualToZero() 53 | { 54 | PostgreSqlStorageOptions options = new(); 55 | Assert.Throws(() => options.InvisibilityTimeout = TimeSpan.Zero); 56 | } 57 | 58 | [Fact] 59 | public void Set_InvisibilityTimeout_ShouldThrowAnException_WhenGivenIntervalIsNegative() 60 | { 61 | PostgreSqlStorageOptions options = new(); 62 | Assert.Throws(() => options.InvisibilityTimeout = TimeSpan.FromSeconds(-1)); 63 | } 64 | 65 | [Fact] 66 | public void Set_InvisibilityTimeout_SetsTheValue() 67 | { 68 | PostgreSqlStorageOptions options = new(); 69 | options.InvisibilityTimeout = TimeSpan.FromSeconds(1); 70 | Assert.Equal(TimeSpan.FromSeconds(1), options.InvisibilityTimeout); 71 | } 72 | 73 | [Fact] 74 | public void Set_DistributedLockTimeout_ShouldThrowAnException_WhenGivenIntervalIsEqualToZero() 75 | { 76 | PostgreSqlStorageOptions options = new(); 77 | Assert.Throws(() => options.DistributedLockTimeout = TimeSpan.Zero); 78 | } 79 | 80 | [Fact] 81 | public void Set_DistributedLockTimeout_ShouldThrowAnException_WhenGivenIntervalIsNegative() 82 | { 83 | PostgreSqlStorageOptions options = new(); 84 | Assert.Throws(() => options.DistributedLockTimeout = TimeSpan.FromSeconds(-1)); 85 | } 86 | 87 | [Fact] 88 | public void Set_DistributedLockTimeout_SetsTheValue() 89 | { 90 | PostgreSqlStorageOptions options = new(); 91 | options.DistributedLockTimeout = TimeSpan.FromSeconds(1); 92 | Assert.Equal(TimeSpan.FromSeconds(1), options.DistributedLockTimeout); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/Scripts/Clean.sql: -------------------------------------------------------------------------------- 1 | SET search_path = 'hangfire'; 2 | 3 | DELETE FROM hangfire."aggregatedcounter"; 4 | DELETE FROM hangfire."counter"; 5 | DELETE FROM hangfire."hash"; 6 | DELETE FROM hangfire."job"; 7 | DELETE FROM hangfire."jobparameter"; 8 | DELETE FROM hangfire."jobqueue"; 9 | DELETE FROM hangfire."list"; 10 | DELETE FROM hangfire."lock"; 11 | DELETE FROM hangfire."server"; 12 | DELETE FROM hangfire."set"; 13 | DELETE FROM hangfire."state"; -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/Utils/CleanDatabaseAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using System.Reflection; 3 | using System.Threading; 4 | using Dapper; 5 | using Npgsql; 6 | using Xunit.Sdk; 7 | 8 | namespace Hangfire.PostgreSql.Tests.Utils 9 | { 10 | public class CleanDatabaseAttribute : BeforeAfterTestAttribute 11 | { 12 | private static readonly object _globalLock = new(); 13 | private static bool _sqlObjectInstalled; 14 | 15 | public override void Before(MethodInfo methodUnderTest) 16 | { 17 | Monitor.Enter(_globalLock); 18 | 19 | if (!_sqlObjectInstalled) 20 | { 21 | RecreateSchemaAndInstallObjects(); 22 | _sqlObjectInstalled = true; 23 | } 24 | 25 | CleanTables(); 26 | } 27 | 28 | public override void After(MethodInfo methodUnderTest) 29 | { 30 | try { } 31 | finally 32 | { 33 | Monitor.Exit(_globalLock); 34 | } 35 | } 36 | 37 | private static void RecreateSchemaAndInstallObjects() 38 | { 39 | using NpgsqlConnection masterConnection = ConnectionUtils.CreateMasterConnection(); 40 | bool databaseExists = masterConnection.QuerySingleOrDefault($@"SELECT true :: boolean FROM pg_database WHERE datname = @DatabaseName;", 41 | new { 42 | DatabaseName = ConnectionUtils.GetDatabaseName(), 43 | }) ?? false; 44 | 45 | if (!databaseExists) 46 | { 47 | masterConnection.Execute($@"CREATE DATABASE ""{ConnectionUtils.GetDatabaseName()}"""); 48 | } 49 | 50 | using NpgsqlConnection connection = ConnectionUtils.CreateConnection(); 51 | if (connection.State == ConnectionState.Closed) 52 | { 53 | connection.Open(); 54 | } 55 | 56 | PostgreSqlObjectsInstaller.Install(connection); 57 | PostgreSqlTestObjectsInitializer.CleanTables(connection); 58 | } 59 | 60 | private static void CleanTables() 61 | { 62 | using NpgsqlConnection connection = ConnectionUtils.CreateConnection(); 63 | PostgreSqlTestObjectsInitializer.CleanTables(connection); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/Utils/ConnectionUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Hangfire.Annotations; 4 | using Hangfire.PostgreSql.Factories; 5 | using Npgsql; 6 | 7 | namespace Hangfire.PostgreSql.Tests.Utils 8 | { 9 | public static class ConnectionUtils 10 | { 11 | private const string DatabaseVariable = "Hangfire_PostgreSql_DatabaseName"; 12 | private const string SchemaVariable = "Hangfire_PostgreSql_SchemaName"; 13 | 14 | private const string ConnectionStringTemplateVariable = "Hangfire_PostgreSql_ConnectionStringTemplate"; 15 | 16 | private const string MasterDatabaseName = "postgres"; 17 | private const string DefaultDatabaseName = @"hangfire_tests"; 18 | private const string DefaultSchemaName = @"hangfire"; 19 | 20 | private const string DefaultConnectionStringTemplate = @"Server=127.0.0.1;Port=5432;Database=postgres;User Id=postgres;Password=password;"; 21 | 22 | public static string GetDatabaseName() 23 | { 24 | return Environment.GetEnvironmentVariable(DatabaseVariable) ?? DefaultDatabaseName; 25 | } 26 | 27 | public static string GetSchemaName() 28 | { 29 | return Environment.GetEnvironmentVariable(SchemaVariable) ?? DefaultSchemaName; 30 | } 31 | 32 | public static string GetMasterConnectionString() 33 | { 34 | return string.Format(CultureInfo.InvariantCulture, GetConnectionStringTemplate(), MasterDatabaseName); 35 | } 36 | 37 | public static string GetConnectionString() 38 | { 39 | return string.Format(CultureInfo.InvariantCulture, GetConnectionStringTemplate(), GetDatabaseName()); 40 | } 41 | 42 | public static NpgsqlConnectionFactory GetDefaultConnectionFactory([CanBeNull] PostgreSqlStorageOptions options = null) 43 | { 44 | return new NpgsqlConnectionFactory(GetConnectionString(), options ?? new PostgreSqlStorageOptions()); 45 | } 46 | 47 | private static string GetConnectionStringTemplate() 48 | { 49 | return Environment.GetEnvironmentVariable(ConnectionStringTemplateVariable) 50 | ?? DefaultConnectionStringTemplate; 51 | } 52 | 53 | public static NpgsqlConnection CreateConnection() 54 | { 55 | NpgsqlConnectionStringBuilder csb = new(GetConnectionString()); 56 | 57 | NpgsqlConnection connection = new() { 58 | ConnectionString = csb.ToString(), 59 | }; 60 | connection.Open(); 61 | 62 | return connection; 63 | } 64 | 65 | public static NpgsqlConnection CreateMasterConnection() 66 | { 67 | NpgsqlConnectionStringBuilder csb = new(GetMasterConnectionString()); 68 | 69 | NpgsqlConnection connection = new() { 70 | ConnectionString = csb.ToString(), 71 | }; 72 | connection.Open(); 73 | 74 | return connection; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/Utils/DefaultConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using Npgsql; 2 | 3 | namespace Hangfire.PostgreSql.Tests.Utils 4 | { 5 | public class DefaultConnectionFactory : IConnectionFactory 6 | { 7 | /// 8 | /// Get or create NpgsqlConnection 9 | /// 10 | public NpgsqlConnection GetOrCreateConnection() 11 | { 12 | return ConnectionUtils.CreateConnection(); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/Utils/Helper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Globalization; 4 | using Dapper; 5 | using Hangfire.PostgreSql.Tests.Entities; 6 | 7 | namespace Hangfire.PostgreSql.Tests.Utils 8 | { 9 | public static class Helper 10 | { 11 | public static TestJob GetTestJob(IDbConnection connection, string schemaName, string jobId) 12 | { 13 | return connection 14 | .QuerySingle($@"SELECT ""id"" ""Id"", ""invocationdata"" ""InvocationData"", ""arguments"" ""Arguments"", ""expireat"" ""ExpireAt"", ""statename"" ""StateName"", ""stateid"" ""StateId"", ""createdat"" ""CreatedAt"" FROM ""{schemaName}"".""job"" WHERE ""id"" = @Id OR @Id = -1", 15 | new { Id = Convert.ToInt64(jobId, CultureInfo.InvariantCulture) }); 16 | } 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/Utils/PostgreSqlStorageExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Hangfire.PostgreSql.Tests.Utils 2 | { 3 | internal static class PostgreSqlStorageExtensions 4 | { 5 | public static PostgreSqlConnection GetStorageConnection(this PostgreSqlStorage storage) 6 | { 7 | return storage.GetConnection() as PostgreSqlConnection; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/Utils/PostgreSqlStorageFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Hangfire.PostgreSql.Factories; 3 | using Moq; 4 | using Npgsql; 5 | 6 | namespace Hangfire.PostgreSql.Tests.Utils 7 | { 8 | public class PostgreSqlStorageFixture : IDisposable 9 | { 10 | private readonly PostgreSqlStorageOptions _storageOptions; 11 | private bool _initialized; 12 | private NpgsqlConnection _mainConnection; 13 | 14 | public PostgreSqlStorageFixture() 15 | { 16 | PersistentJobQueueMock = new Mock(); 17 | 18 | Mock provider = new(); 19 | provider.Setup(x => x.GetJobQueue()) 20 | .Returns(PersistentJobQueueMock.Object); 21 | 22 | PersistentJobQueueProviderCollection = new PersistentJobQueueProviderCollection(provider.Object); 23 | 24 | _storageOptions = new PostgreSqlStorageOptions { 25 | SchemaName = ConnectionUtils.GetSchemaName(), 26 | EnableTransactionScopeEnlistment = true, 27 | }; 28 | } 29 | 30 | public Mock PersistentJobQueueMock { get; } 31 | 32 | public PersistentJobQueueProviderCollection PersistentJobQueueProviderCollection { get; } 33 | 34 | public PostgreSqlStorage Storage { get; private set; } 35 | public NpgsqlConnection MainConnection => _mainConnection ?? (_mainConnection = ConnectionUtils.CreateConnection()); 36 | 37 | public void Dispose() 38 | { 39 | _mainConnection?.Dispose(); 40 | _mainConnection = null; 41 | } 42 | 43 | public void SetupOptions(Action storageOptionsConfigure) 44 | { 45 | storageOptionsConfigure(_storageOptions); 46 | } 47 | 48 | public PostgreSqlStorage SafeInit(NpgsqlConnection connection = null) 49 | { 50 | return _initialized 51 | ? Storage 52 | : ForceInit(connection); 53 | } 54 | 55 | public PostgreSqlStorage ForceInit(NpgsqlConnection connection = null) 56 | { 57 | Storage = new PostgreSqlStorage(new ExistingNpgsqlConnectionFactory(connection ?? MainConnection, _storageOptions), _storageOptions) { 58 | QueueProviders = PersistentJobQueueProviderCollection, 59 | }; 60 | _initialized = true; 61 | return Storage; 62 | } 63 | 64 | public void SafeInit(PostgreSqlStorageOptions options, 65 | PersistentJobQueueProviderCollection jobQueueProviderCollection = null, 66 | NpgsqlConnection connection = null) 67 | { 68 | if (!_initialized) 69 | { 70 | ForceInit(options, jobQueueProviderCollection, connection); 71 | return; 72 | } 73 | 74 | Storage.QueueProviders = jobQueueProviderCollection; 75 | } 76 | 77 | public void ForceInit(PostgreSqlStorageOptions options, 78 | PersistentJobQueueProviderCollection jobQueueProviderCollection = null, 79 | NpgsqlConnection connection = null) 80 | { 81 | Storage = new PostgreSqlStorage(new ExistingNpgsqlConnectionFactory(connection ?? MainConnection, options), options) { 82 | QueueProviders = jobQueueProviderCollection, 83 | }; 84 | _initialized = true; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Hangfire.PostgreSql.Tests/Utils/PostgreSqlTestObjectsInitializer.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Hangfire.PostgreSql. 2 | // Copyright © 2014 Frank Hommers . 3 | // 4 | // Hangfire.PostgreSql is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as 6 | // published by the Free Software Foundation, either version 3 7 | // of the License, or any later version. 8 | // 9 | // Hangfire.PostgreSql is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public 15 | // License along with Hangfire.PostgreSql. If not, see . 16 | // 17 | // This work is based on the work of Sergey Odinokov, author of 18 | // Hangfire. 19 | // 20 | // Special thanks goes to him. 21 | 22 | using System; 23 | using System.Data; 24 | using System.IO; 25 | using System.Reflection; 26 | using Npgsql; 27 | 28 | namespace Hangfire.PostgreSql.Tests.Utils 29 | { 30 | internal static class PostgreSqlTestObjectsInitializer 31 | { 32 | public static void CleanTables(NpgsqlConnection connection) 33 | { 34 | if (connection == null) throw new ArgumentNullException(nameof(connection)); 35 | 36 | string script = GetStringResource(typeof(PostgreSqlTestObjectsInitializer).GetTypeInfo().Assembly, 37 | "Hangfire.PostgreSql.Tests.Scripts.Clean.sql").Replace("'hangfire'", $"'{ConnectionUtils.GetSchemaName()}'"); 38 | 39 | using NpgsqlTransaction transaction = connection.BeginTransaction(IsolationLevel.Serializable); 40 | using NpgsqlCommand command = new(script, connection, transaction); 41 | command.CommandTimeout = 120; 42 | command.ExecuteNonQuery(); 43 | transaction.Commit(); 44 | } 45 | 46 | private static string GetStringResource(Assembly assembly, string resourceName) 47 | { 48 | using Stream stream = assembly.GetManifestResourceStream(resourceName); 49 | if (stream == null) 50 | { 51 | throw new InvalidOperationException($"Requested resource '{resourceName}' was not found in the assembly '{assembly}'."); 52 | } 53 | 54 | using StreamReader reader = new(stream); 55 | return reader.ReadToEnd(); 56 | } 57 | } 58 | } 59 | --------------------------------------------------------------------------------