milena 0.5.3.0 → 0.5.4.0
raw patch · 5 files changed
+176/−20 lines, 5 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Network.Kafka: commitOffset :: Kafka m => OffsetCommitRequest -> m OffsetCommitResponse
+ Network.Kafka: commitOffset' :: Kafka m => Handle -> OffsetCommitRequest -> m OffsetCommitResponse
+ Network.Kafka: commitOffsetRequest :: ConsumerGroup -> TopicName -> Partition -> Offset -> OffsetCommitRequest
+ Network.Kafka: deleteTopic :: Kafka m => DeleteTopicsRequest -> m DeleteTopicsResponse
+ Network.Kafka: deleteTopic' :: Kafka m => Handle -> DeleteTopicsRequest -> m DeleteTopicsResponse
+ Network.Kafka: deleteTopicsRequest :: TopicName -> DeleteTopicsRequest
+ Network.Kafka: fetchOffset :: Kafka m => OffsetFetchRequest -> m OffsetFetchResponse
+ Network.Kafka: fetchOffset' :: Kafka m => Handle -> OffsetFetchRequest -> m OffsetFetchResponse
+ Network.Kafka: fetchOffsetRequest :: ConsumerGroup -> TopicName -> Partition -> OffsetFetchRequest
+ Network.Kafka.Protocol: DeleteTopicsReq :: ([TopicName], Timeout) -> DeleteTopicsRequest
+ Network.Kafka.Protocol: DeleteTopicsRequest :: DeleteTopicsRequest -> RequestMessage
+ Network.Kafka.Protocol: DeleteTopicsResp :: [(TopicName, KafkaError)] -> DeleteTopicsResponse
+ Network.Kafka.Protocol: [DeleteTopicsRR] :: MonadIO m => DeleteTopicsRequest -> ReqResp (m DeleteTopicsResponse)
+ Network.Kafka.Protocol: [OffsetCommitRR] :: MonadIO m => OffsetCommitRequest -> ReqResp (m OffsetCommitResponse)
+ Network.Kafka.Protocol: [OffsetFetchRR] :: MonadIO m => OffsetFetchRequest -> ReqResp (m OffsetFetchResponse)
+ Network.Kafka.Protocol: [_deleteTopicsResponseFields] :: DeleteTopicsResponse -> [(TopicName, KafkaError)]
+ Network.Kafka.Protocol: instance GHC.Classes.Eq Network.Kafka.Protocol.DeleteTopicsRequest
+ Network.Kafka.Protocol: instance GHC.Classes.Eq Network.Kafka.Protocol.DeleteTopicsResponse
+ Network.Kafka.Protocol: instance GHC.Generics.Generic Network.Kafka.Protocol.DeleteTopicsRequest
+ Network.Kafka.Protocol: instance GHC.Generics.Generic Network.Kafka.Protocol.DeleteTopicsResponse
+ Network.Kafka.Protocol: instance GHC.Show.Show Network.Kafka.Protocol.DeleteTopicsRequest
+ Network.Kafka.Protocol: instance GHC.Show.Show Network.Kafka.Protocol.DeleteTopicsResponse
+ Network.Kafka.Protocol: instance Network.Kafka.Protocol.Deserializable Network.Kafka.Protocol.DeleteTopicsResponse
+ Network.Kafka.Protocol: instance Network.Kafka.Protocol.Serializable Network.Kafka.Protocol.DeleteTopicsRequest
+ Network.Kafka.Protocol: instance Network.Kafka.Protocol.Serializable Network.Kafka.Protocol.DeleteTopicsResponse
+ Network.Kafka.Protocol: newtype DeleteTopicsRequest
+ Network.Kafka.Protocol: newtype DeleteTopicsResponse
Files
- CHANGELOG.md +10/−0
- Network/Kafka.hs +38/−0
- Network/Kafka/Protocol.hs +26/−7
- milena.cabal +3/−3
- test/tests.hs +99/−10
CHANGELOG.md view
@@ -1,3 +1,13 @@+0.5.4.0++* commitOffset and fetchOffset to the API (https://github.com/adamflott/milena/pull/7)+* Add Delete Topic support+* Make tests pass on multiple invocations++0.5.3.0++* Add Create Topic support (https://github.com/adamflott/milena/pull/6). Thanks matth!+ 0.5.2.3 * Bump network upper bounds
Network/Kafka.hs view
@@ -202,6 +202,44 @@ CreateTopicsReq ([(topic, partition, replication_factor, replica_assignment, config)], defaultRequestTimeout) +deleteTopic :: Kafka m => DeleteTopicsRequest -> m DeleteTopicsResponse+deleteTopic request = withAnyHandle $ flip deleteTopic' request++deleteTopic' :: Kafka m => Handle -> DeleteTopicsRequest -> m DeleteTopicsResponse+deleteTopic' h request = makeRequest h $ DeleteTopicsRR request++deleteTopicsRequest :: TopicName -> DeleteTopicsRequest+deleteTopicsRequest topic = DeleteTopicsReq ([topic], defaultRequestTimeout)+++fetchOffset :: Kafka m => OffsetFetchRequest -> m OffsetFetchResponse+fetchOffset request = withAnyHandle $ flip fetchOffset' request++fetchOffset' :: Kafka m => Handle -> OffsetFetchRequest -> m OffsetFetchResponse+fetchOffset' h request = makeRequest h $ OffsetFetchRR request++fetchOffsetRequest ::+ ConsumerGroup -> TopicName -> Partition -> OffsetFetchRequest+fetchOffsetRequest consumerGroup topic partition =+ OffsetFetchReq+ (consumerGroup, [(topic, [partition])])+++commitOffset :: Kafka m => OffsetCommitRequest -> m OffsetCommitResponse+commitOffset request = withAnyHandle $ flip commitOffset' request++commitOffset' ::+ Kafka m => Handle -> OffsetCommitRequest -> m OffsetCommitResponse+commitOffset' h request = makeRequest h $ OffsetCommitRR request++commitOffsetRequest ::+ ConsumerGroup -> TopicName -> Partition -> Offset -> OffsetCommitRequest+commitOffsetRequest consumerGroup topic partition offset =+ let time = -1+ metadata_ = Metadata "milena"+ in OffsetCommitReq+ (consumerGroup, [(topic, [(partition, offset, time, metadata_)])])+ getTopicPartitionLeader :: Kafka m => TopicName -> Partition -> m Broker getTopicPartitionLeader t p = do
Network/Kafka/Protocol.hs view
@@ -2,24 +2,28 @@ ( module Network.Kafka.Protocol ) where +import Prelude hiding ((.), id)++-- base import Control.Applicative import Control.Category (Category(..)) import Control.Exception (Exception)-import Control.Lens import Control.Monad (replicateM, liftM2, liftM3, liftM4, liftM5, unless)-import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Bits ((.&.))+import Data.Int+import GHC.Exts (IsString(..))+import GHC.Generics (Generic)+import System.IO++-- Hackage+import Control.Lens+import Control.Monad.IO.Class (MonadIO, liftIO) import Data.ByteString.Char8 (ByteString) import Data.ByteString.Lens (unpackedChars) import Data.Digest.CRC32-import Data.Int import Data.Serialize.Get import Data.Serialize.Put-import GHC.Exts (IsString(..))-import GHC.Generics (Generic)-import System.IO import Numeric.Lens-import Prelude hiding ((.), id) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB (fromStrict, toStrict) import qualified Codec.Compression.GZip as GZip (compress, decompress)@@ -31,6 +35,9 @@ FetchRR :: MonadIO m => FetchRequest -> ReqResp (m FetchResponse) OffsetRR :: MonadIO m => OffsetRequest -> ReqResp (m OffsetResponse) TopicsRR :: MonadIO m => CreateTopicsRequest -> ReqResp (m CreateTopicsResponse)+ DeleteTopicsRR :: MonadIO m => DeleteTopicsRequest -> ReqResp (m DeleteTopicsResponse)+ OffsetCommitRR :: MonadIO m => OffsetCommitRequest -> ReqResp (m OffsetCommitResponse)+ OffsetFetchRR :: MonadIO m => OffsetFetchRequest -> ReqResp (m OffsetFetchResponse) doRequest' :: (Deserializable a, MonadIO m) => CorrelationId -> Handle -> Request -> m (Either String a) doRequest' correlationId h r = do@@ -53,6 +60,9 @@ doRequest clientId correlationId h (FetchRR req) = doRequest' correlationId h $ Request (correlationId, clientId, FetchRequest req) doRequest clientId correlationId h (OffsetRR req) = doRequest' correlationId h $ Request (correlationId, clientId, OffsetRequest req) doRequest clientId correlationId h (TopicsRR req) = doRequest' correlationId h $ Request (correlationId, clientId, CreateTopicsRequest req)+doRequest clientId correlationId h (DeleteTopicsRR req) = doRequest' correlationId h $ Request (correlationId, clientId, DeleteTopicsRequest req)+doRequest clientId correlationId h (OffsetCommitRR req) = doRequest' correlationId h $ Request (correlationId, clientId, OffsetCommitRequest req)+doRequest clientId correlationId h (OffsetFetchRR req) = doRequest' correlationId h $ Request (correlationId, clientId, OffsetFetchRequest req) class Serializable a where serialize :: a -> Put@@ -75,6 +85,7 @@ | OffsetFetchRequest OffsetFetchRequest | GroupCoordinatorRequest GroupCoordinatorRequest | CreateTopicsRequest CreateTopicsRequest+ | DeleteTopicsRequest DeleteTopicsRequest deriving (Show, Generic, Eq) newtype MetadataRequest = MetadataReq [TopicName] deriving (Show, Eq, Serializable, Generic, Deserializable)@@ -106,6 +117,10 @@ TopicsResp { _topicsResponseFields :: [(TopicName, KafkaError)] } deriving (Show, Eq, Deserializable, Serializable, Generic) +newtype DeleteTopicsResponse =+ DeleteTopicsResp { _deleteTopicsResponseFields :: [(TopicName, KafkaError)] }+ deriving (Show, Eq, Deserializable, Serializable, Generic)+ newtype MetadataResponse = MetadataResp { _metadataResponseFields :: ([Broker], [TopicMetadata]) } deriving (Show, Eq, Deserializable, Generic) newtype Broker = Broker { _brokerFields :: (NodeId, Host, Port) } deriving (Show, Eq, Ord, Deserializable, Generic) newtype NodeId = NodeId { _nodeId :: Int32 } deriving (Show, Eq, Deserializable, Num, Integral, Ord, Real, Enum, Generic)@@ -181,6 +196,8 @@ newtype GroupCoordinatorRequest = GroupCoordinatorReq ConsumerGroup deriving (Show, Eq, Serializable, Generic) newtype CreateTopicsRequest = CreateTopicsReq ([(TopicName, Partition, ReplicationFactor, [(Partition, Replicas)], [(KafkaString, Metadata)])], Timeout) deriving (Show, Eq, Serializable, Generic)+newtype DeleteTopicsRequest = DeleteTopicsReq ([TopicName], Timeout) deriving (Show, Eq, Serializable, Generic)+ newtype OffsetCommitRequest = OffsetCommitReq (ConsumerGroup, [(TopicName, [(Partition, Offset, Time, Metadata)])]) deriving (Show, Eq, Serializable, Generic) newtype OffsetFetchRequest = OffsetFetchReq (ConsumerGroup, [(TopicName, [Partition])]) deriving (Show, Eq, Serializable, Generic) newtype ConsumerGroup = ConsumerGroup KafkaString deriving (Show, Eq, Serializable, Deserializable, IsString, Generic)@@ -286,6 +303,7 @@ apiKey OffsetFetchRequest{} = ApiKey 9 apiKey GroupCoordinatorRequest{} = ApiKey 10 apiKey CreateTopicsRequest{} = ApiKey 19+apiKey DeleteTopicsRequest{} = ApiKey 20 instance Serializable RequestMessage where serialize (ProduceRequest r) = serialize r@@ -296,6 +314,7 @@ serialize (OffsetFetchRequest r) = serialize r serialize (GroupCoordinatorRequest r) = serialize r serialize (CreateTopicsRequest r) = serialize r+ serialize (DeleteTopicsRequest r) = serialize r instance Serializable Int64 where serialize = putWord64be . fromIntegral instance Serializable Int32 where serialize = putWord32be . fromIntegral
milena.cabal view
@@ -1,13 +1,13 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.1.+-- This file has been generated from package.yaml by hpack version 0.31.2. -- -- see: https://github.com/sol/hpack ----- hash: 8060a8b3f6bf4aad48a041ce65dd55877fe0f1bd5353f32359bae70b010f2843+-- hash: a7bdec96a93aec94bc09e9d89b957ff87d484a0d2f73237f8816de0a56cd03b4 name: milena-version: 0.5.3.0+version: 0.5.4.0 synopsis: A Kafka client for Haskell. description: A Kafka client for Haskell. The protocol module is stable (the only changes will be to support changes in the Kafka protocol). The API is functional but subject to change.
test/tests.hs view
@@ -2,23 +2,42 @@ module Main where -import Data.Functor+import Prelude++-- base import Data.Either (isRight, isLeft) import qualified Data.List.NonEmpty as NE++-- Hackage import Control.Lens import Control.Monad.Except (catchError, throwError) import Control.Monad.Trans (liftIO)-import Network.Kafka-import Network.Kafka.Consumer-import Network.Kafka.Producer-import Network.Kafka.Protocol (ProduceResponse(..), KafkaError(..), CompressionCodec(..), CreateTopicsResponse(..)) import Test.Tasty import Test.Tasty.Hspec import Test.Tasty.QuickCheck import qualified Data.ByteString.Char8 as B -import Prelude+-- local+import Network.Kafka+import Network.Kafka.Consumer+import Network.Kafka.Producer+import Network.Kafka.Protocol (ProduceResponse(..),+ KafkaError(..),+ CompressionCodec(..),+ CreateTopicsResponse(..),+ DeleteTopicsResponse(..),+ Offset(..),+ OffsetCommitResponse(..),+ OffsetFetchResponse(..),+ ConsumerGroup(..),+ Partition(..),+ KafkaString(..),+ Metadata(..),+ TopicName(..),+ ) ++ main :: IO () main = testSpec "the specs" specs >>= defaultMain @@ -32,6 +51,13 @@ stateWaitTime .= 1000 byteMessages = fmap (TopicAndMessage topic . makeMessage . B.pack) + let cleanup :: TopicName -> IO ()+ cleanup topicName = do+ run $ do+ stateAddresses %= NE.cons ("localhost", 9092)+ deleteTopic (deleteTopicsRequest topicName)+ pure ()+ describe "can talk to local Kafka server" $ do prop "can produce messages" $ \ms -> do result <- run . produceMessages $ byteMessages ms@@ -129,13 +155,76 @@ use stateAddresses result `shouldBe` fmap NE.nub result - describe "create topics" $++ let newTopicName = "milena-test-13-partitions"+ describe "create topics" $ do it "create topics with multiple partitions" $ do- let t = "milena-test-13-partitions" result <- run $ do stateAddresses %= NE.cons ("localhost", 9092)- createTopic (createTopicsRequest t 13 1 [] [])- result `shouldBe` (Right $ TopicsResp [(t, NoError)])+ createTopic (createTopicsRequest newTopicName 13 1 [] [])+ result `shouldBe` (Right $ TopicsResp [(newTopicName, NoError)])++ it "create already existing topic" $ do+ result <- run $ do+ stateAddresses %= NE.cons ("localhost", 9092)+ createTopic (createTopicsRequest newTopicName 13 1 [] [])+ result `shouldBe` (Right $ TopicsResp [(newTopicName, TopicAlreadyExists)])++ describe "delete topics" $+ it "delete topics" $ do+ result <- run $ do+ stateAddresses %= NE.cons ("localhost", 9092)+ deleteTopic (deleteTopicsRequest newTopicName)+ result `shouldBe` (Right $ DeleteTopicsResp [(newTopicName, NoError)])+++ let commitOffsetTopicName = "commit-offset"+ t = commitOffsetTopicName++ Test.Tasty.Hspec.afterAll_ (cleanup commitOffsetTopicName) $ do+ describe "can commit messages" $ do+ it "create a topic" $ do+ topicCreation <- run $ do+ stateAddresses %= NE.cons ("localhost", 9092)+ createTopic (createTopicsRequest t 3 1 [] [])+ topicCreation `shouldBe` (Right $ TopicsResp [(t, NoError)])++ it "commit offset 5 to partition 0 for consumer group \"group1\"" $ do+ commitOff <- run $ do+ stateAddresses %= NE.cons ("localhost", 9092)+ commitOffset (commitOffsetRequest (ConsumerGroup "group1") t 0 (Offset 5))+ commitOff `shouldBe` Right (OffsetCommitResp [(t,[(Partition 0,NoError)])])++ it "commit offset 15 to partition 1 for consumer group \"group1\"" $ do+ commitOff <- run $ do+ stateAddresses %= NE.cons ("localhost", 9092)+ commitOffset (commitOffsetRequest (ConsumerGroup "group1") t 1 (Offset 15))+ commitOff `shouldBe` Right (OffsetCommitResp [(t,[(Partition 1,NoError)])])++ it "commit offset 10 to partition 2 for consumer group \"group2\"" $ do+ commitOff <- run $ do+ stateAddresses %= NE.cons ("localhost", 9092)+ commitOffset (commitOffsetRequest (ConsumerGroup "group2") t 2 10)+ commitOff `shouldSatisfy` isRight++ it "fetch offset from partition 0 for \"group1\"" $ do+ fetchOff <- run $ do+ stateAddresses %= NE.cons ("localhost", 9092)+ fetchOffset (fetchOffsetRequest (ConsumerGroup "group1") t 0)+ fetchOff `shouldBe` Right (OffsetFetchResp [(t,[(Partition 0, Offset 5,Metadata (KString {_kString = ""}),NoError)])])++ it "fetch offset from partition 0 for \"group2\"" $ do+ fetchOff <- run $ do+ stateAddresses %= NE.cons ("localhost", 9092)+ fetchOffset (fetchOffsetRequest (ConsumerGroup "group2") t 0)+ fetchOff `shouldBe` Right (OffsetFetchResp [(t,[(Partition 0, Offset (-1),Metadata (KString {_kString = ""}),UnknownTopicOrPartition)])])++ it "note that getLastOffset is unchanged" $ do+ getLastOff <- run $ do+ stateAddresses %= NE.cons ("localhost", 9092)+ getLastOffset EarliestTime 0 t+ getLastOff `shouldSatisfy` isRight+ getLastOff `shouldBe` (Right $ Offset 0) prop :: Testable prop => String -> prop -> SpecWith () prop s = it s . property